commit 2e132878e466dd8c5a58a15455919312490f8707 Author: Ruben Fiszel Date: Thu May 5 04:25:58 2022 +0200 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..aa681c16c9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +frontend/node_modules/ +frontend/build/ +frontend/.svelte-kit/ + +backend/target/ diff --git a/.env b/.env new file mode 100644 index 0000000000..b1eb8f5226 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +SITE_URL=localhost +DB_PASSWORD=changeme +POSTGRES_VERSION=13.3.0 diff --git a/.github/Dockerfile b/.github/Dockerfile new file mode 100644 index 0000000000..951c5483ce --- /dev/null +++ b/.github/Dockerfile @@ -0,0 +1,7 @@ +FROM nikolaik/python-nodejs + +RUN npm install -g @apidevtools/swagger-cli +RUN pip install openapi-python-client +RUN pip install poetry + + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..dd84ea7824 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/change-versions.sh b/.github/change-versions.sh new file mode 100755 index 0000000000..1274107970 --- /dev/null +++ b/.github/change-versions.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +VERSION=$1 +echo "Updating versions to: $VERSION" + +sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" backend/Cargo.toml +sed -i -e "/version: /s/: .*/: $VERSION/" backend/openapi.yaml +sed -i -e "/\"version\": /s/: .*,/: \"$VERSION\",/" frontend/package.json +sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" python-client/wmill/pyproject.toml +sed -i -e "/^windmill-api =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill/pyproject.toml +sed -i -e "/^version =/s/= .*/= \"$VERSION\"/" python-client/wmill_pg/pyproject.toml +sed -i -e "/^wmill =/s/= .*/= \"\\^$VERSION\"/" python-client/wmill_pg/pyproject.toml +sed -i -e "/^wmill =/s/= .*/= \">=$VERSION\"/" Pipfile +sed -i -e "/^wmill_pg =/s/= .*/= \">=$VERSION\"/" Pipfile + +sed -i -zE "s/name = \"windmill\"\nversion = \"[^\"]*\"\\n(.*)/name = \"windmill\"\nversion = \"$VERSION\"\\n\\1/" backend/Cargo.lock diff --git a/.github/workflows/change-versions.yml b/.github/workflows/change-versions.yml new file mode 100644 index 0000000000..c9754af84f --- /dev/null +++ b/.github/workflows/change-versions.yml @@ -0,0 +1,14 @@ +name: Change versions +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "version.txt" +jobs: + change_version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Change versions + run: ./.github/change-versions.sh "$(cat version.txt)" + - uses: stefanzweifel/git-auto-commit-action@v4 diff --git a/.github/workflows/clean-docker.yml b/.github/workflows/clean-docker.yml new file mode 100644 index 0000000000..e1120b3aa1 --- /dev/null +++ b/.github/workflows/clean-docker.yml @@ -0,0 +1,13 @@ +name: Clean docker +on: + schedule: + # * is a special character in YAML so you have to quote this string + - cron: "0 0 */2 * *" + +jobs: + build: + runs-on: [self-hosted, new] + steps: + - name: clean docker + run: | + sudo docker system prune -f diff --git a/.github/workflows/deploy_to_windmill.yml b/.github/workflows/deploy_to_windmill.yml new file mode 100644 index 0000000000..215986d64e --- /dev/null +++ b/.github/workflows/deploy_to_windmill.yml @@ -0,0 +1,18 @@ +name: Deploy to windmill.dev + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Deploy to windmill.dev + uses: windmill-labs/windmill-gh-action-deploy@v1.0.0 + with: + dry_run: false + input_dir: community + windmill_workspace: starter + windmill_token: ${{ secrets.WINDMILL_API_TOKEN }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000000..182ae95751 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,39 @@ +name: Docker Image CI +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: [self-hosted, new] + env: + DOCKER_BUILDKIT: 1 + steps: + # - name: Wait for release to succeed + # if: github.ref == 'refs/heads/main' + # uses: lewagon/wait-on-check-action@v1.0.0 + # with: + # ref: ${{ github.ref }} + # check-name: "Release please" + # repo-token: ${{ secrets.GITHUB_TOKEN }} + # wait-interval: 10 + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: deploy staging stack + run: | + docker build . --cache-from "registry.wimill.xyz/windmill:staging" -t "registry.wimill.xyz/windmill:staging" --build-arg BUILDKIT_INLINE_CACHE=1 + docker push "registry.wimill.xyz/windmill:staging" + - name: deploy demo stack + if: github.ref == 'refs/heads/main' + run: | + docker tag registry.wimill.xyz/windmill:staging registry.wimill.xyz/windmill:main + docker push registry.wimill.xyz/windmill:main + # - name: pruning unused images + # run: sudo docker image prune -a diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml new file mode 100644 index 0000000000..1e3fa2dc64 --- /dev/null +++ b/.github/workflows/on-release.yml @@ -0,0 +1,38 @@ +name: Build LSP Docker +on: + push: + branches: [main] + paths: + - "python-client/**" + - "Pipfile" + - ".github/workflows/on-release.yml" + +jobs: + build_lsp: + runs-on: [self-hosted, new] + steps: + - name: Wait for release to succeed + if: github.ref == 'refs/heads/main' + uses: lewagon/wait-on-check-action@v1.0.0 + with: + ref: ${{ github.ref }} + check-name: "Release please" + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 10 + - uses: actions/checkout@v2 + - name: Upload python client + env: + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + cd python-client + export PATH=$PATH:/usr/local/bin + export PATH=$PATH:/root/.local/bin + ./publish.sh + - name: Build the Docker image + run: | + echo "branch main" + sudo docker pull "registry.wimill.xyz/lsp:main" || true + sudo docker build -f DockerfileLSP . --cache-from "registry.wimill.xyz/lsp:main" -t "registry.wimill.xyz/lsp:main" --build-arg BUILDKIT_INLINE_CACHE=1 + - name: push to registry + run: | + sudo docker push "registry.wimill.xyz/lsp:main" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000000..8b23cc11b8 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,15 @@ +on: + push: + branches: + - main +name: release-please +jobs: + release-please: + name: "Release please" + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/release-please-action@v2 + with: + release-type: simple + package-name: windmill + token: ${{ secrets.PAT_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..a20c8949e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +.DS_Store +local/ +frontend/src/routes/test.svelte +CaddyfileRemoteMalo diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..1cb5e3d51b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +## [1.5.0](https://www.github.com/windmill-labs/windmill-server/compare/v1.4.2...v1.5.0) (2022-05-02) + + +### Features + +* dynamic input ([2aac191](https://www.github.com/windmill-labs/windmill-server/commit/2aac1917c295f478c5d81ed0346857385e591ceb)) + +### [1.4.2](https://www.github.com/windmill-labs/windmill-server/compare/v1.4.1...v1.4.2) (2022-04-29) + + +### Bug Fixes + +* fix release ([13f8e39](https://www.github.com/windmill-labs/windmill-server/commit/13f8e39a11025b82c45dd16092df438f19d910e5)) + +### [1.4.1](https://www.github.com/windmill-labs/windmill-server/compare/v1.4.0...v1.4.1) (2022-04-29) + + +### Bug Fixes + +* base url python client ([b221bf0](https://www.github.com/windmill-labs/windmill-server/commit/b221bf01e30155007a0dcaac2799cc762a7b5f3b)) + +## [1.4.0](https://www.github.com/windmill-labs/windmill-server/compare/v1.3.0...v1.4.0) (2022-04-27) + + +### Features + +* variables backend logic ([9864028](https://www.github.com/windmill-labs/windmill-server/commit/9864028c648cec368f4541145e8b21e10d81627b)) +* variables backend logic ([d762f93](https://www.github.com/windmill-labs/windmill-server/commit/d762f93a0c75ad73229fd7a56ae3372ff2e8e41a)) +* variables backend logic ([3e567a8](https://www.github.com/windmill-labs/windmill-server/commit/3e567a8782d0377afde9a362d4bea6dff6cd5b3f)) + +## [1.3.0](https://www.github.com/windmill-labs/windmill-server/compare/v1.2.0...v1.3.0) (2022-04-27) + + +### Features + +* secret decryption is audited ([d5c5877](https://www.github.com/windmill-labs/windmill-server/commit/d5c58771e1ce0acf766843bc38d724044c905569)) + +## [1.2.0](https://www.github.com/windmill-labs/windmill-server/compare/v1.1.1...v1.2.0) (2022-04-15) + + +### Features + +* custom env ([a89e807](https://www.github.com/windmill-labs/windmill-server/commit/a89e807aa4f5ec3584285a1e9ef126a0a77cf766)) + +### [1.1.1](https://www.github.com/windmill-labs/windmill-server/compare/v1.1.0...v1.1.1) (2022-03-14) + + +### Bug Fixes + +* pg commit ([35967d5](https://www.github.com/windmill-labs/windmill-server/commit/35967d50461a5a53ca3d975e908f74ae54c02100)) + +## [1.1.0](https://www.github.com/windmill-labs/windmill-server/compare/v1.0.0...v1.1.0) (2022-03-13) + + +### Features + +* query_pg is in integrated into wmill ([2e36244](https://www.github.com/windmill-labs/windmill-server/commit/2e3624447216009e1e8ec1ba416952697a0ef4d4)) + +## 1.0.0 (2022-02-06) + + +### Features + +* functioning workspaces ([38de8c3](https://www.github.com/windmill-labs/windmill/commit/38de8c3f572f3bd3b8a3252079d930572723ab8a)) + +## [0.9.0](https://www.github.com/windmill-labs/windmill/compare/v0.0.1...v0.9.0) (2021-12-30) + +### Features + +- Renaming to windmill diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000000..f694d3fc41 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,4 @@ +{$SITE_URL} { + bind {$ADDRESS} + reverse_proxy /* server:8000 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..b484f5b9aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,99 @@ +FROM python:3.10-slim-buster as nsjail + +WORKDIR /nsjail + +RUN apt-get -y update \ + && apt-get install -y \ + bison=2:3.3.* \ + flex=2.6.* \ + g++=4:8.3.* \ + gcc=4:8.3.* \ + git=1:2.20.* \ + libprotobuf-dev=3.6.* \ + libnl-route-3-dev=3.4.* \ + make=4.2.* \ + pkg-config=0.29-6 \ + protobuf-compiler=3.6.* + +RUN git clone -b master --single-branch https://github.com/google/nsjail.git . \ + && git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800 +RUN make + +FROM mhart/alpine-node:14 as frontend + +# install dependencies +WORKDIR /frontend +COPY ./frontend/package.json ./frontend/package-lock.json ./ +RUN npm ci + +# Copy all local files into the image. +COPY frontend . +RUN mkdir /backend +COPY /backend/openapi.yaml /backend/openapi.yaml +RUN npm run generate-backend-client +RUN npm run build + +FROM rust:slim-buster as builder + +RUN apt-get update && apt-get install -y git libssl-dev pkg-config + +RUN USER=root cargo new --bin windmill +WORKDIR /windmill + +COPY ./backend/Cargo.toml . +COPY ./backend/Cargo.lock . +COPY ./backend/.cargo/ .cargo/ + +RUN apt-get -y update \ + && apt-get install -y \ + curl + +ENV CARGO_INCREMENTAL=1 + +RUN cargo build --release +RUN rm src/*.rs + +RUN rm ./target/release/deps/windmill* +ENV SQLX_OFFLINE=true + +ADD ./backend ./ +ADD ./nsjail /nsjail + +COPY --from=1 /frontend /frontend +ADD .git/ .git/ + +RUN cargo build --release + + +FROM debian:buster-slim +ARG APP=/usr/src/app + +RUN apt-get update \ + && apt-get install -y ca-certificates tzdata libpq5 python3 python3-pip \ + make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \ + libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libxml2-dev \ + libxmlsec1-dev libffi-dev liblzma-dev mecab-ipadic-utf8 libgdbm-dev libc6-dev git libprotobuf-dev=3.6.* libnl-route-3-dev=3.4.* \ + libv8-dev \ + && rm -rf /var/lib/apt/lists/* + +ENV TZ=Etc/UTC + +ENV PYTHON_VERSION 3.10.4 + +RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz \ + && tar -xf Python-${PYTHON_VERSION}.tgz && cd Python-${PYTHON_VERSION}/ && ./configure --enable-optimizations \ + && make -j 4 && make install + +RUN python3 -m pip install pip-tools + +COPY --from=builder /windmill/target/release/windmill ${APP}/windmill + +COPY --from=nsjail /nsjail/nsjail /bin/nsjail + +RUN mkdir -p ${APP} + +WORKDIR ${APP} + +EXPOSE 8000 + +CMD ["./windmill"] diff --git a/DockerfileLSP b/DockerfileLSP new file mode 100644 index 0000000000..095d920115 --- /dev/null +++ b/DockerfileLSP @@ -0,0 +1,16 @@ +FROM nikolaik/python-nodejs + +RUN yarn global add diagnostic-languageserver +RUN yarn global add pyright +RUN pip3 install black tornado python-lsp-jsonrpc + +COPY Pipfile . + +RUN cat Pipfile + +RUN pipenv install + +COPY pyls_launcher.py . + +CMD ["python3" ,"pyls_launcher.py"] + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..4824a1f07f --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ + +Source code in this repository is variously licensed under the Apache License +Version 2.0 (see file ./LICENSE-APACHE),or the AGPLv3 License (see file ./LICENSE-AGPL) + +Every file is under copyright (c) Ruben Fiszel 2021 unless otherwise specified. +Every file is under License AGPL unless otherwise specified +or belonging to one of the below cases: + +The files under backend/ are AGPL Licensed. +The files under frontend/ are AGPL Licensed. +The files under python-client/ are Apache 2.0 Licensed. +The files under community/ are Apache 2.0 Licensed. diff --git a/LICENSE-AGPL b/LICENSE-AGPL new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/LICENSE-AGPL @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000000..e63c8fad02 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Ruben Fiszel + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..353e398e2d --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +Ruben Fiszel + +Copyright (c) 2021 Ruben Fiszel + +Source code in this repository is variously licensed under the Apache License +Version 2.0 or the GNU Affero General Public License. Please see +LICENSE for more information. + +* For a copy of the Apache License Version 2.0, please see LICENSE-APACHE + as included in this repository's top-level directory. + +* For a copy of the GNU Affero General Public License, please see LICENSE-ALPH + as included in this repository's top-level directory. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000000..2f5e252d09 --- /dev/null +++ b/Pipfile @@ -0,0 +1,23 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +wmill = ">=1.5.0" +wmill_pg = ">=1.5.0" +requests = "*" +sendgrid = "*" +psycopg2-binary = "*" +mysql-connector-python = "*" +pymongo = "*" +slack_sdk = "*" +google-api-python-client = "*" +pandas = "*" +numpy = "*" +seaborn = "*" +yfinance = "*" +pyowm = "*" +pyairtable = "*" +matplotlib = "*" + diff --git a/README.md b/README.md new file mode 100644 index 0000000000..c1b47b2ae2 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +

+ windmill.dev +

+

+ Windmill.dev is an OSS developer platform to quickly build production-grade multi-steps automations and internal apps from minimal Python and Typescript scripts. +

+

+ + Package version + + + Discord Shield + +

+ +--- + +**Join the alpha (personal workspaces are free forever)**: + + +**Documentation**: + +**Discord**: + +**We are hiring**: Software Engineers, DevOps, Solutions Engineers, Growth: + + +If you would like to, you can show your support for the project by starring this +repo. + +--- + +# Windmill + +![Windmill](./windmill.webp) + +Windmill is fully open-sourced: + +- community parts and python-client are Apache 2.0 +- backend, frontend and everything else under AGPLv3. + +## Stack + +- postgres as the database +- backend in Rust with the follwing highly-available and horizontally scalable + architecture: + - stateless API backend + - workers that pull jobs from a queue +- frontend in svelte +- scripts executions are sandboxed using google's nsjail +- javascript runtime is deno_core rust library (which itself uses the rusty_v8 + and hence V8 underneath) +- typescript runtime is deno +- python runtime is python3 + +### Developent stack + +- caddy is the reverse proxy + handle https + +## How to self-host + +Complete instructions coming soon + +## Copyright + +2021 [Ruben Fiszel](https://github.com/rubenfiszel) + +## Acknowledgement + +This project is inspired from a previous project called +[Delightool](https://github.com/windmill-labs/delightool-legacy) which was also +build by [Ruben](https://github.com/rubenfiszel) but the frontend was realized +in large parts by [Malo Marrec]((https://github.com/malomarrec). Windmill is a +child but distinct project and realized with Malo's blessing. diff --git a/backend/.cargo/config b/backend/.cargo/config new file mode 100644 index 0000000000..33306abf17 --- /dev/null +++ b/backend/.cargo/config @@ -0,0 +1,3 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"] +incremental = true diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000000..14ee5009d7 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +target/ +.env diff --git a/backend/Cargo.lock b/backend/Cargo.lock new file mode 100644 index 0000000000..c10e25a952 --- /dev/null +++ b/backend/Cargo.lock @@ -0,0 +1,4170 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", + "opaque-debug 0.3.0", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.6", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" + +[[package]] +name = "argon2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii-canvas" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8eb72df928aafb99fe5d37b383f2fe25bd2a765e3e5f7c365916b6f2463a29" +dependencies = [ + "term", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-timer" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5fa6ed76cb2aa820707b4eb9ec46f42da9ce70b0eafab5e5e34942b38a44d5" +dependencies = [ + "libc", + "wasm-bindgen", + "winapi 0.3.9", +] + +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits", +] + +[[package]] +name = "attohttpc" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" +dependencies = [ + "http", + "log", + "url", + "wildmatch", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47594e438a243791dba58124b6669561f5baa14cb12046641d8008bf035e5a25" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes 1.1.0", + "futures-util", + "headers", + "http", + "http-body 0.4.4", + "hyper 0.14.18", + "itoa 1.0.1", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite 0.2.8", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio 1.17.0", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a671c9ae99531afdd5d3ee8340b8da547779430689947144c140fc74a740244" +dependencies = [ + "async-trait", + "bytes 1.1.0", + "futures-util", + "http", + "http-body 0.4.4", + "mime", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "base64ct" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest 0.10.3", +] + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding 0.1.5", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding 0.2.1", + "generic-array 0.14.5", +] + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding 0.2.1", + "cipher", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "js-sys", + "libc", + "num-integer", + "num-traits", + "serde", + "time 0.1.43", + "wasm-bindgen", + "winapi 0.3.9", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "console-api" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cb05777feccbb2642d4f2df44d0505601a2cd88ca517d8c913f263a5a8dc8b" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bd6b23feb4180ccf20cefca9357262818443aafb7159b5e503d170d442a872" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio 1.17.0", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +dependencies = [ + "percent-encoding", + "time 0.3.9", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-any" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3fc9f32c34de51ba0a727fd84d2cc83587efeca519cfca1105e3efa9c1c78fb" +dependencies = [ + "debug-helper", +] + +[[package]] +name = "crc-catalog" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "cron" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76219e9243e100d5a37676005f08379297f8addfebc247613299600625c734d" +dependencies = [ + "chrono", + "nom", + "once_cell", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array 0.14.5", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + +[[package]] +name = "deno_core" +version = "0.110.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffca9c7f3b54e5a63124fc13825d70f20f1c286fcb7440922f86566bc65edd99" +dependencies = [ + "anyhow", + "futures", + "indexmap", + "lazy_static", + "libc", + "log", + "parking_lot 0.11.2", + "pin-project", + "serde", + "serde_json", + "serde_v8", + "url", + "v8", +] + +[[package]] +name = "des" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac41dd49fb554432020d52c875fc290e110113f864c6b1b525cd62c7e7747a5d" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug 0.3.0", +] + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", + "subtle 2.4.1", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users 0.3.5", + "winapi 0.3.9", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.3", + "winapi 0.3.9", +] + +[[package]] +name = "docopt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" +dependencies = [ + "lazy_static", + "regex", + "serde", + "strsim", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +dependencies = [ + "serde", +] + +[[package]] +name = "email-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6690291166824e467790ac08ba42f241791567e8337bbf00c5a6e87889629f98" +dependencies = [ + "base64", +] + +[[package]] +name = "ena" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8944dc8fa28ce4a38f778bd46bf7d923fe73eed5a439398507246c8e017e6f36" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enum-as-inner" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "570d109b813e904becc80d8d5da38376818a143348413f7149f1340fe04754d4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "external-ip" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2406194c5c4be3678bd7c1c128237ec589a6a3b7a3b05786971998bda7693c27" +dependencies = [ + "futures", + "http", + "hyper 0.14.18", + "hyper-tls 0.5.0", + "igd", + "log", + "rand 0.8.5", + "trust-dns-resolver", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.13", + "winapi 0.3.9", +] + +[[package]] +name = "fixedbitset" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33" + +[[package]] +name = "flate2" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fslock" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57eafdd0c16f57161105ae1b98a1238f97645f2f588438b2949c99a2af9616bf" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.8", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "git-version" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" +dependencies = [ + "git-version-macro", + "proc-macro-hack", +] + +[[package]] +name = "git-version-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio 0.2.25", + "tokio-util 0.3.1", + "tracing", + "tracing-futures", +] + +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes 1.1.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio 1.17.0", + "tokio-util 0.7.1", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31672b7011be2c4f7456c4ddbcb40e7e9a4a9fad8efe49a6ebaf5f307d0109c0" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "headers" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +dependencies = [ + "base64", + "bitflags", + "bytes 1.1.0", + "headers-core", + "http", + "httpdate 1.0.2", + "mime", + "sha-1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" +dependencies = [ + "crypto-mac", + "digest 0.8.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.3", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi 0.3.9", +] + +[[package]] +name = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes 1.1.0", + "fnv", + "itoa 1.0.1", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes 0.5.6", + "http", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes 1.1.0", + "http", + "pin-project-lite 0.2.8", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[package]] +name = "httparse" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +dependencies = [ + "bytes 0.5.6", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.2.7", + "http", + "http-body 0.3.1", + "httparse", + "httpdate 0.3.2", + "itoa 0.4.8", + "pin-project", + "socket2 0.3.19", + "tokio 0.2.25", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +dependencies = [ + "bytes 1.1.0", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.13", + "http", + "http-body 0.4.4", + "httparse", + "httpdate 1.0.2", + "itoa 1.0.1", + "pin-project-lite 0.2.8", + "socket2 0.4.4", + "tokio 1.17.0", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +dependencies = [ + "http", + "hyper 0.14.18", + "rustls 0.20.4", + "tokio 1.17.0", + "tokio-rustls 0.23.3", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.18", + "pin-project-lite 0.2.8", + "tokio 1.17.0", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +dependencies = [ + "bytes 0.5.6", + "hyper 0.13.10", + "native-tls", + "tokio 0.2.25", + "tokio-tls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.1.0", + "hyper 0.14.18", + "native-tls", + "tokio 1.17.0", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "igd" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4e7ee8b51e541486d7040883fe1f00e2a9954bcc24fd155b7e4f03ed4b93dd" +dependencies = [ + "attohttpc", + "log", + "rand 0.8.5", + "url", + "xmltree", +] + +[[package]] +name = "indexmap" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +dependencies = [ + "autocfg 1.1.0", + "hashbrown 0.9.1", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +dependencies = [ + "socket2 0.3.19", + "widestring", + "winapi 0.3.9", + "winreg 0.6.2", +] + +[[package]] +name = "ipnet" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" + +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-pointer" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe841b94e719a482213cee19dd04927cf412f26d8dc84c5a446c081e49c2997" +dependencies = [ + "serde_json", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lalrpop" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64dc3698e75d452867d9bd86f4a723f452ce9d01fe1d55990b79f0c790aa67db" +dependencies = [ + "ascii-canvas", + "atty", + "bit-set", + "diff", + "docopt", + "ena", + "itertools 0.8.2", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax", + "serde", + "serde_derive", + "sha2 0.8.2", + "string_cache", + "term", + "unicode-xid 0.1.0", +] + +[[package]] +name = "lalrpop-util" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c277d18683b36349ab5cd030158b54856fca6bb2d5dc5263b06288f486958b7c" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lettre" +version = "0.10.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5144148f337be14dabfc0f0d85b691a68ac6c77ef22a5c47c5504b70a7c9fcf3" +dependencies = [ + "async-trait", + "base64", + "email-encoding", + "fastrand", + "futures-io", + "futures-util", + "httpdate 1.0.2", + "idna", + "mime", + "nom", + "once_cell", + "quoted_printable", + "regex", + "rustls 0.20.4", + "rustls-pemfile", + "tokio 1.17.0", + "tokio-rustls 0.23.3", + "webpki-roots 0.22.3", +] + +[[package]] +name = "libc" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg 1.1.0", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "magic-crypt" +version = "3.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c913782c21b53ad246863641fffbaf73a9eb32ff0d939b10d361b7294e2ea9c" +dependencies = [ + "aes", + "base64", + "block-modes", + "crc-any", + "des", + "digest 0.9.0", + "md-5 0.9.1", + "sha2 0.9.9", + "tiger", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "md-5" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658646b21e0b72f7866c7038ab086d3d5e1cd6271f060fd37defb241949d0582" +dependencies = [ + "digest 0.10.3", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow 0.3.7", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg 1.1.0", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.1.0", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +dependencies = [ + "libc", +] + +[[package]] +name = "oauth2" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e47cfc4c0a1a519d9a025ebfbac3a2439d1b5cdf397d72dcb79b11d9920dab" +dependencies = [ + "base64", + "chrono", + "getrandom 0.2.6", + "http", + "rand 0.8.5", + "reqwest 0.11.10", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.9.9", + "thiserror", + "url", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg 1.1.0", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordermap" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a86ed3f5f244b372d6b1a00b72ef7f8876d0bc6a78a4c9985c53614041512063" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.2", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.13", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.13", + "smallvec", + "windows-sys", +] + +[[package]] +name = "password-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa26fd5c3cd6e6bb83dd9c0cef40fbeb77d7596339ca46c18a6f66919bb07769" +dependencies = [ + "base64ct", + "rand_core 0.6.3", + "subtle 2.4.1", +] + +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "petgraph" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f" +dependencies = [ + "fixedbitset", + "ordermap", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared", + "rand 0.6.5", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid 0.2.2", +] + +[[package]] +name = "prost" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07b0857a71a8cb765763950499cae2413c3f9cede1133478c43600d9e146890" +dependencies = [ + "bytes 1.1.0", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" +dependencies = [ + "anyhow", + "itertools 0.10.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" +dependencies = [ + "bytes 1.1.0", + "prost", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f" + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.6", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.6", + "redox_syscall 0.2.13", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +dependencies = [ + "base64", + "bytes 0.5.6", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body 0.3.1", + "hyper 0.13.10", + "hyper-tls 0.4.3", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.8", + "serde", + "serde_urlencoded", + "tokio 0.2.25", + "tokio-tls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.7.0", +] + +[[package]] +name = "reqwest" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +dependencies = [ + "base64", + "bytes 1.1.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.13", + "http", + "http-body 0.4.4", + "hyper 0.14.18", + "hyper-rustls", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.8", + "rustls 0.20.4", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio 1.17.0", + "tokio-native-tls", + "tokio-rustls 0.23.3", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.22.3", + "winreg 0.10.1", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "retainer" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8c01a8276c14d0f8d51ebcf8a48f0748f9f73f5f6b29e688126e6a52bcb145" +dependencies = [ + "async-lock", + "async-timer", + "log", + "rand 0.8.5", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rust-embed" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a17e5ac65b318f397182ae94e532da0ba56b88dd1200b774715d36c4943b1c3" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e763e24ba2bf0c72bc6be883f967f794a019fafd1b86ba1daff9c91a7edd30" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756feca3afcbb1487a1d01f4ecd94cf8ec98ea074c55a69e7136d29fb6166029" +dependencies = [ + "sha2 0.9.9", + "walkdir", +] + +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +dependencies = [ + "log", + "ring", + "sct 0.7.0", + "webpki 0.22.0", +] + +[[package]] +name = "rustls-pemfile" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" +dependencies = [ + "base64", +] + +[[package]] +name = "rustpython-parser" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85b1038ecd8791bdae455ae784b48f522ebeca1b3323d0af2b251c5c8ff1c68" +dependencies = [ + "lalrpop", + "lalrpop-util", + "log", + "num-bigint", + "num-traits", + "unic-emoji-char", + "unic-ucd-ident", + "unicode_names2", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "indexmap", + "itoa 1.0.1", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7868ad3b8196a8a0aea99a8220b124278ee5320a55e4fde97794b6f85b1a377" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.1", + "ryu", + "serde", +] + +[[package]] +name = "serde_v8" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6217b505e32edf22e28516ac16ad36dc783f13053311f5fa11fbe95d47ec633" +dependencies = [ + "serde", + "serde_bytes", + "v8", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "slack-http-verifier" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2848741518fc1c58c7c28b0c4136e62a523438b3700311f7a1faffd37e756f8" +dependencies = [ + "crypto-mac", + "hex", + "hmac 0.7.1", + "http", + "reqwest 0.10.10", + "sha2 0.8.2", +] + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "sql-builder" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1008d95d2ec2d062959352527be30e10fec42a1aa5e5a48d990a5ff0fb9bdc0" +dependencies = [ + "anyhow", + "thiserror", +] + +[[package]] +name = "sqlformat" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" +dependencies = [ + "itertools 0.10.3", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2be6e4d8b60285763c7a0c17f57839d5f1e864d948fe7ff26eaf38f04db95fc" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5728d5a4de38574e1a43139cf1d0124e978af8c19ea795d1d61c287785924cf2" +dependencies = [ + "ahash", + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes 1.1.0", + "chrono", + "crc", + "crossbeam-queue", + "dirs 4.0.0", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "hkdf", + "hmac 0.12.1", + "indexmap", + "itoa 1.0.1", + "libc", + "log", + "md-5 0.10.1", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rand 0.8.5", + "rustls 0.19.1", + "serde", + "serde_json", + "sha-1", + "sha2 0.10.2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "uuid", + "webpki 0.21.4", + "webpki-roots 0.21.1", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce1fd7df05de96f76eb0d452e5ce8b106607933ac3c9a8f5c65c1ecc5955f5" +dependencies = [ + "dotenv", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2cc78368bdd33ba6ad1c300f2b9ff4a844f9d7db140c34bc61ef304e1fc160" +dependencies = [ + "once_cell", + "tokio 1.17.0", + "tokio-rustls 0.22.0", +] + +[[package]] +name = "string_cache" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c058a82f9fd69b1becf8c274f412281038877c553182f1d02eb027045a2d67" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", + "serde", + "string_cache_codegen", + "string_cache_shared", +] + +[[package]] +name = "string_cache_codegen" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f45ed1b65bf9a4bf2f7b7dc59212d1926e9eaf00fa998988e420fd124467c6" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "string_cache_shared", +] + +[[package]] +name = "string_cache_shared" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc" + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid 0.2.2", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "libc", + "redox_syscall 0.2.13", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "term" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" +dependencies = [ + "byteorder", + "dirs 1.0.5", + "winapi 0.3.9", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tiger" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443e531cbcf9de83258cfef70bcd56c91188de5819ebd4b19c85f589e0617005" +dependencies = [ + "block-buffer 0.9.0", + "byteorder", + "digest 0.9.0", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa 1.0.1", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "memchr", + "mio 0.6.23", + "num_cpus", + "pin-project-lite 0.1.12", + "slab", +] + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes 1.1.0", + "libc", + "memchr", + "mio 0.8.2", + "num_cpus", + "once_cell", + "parking_lot 0.12.0", + "pin-project-lite 0.2.8", + "signal-hook-registry", + "socket2 0.4.4", + "tokio-macros", + "tracing", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite 0.2.8", + "tokio 1.17.0", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio 1.17.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls 0.19.1", + "tokio 1.17.0", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4151fda0cf2798550ad0b34bcfc9b9dcc2a9d2471c895c68f3a8818e54f2389e" +dependencies = [ + "rustls 0.20.4", + "tokio 1.17.0", + "webpki 0.22.0", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.8", + "tokio 1.17.0", +] + +[[package]] +name = "tokio-tar" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50188549787c32c1c3d9c8c71ad7e003ccf2f102489c5a96e385c84760477f4" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.2.13", + "tokio 1.17.0", + "tokio-stream", + "xattr", +] + +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.12", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes 1.1.0", + "futures-core", + "futures-sink", + "pin-project-lite 0.2.8", + "tokio 1.17.0", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30fb54bf1e446f44d870d260d99957e7d11fb9d0a0f5bd1a662ad1411cc103f9" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes 1.1.0", + "futures-core", + "futures-util", + "h2 0.3.13", + "http", + "http-body 0.4.4", + "hyper 0.14.18", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio 1.17.0", + "tokio-stream", + "tokio-util 0.7.1", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tower" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite 0.2.8", + "rand 0.8.5", + "slab", + "tokio 1.17.0", + "tokio-util 0.7.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-cookies" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf1904fcf965b948ece576eae3cf85e5cad94f99acb6b5834148b61fbecd38e" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot 0.12.0", + "pin-project-lite 0.2.8", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba3f3efabf7fb41fae8534fc20a817013dd1c12cb45441efb6c82e6556b4cd8" +dependencies = [ + "bitflags", + "bytes 1.1.0", + "futures-core", + "futures-util", + "http", + "http-body 0.4.4", + "http-range-header", + "pin-project-lite 0.2.8", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.8", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +dependencies = [ + "lazy_static", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +dependencies = [ + "ansi_term", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "trust-dns-proto" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca94d4e9feb6a181c690c4040d7a24ef34018d8313ac5044a61d21222ae24e31" +dependencies = [ + "async-trait", + "cfg-if 1.0.0", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "lazy_static", + "log", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio 1.17.0", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecae383baad9995efaa34ce8e57d12c3f305e545887472a492b838f4b5cfb77a" +dependencies = [ + "cfg-if 1.0.0", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot 0.11.2", + "resolv-conf", + "smallvec", + "thiserror", + "tokio 1.17.0", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "ulid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220b18413e1fe5e85a5580b22f44241f82404a66c792c9f3c9eda74c52d9a22e" +dependencies = [ + "chrono", + "rand 0.8.5", + "uuid", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-emoji-char" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "unicode_names2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87d6678d7916394abad0d4b19df4d3802e1fd84abd7d701f39b75ee71b9e8cf1" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.6", + "serde", +] + +[[package]] +name = "v8" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "506523e86ccc15982be412bdde87a142771c139e94a8ecedda1da051a079b81d" +dependencies = [ + "bitflags", + "fslock", + "lazy_static", + "libc", + "which", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki 0.21.4", +] + +[[package]] +name = "webpki-roots" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" +dependencies = [ + "webpki 0.22.0", +] + +[[package]] +name = "which" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +dependencies = [ + "either", + "lazy_static", + "libc", +] + +[[package]] +name = "whoami" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "wildmatch" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windmill" +version = "1.5.0" +dependencies = [ + "anyhow", + "argon2", + "async-recursion", + "axum", + "chrono", + "console-subscriber", + "cron", + "deno_core", + "dotenv", + "external-ip", + "futures", + "git-version", + "headers", + "hex", + "hyper 0.14.18", + "indexmap", + "itertools 0.10.3", + "json-pointer", + "lettre", + "magic-crypt", + "mime_guess", + "oauth2", + "rand 0.8.5", + "rand_core 0.6.3", + "regex", + "reqwest 0.11.10", + "retainer", + "rust-embed", + "rustpython-parser", + "serde", + "serde_json", + "serde_urlencoded", + "slack-http-verifier", + "sql-builder", + "sqlx", + "tempfile", + "thiserror", + "time 0.3.9", + "tokio 1.17.0", + "tokio-tar", + "tokio-util 0.7.1", + "tower", + "tower-cookies", + "tower-http", + "tracing", + "tracing-subscriber", + "ulid", + "url", + "urlencoding", + "uuid", +] + +[[package]] +name = "windows-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000000..d5d21cc89f --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "windmill" +version = "1.5.0" +authors = ["Ruben Fiszel "] +edition = "2021" + +[build-dependencies] +deno_core = "^0" + +[dependencies] +axum = { version = "^0", features = ["headers"] } +headers = "^0" +hyper = { version = "^0", features = ["full"] } +tokio = { version = "^1", features = ["full", "tracing"] } +tower = "^0" +tower-http = { version = "^0", features = ["trace"] } +tower-cookies = "^0" +serde = "^1" +serde_json = { version = "^1", features = ["preserve_order"] } +uuid = { version = "^0", features = ["serde", "v4"] } +thiserror = "^1" +anyhow = "^1" +chrono = { version = "^0", features = ["serde"]} +tracing = "^0" +tracing-subscriber = { version = "^0", features = ["env-filter", "json"]} +console-subscriber = "^0" + +rust-embed = "^6" +mime_guess = "^2" +hex = "^0" +sql-builder = "^3" +argon2 = "^0" +retainer = "^0" +rand = "^0.8.4" +rand_core = { version = "^0.6.3", features = ["std"] } +magic-crypt = "^3" +git-version = "^0" +rustpython-parser = "^0" +cron = "^0" +external-ip = "^4" +lettre = { version = "^0.10.0-rc.4", features = ["rustls-tls", "tokio1", "tokio1-rustls-tls", "builder", "smtp-transport"], default-features = false} +urlencoding = "^2" +oauth2 = "^4" +url = "^2" +reqwest = { version = "^0", features = ["json"] } +time = "0.3.7" +slack-http-verifier = "^0" +serde_urlencoded = "^0" +tokio-tar = "^0" +tempfile = "^3" +tokio-util = { version = "0.7.0", features = ["io"] } +json-pointer = "^0" +itertools = "^0" +regex = "^1" +deno_core = "^0" +indexmap = "~1.6.2" +async-recursion = "^1" + +sqlx = { version = "^0", features = ["macros", "offline", "migrate", "uuid", "json", "chrono", "postgres", "runtime-tokio-rustls"]} +dotenv = "^0" +ulid = { version = "^0", features = ["uuid"] } +futures = "^0" diff --git a/backend/LICENSE b/backend/LICENSE new file mode 100644 index 0000000000..1bc5dfa959 --- /dev/null +++ b/backend/LICENSE @@ -0,0 +1,95 @@ +Business Source License 1.1 + +Parameters + +Licensor: Ruben Fiszel +Licensed Work: windmill.dev backend 0.9.0 + The Licensed Work is (c) 2021 Ruben Fiszel +Additional Use Grant: None + +Change Date: 2026-01-01 + +Change License: Apache License, Version 2.0 + +For information about alternative licensing arrangements for the Software, +please visit: https://windmill.dev + +Notice + +The Business Source License (this document, or the “License”) is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. + +Covenants of Licensor + +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. + +3. To specify a Change Date. + +4. Not to modify this License in any other way. diff --git a/backend/build.rs b/backend/build.rs new file mode 100644 index 0000000000..aca8d19f2c --- /dev/null +++ b/backend/build.rs @@ -0,0 +1,17 @@ +use std::fs::File; +use std::io::Write; + +use deno_core::{JsRuntime, RuntimeOptions}; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + let options = RuntimeOptions { + will_snapshot: true, + ..Default::default() + }; + let mut runtime = JsRuntime::new(options); + + let mut snap = File::create("v8.snap").expect("can create snap file"); + snap.write_all(&runtime.snapshot()) + .expect("can write content to snap"); +} diff --git a/backend/migrations/.gitkeep b/backend/migrations/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/migrations/20220123221903_first.down.sql b/backend/migrations/20220123221903_first.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/migrations/20220123221903_first.up.sql b/backend/migrations/20220123221903_first.up.sql new file mode 100644 index 0000000000..52d7d23cc7 --- /dev/null +++ b/backend/migrations/20220123221903_first.up.sql @@ -0,0 +1,575 @@ +-- Add migration script here +create SCHEMA IF NOT exists extensions; +create extension if not exists "uuid-ossp" with schema extensions; + +CREATE TABLE workspace ( + id VARCHAR(50) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + owner VARCHAR(50) NOT NULL, + domain VARCHAR(30), + deleted BOOLEAN NOT NULL DEFAULT false, + CONSTRAINT proper_id CHECK (id ~ '^\w+(-\w+)*$') +); + +INSERT INTO workspace(id, name, owner) VALUES + ('starter', 'Starter', 'admin@windmill.dev'), + ('demo', 'Demo', 'admin@windmill.dev'); + +CREATE TABLE script ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + hash BIGINT NOT NULL, + path varchar(255) NOT NULL, + parent_hashes BIGINT[], + summary TEXT NOT NULL, + description TEXT NOT NULL, + content TEXT NOT NULL, + created_by VARCHAR(50) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + archived BOOLEAN NOT NULL DEFAULT false, + schema JSONB, + deleted BOOLEAN NOT NULL DEFAULT false, + is_template boolean DEFAULT false, + PRIMARY KEY (workspace_id, hash), + CONSTRAINT proper_id CHECK (path ~ '^[ug](\/[\w-]+){2,}$') +); + +CREATE TABLE flow ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + path varchar(255) NOT NULL, + summary TEXT NOT NULL, + description TEXT NOT NULL, + value JSONB NOT NULL, + edited_by VARCHAR(50) NOT NULL, + edited_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + archived BOOLEAN NOT NULL DEFAULT false, + schema JSONB, + PRIMARY KEY (workspace_id, path), + CONSTRAINT proper_id CHECK (path ~ '^[ug](\/[\w-]+){2,}$') +); + +INSERT INTO script(workspace_id, created_by, content, schema, summary, description, path, hash) VALUES ( +'starter', +'system', 'import wmill +import psycopg2 + +client = wmill.Client() + +def main(): + # query that returns rows will return them as a list + res1 = query_pg("SELECT * from demo", "g/all/demodb") + + # query that does not return rows will return None + res2 = query_pg("UPDATE demo SET value = ''value''", "g/all/demodb") + + # one can use RETURNING to still fetch the updated rows + res3 = query_pg("UPDATE demo SET value = ''value'' RETURNING *", "g/all/demodb") + + # output expects a dict + return {"res1": res1, "res2": res2, "res3": res3} + + + +def query_pg(query: str, resource: str): + pg_con = client.get_resource(resource) + conn = psycopg2.connect(**pg_con) + cur = conn.cursor() + cur.execute(f"{query};") + if cur.description: + return cur.fetchall() + else: + return None', +'{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": {}, + "required": [], + "type": "object" +}', +'Query the demodb resource',' +An example of how to use resources from scripts. In this example, we will query the demo database demodb that is set up by default on Windmill.', +'u/bot/postgres_example', 43), +( +'starter', +'system', +'import os + +def main(name: str = "Nicolas Bourbaki"): + print(f"Hello World and a warm welcome especially to {name}") + print("The env variable at `g/all/pretty_secret`: ", os.environ.get("G_ALL_PRETTY_SECRET")) + return {"len": len(name), "splitted": name.split() }', +'{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "name": { + "description": "", + "type": "string", + "default": "Nicolas Bourbaki" + } + }, + "required": [], + "type": "object" +}', +'Hello World', '', 'u/bot/hello_world', 44); + + +CREATE INDEX index_script_on_path_created_at ON script (path, created_at); + +CREATE TYPE JOB_KIND AS ENUM ('script', 'preview', 'flow', 'dependencies'); + +CREATE TABLE queue ( + id UUID PRIMARY KEY, + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + parent_job UUID, + created_by VARCHAR(50) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + started_at TIMESTAMP WITH TIME ZONE, + scheduled_for TIMESTAMP WITH TIME ZONE NOT NULL, + running BOOLEAN NOT NULL DEFAULT FALSE, + script_hash BIGINT, + script_path VARCHAR(255), + args JSONB, + logs TEXT, + raw_code TEXT, + canceled boolean NOT NULL DEFAULT false, + canceled_by VARCHAR(50), + canceled_reason TEXT, + last_ping TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + job_kind JOB_KIND NOT NULL DEFAULT 'script', + env_id INTEGER, + schedule_path VARCHAR(255), + permissioned_as VARCHAR(55) NOT NULL DEFAULT 'g/all', + flow_status JSONB +); + +CREATE INDEX index_queue_on_workspace_id ON queue (workspace_id); +CREATE INDEX index_queue_on_scheduled_for ON queue (scheduled_for); +CREATE INDEX index_queue_on_running ON queue (running); +CREATE INDEX index_queue_on_created ON queue (created_at); +CREATE INDEX index_queue_on_script_path ON queue (script_path); +CREATE INDEX index_queue_on_script_hash ON queue (script_hash); + +CREATE TABLE completed_job ( + id UUID PRIMARY KEY, + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + parent_job UUID, + created_by VARCHAR(50) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + duration INT NOT NULL, + success BOOLEAN NOT NULL, + script_hash BIGINT, + script_path VARCHAR(255), + args JSONB, + result JSONB, + logs TEXT, + deleted BOOLEAN NOT NULL DEFAULT false, + raw_code TEXT, + canceled boolean NOT NULL DEFAULT false, + canceled_by VARCHAR(50), + canceled_reason TEXT, + job_kind JOB_KIND NOT NULL DEFAULT 'script', + env_id INTEGER NOT NULL DEFAULT 0, + schedule_path varchar(255), + permissioned_as VARCHAR(55) NOT NULL DEFAULT 'g/all', + flow_status JSONB +); + +CREATE INDEX index_completed_on_workspace_id ON completed_job (workspace_id); +CREATE INDEX index_completed_on_created ON completed_job (created_at); +CREATE INDEX index_completed_on_script_path ON completed_job (script_path); +CREATE INDEX index_completed_on_script_hash ON completed_job (script_hash); + +CREATE TABLE usr ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + username VARCHAR(50) NOT NULL, + email VARCHAR(50) NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + operator BOOLEAN NOT NULL DEFAULT false, + disabled BOOLEAN NOT NULL DEFAULT false, + role VARCHAR(50), + PRIMARY KEY (workspace_id, username), + CONSTRAINT proper_username CHECK (username ~ '^[\w-]+$'), + CONSTRAINT proper_email CHECK (email ~ '^(?:[a-z0-9!#$%&''*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&''*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$') +); + +CREATE INDEX index_usr_email ON usr (email); + +CREATE TYPE LOGIN_TYPE AS ENUM ('password', 'github'); + +CREATE TABLE password ( + email VARCHAR(50) PRIMARY KEY, + password_hash VARCHAR(100), + login_type LOGIN_TYPE NOT NULL, + super_admin BOOLEAN NOT NULL DEFAULT FALSE, + verified BOOLEAN NOT NULL DEFAULT FALSE, + name VARCHAR(30), + company VARCHAR(30) +); + +-- CREATE TABLE invite_code ( +-- code VARCHAR(20) PRIMARY KEY, +-- seats_left INTEGER NOT NULL DEFAULT 0, +-- seats_given INTEGER NOT NULL DEFAULT 1 +-- ); + + +CREATE TABLE workspace_settings ( + workspace_id VARCHAR(50) PRIMARY KEY REFERENCES workspace(id), + slack_team_id VARCHAR(50) UNIQUE, + slack_name VARCHAR(50), + slack_command_script VARCHAR(255) +); + + +INSERT INTO workspace_settings (workspace_id) VALUES + ('starter'), + ('demo'); + + +CREATE TABLE workspace_invite ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + email VARCHAR(50), + is_admin bool NOT NULL DEFAULT false, + CONSTRAINT proper_email CHECK (email ~ '^(?:[a-z0-9!#$%&''*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&''*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$'), + PRIMARY KEY (workspace_id, email) +); + +CREATE TABLE magic_link ( + email VARCHAR(50) NOT NULL, + token VARCHAR(100) NOT NULL, + expiration TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (NOW() + interval '1 day'), + PRIMARY KEY (email, token) +); + +CREATE TABLE token ( + token VARCHAR(50) PRIMARY KEY, + label VARCHAR(50), + expiration TIMESTAMP WITH TIME ZONE, + workspace_id VARCHAR(50) REFERENCES workspace(id), + owner VARCHAR(55), + email VARCHAR(50), + super_admin BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + last_used_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); + +CREATE INDEX index_magic_link_exp ON magic_link (expiration); +CREATE INDEX index_token_exp ON token (expiration); + +INSERT INTO usr(workspace_id, email, username, is_admin, role) VALUES + ('starter', 'admin@windmill.dev', 'admin', true, 'Admin'), + ('demo', 'admin@windmill.dev', 'admin', true, 'Ruben'); + +INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company) VALUES + ('admin@windmill.dev', true, '$argon2id$v=19$m=4096,t=3,p=1$z0Kg3qyaS14e+YHeihkJLQ$N69flI6yQ/U98pjAHtbNxbdz2f4PrJEi9Tx1VoYk1as', 'password', true, 'Admin', 'Windmill'), + ('ruben@windmill.dev', true, '$argon2id$v=19$m=4096,t=3,p=1$z0Kg3qyaS14e+YHeihkJLQ$N69flI6yQ/U98pjAHtbNxbdz2f4PrJEi9Tx1VoYk1as', 'password', true, 'Ruben', 'Windmill'), + ('user@windmill.dev', true, '$argon2id$v=19$m=4096,t=3,p=1$z0Kg3qyaS14e+YHeihkJLQ$N69flI6yQ/U98pjAHtbNxbdz2f4PrJEi9Tx1VoYk1as', 'password', false, 'User', 'Windmill'); + +INSERT INTO workspace_invite(workspace_id, email, is_admin) VALUES + ('demo', 'ruben@windmill.dev', true); + +CREATE TABLE variable ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + path VARCHAR(255), + value VARCHAR(4012) NOT NULL, + is_secret BOOLEAN NOT NULL DEFAULT FALSE, + description VARCHAR(255) NOT NULL DEFAULT '', + PRIMARY KEY (workspace_id, path), + CONSTRAINT proper_id CHECK (path ~ '^[ug](\/[\w-]+){2,}$') +); + +-- CREATE TABLE oauth( +-- id VARCHAR(150) NOT NULL PRIMARY KEY, +-- owner VARCHAR(50), +-- workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), +-- type VARCHAR(50) NOT NULL, +-- refresh_token VARCHAR(255), +-- access_token VARCHAR(255) NOT NULL +-- ); + +-- CREATE INDEX index_oauth ON oauth (workspace_id, type, owner); + +CREATE TYPE ACTION_KIND AS ENUM ('create', 'update', 'delete', 'execute'); + +CREATE TABLE audit ( + workspace_id VARCHAR(50) NOT NULL, + id SERIAL, + timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + username VARCHAR(50) NOT NULL, + operation VARCHAR(50) NOT NULL, + action_kind ACTION_KIND NOT NULL, + resource VARCHAR(255), + parameters JSONB, + PRIMARY KEY (workspace_id, id) +); + +CREATE TABLE resource_type ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + name VARCHAR(50), + schema JSONB, + description TEXT, + PRIMARY KEY (workspace_id, name), + CONSTRAINT proper_name CHECK (name ~ '^[\w-]+$') +); + +CREATE TABLE resource ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + path VARCHAR(255), + value JSONB, + description TEXT, + resource_type VARCHAR(50) NOT NULL, + PRIMARY KEY (workspace_id, path), + CONSTRAINT proper_id CHECK (path ~ '^[ug](\/[\w-]+){2,}$') +); + +INSERT INTO resource_type(workspace_id, name, schema, description) VALUES + ('starter', 'postgres', '{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "dbname": { + "description": "The database name", + "type": "string" + }, + "user": { + "description": "The postgres username", + "type": "string" + }, + "password": { + "description": "The postgres users password", + "type": "string" + }, + "sslmode": { + "description": "The sslmode", + "type": "string", + "enum": ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"] + }, + "host": { + "description": "The instance host", + "type": "string" + }, + "port": { + "description": "The instance port", + "type": "integer" + } + }, + "required": ["dbname", "user", "password"] +}', 'A postgres database connection resource') + ; + +INSERT INTO resource(workspace_id, path, value, description, resource_type) VALUES + ('starter', 'g/all/demodb', '{"host": "demodb.service.consul", "dbname": "demodb", + "user": "postgres", "password": "demodb", "sslmode": "disable", "port":"6543"}', 'demodb', 'postgres') +; + + +CREATE TABLE pipenv ( + id SERIAL PRIMARY KEY, + timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + created_by VARCHAR(50) NOT NULL, + python_version VARCHAR(20), + dependencies VARCHAR(255)[] NOT NULL DEFAULT array[]::varchar[], + pipfile_lock TEXT, + job_id UUID +); + +CREATE TABLE schedule( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + path varchar(255), + edited_by varchar(255) NOT NULL, + edited_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + schedule VARCHAR(255) NOT NULL, + offset_ INTEGER NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT true, + script_path varchar(255), + script_hash BIGINT, + args JSONB, + PRIMARY KEY (workspace_id, path), + CONSTRAINT proper_id CHECK (path ~ '^[ug](\/[\w-]+){2,}$') +); + +CREATE TABLE group_ ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + name VARCHAR(50), + summary TEXT, + PRIMARY KEY (workspace_id, name), + CONSTRAINT proper_name CHECK (name ~ '^[\w-]+$') +); + +CREATE TABLE usr_to_group( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + group_ VARCHAR(50) NOT NULL, + usr VARCHAR(50) NOT NULL DEFAULT 'ruben', + CONSTRAINT fk_group FOREIGN KEY(workspace_id, group_) REFERENCES group_(workspace_id, name), + PRIMARY KEY (workspace_id, usr, group_) +); + +INSERT INTO group_ SELECT id, 'all', 'The group that always contains all users of this workspace' FROM workspace; + +CREATE TABLE worker_ping( + worker VARCHAR(50) PRIMARY KEY, + worker_instance VARCHAR(50) NOT NULL, + ping_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + env_id INTEGER NOT NULL DEFAULT -1, + ip VARCHAR(50) NOT NULL DEFAULT 'NO IP', + jobs_executed INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX worker_ping_on_ping_at ON worker_ping (ping_at); + +ALTER TABLE audit ENABLE ROW LEVEL SECURITY; +CREATE POLICY audit_log_see_own ON audit FOR SELECT +USING(audit.username = current_setting('session.user') or current_setting('session.is_admin')::boolean); +-- USING(current_setting('session.is_admin')::boolean); + + +DO +$do$ +BEGIN + IF NOT EXISTS ( + SELECT FROM pg_catalog.pg_roles + WHERE rolname = 'app') THEN + + CREATE ROLE app LOGIN PASSWORD 'changeme'; + END IF; +END +$do$; + +GRANT SELECT ON audit TO app; + +REVOKE ALL +ON ALL TABLES IN SCHEMA public +FROM PUBLIC; + +GRANT ALL +ON ALL TABLES IN SCHEMA public +TO admin; + +ALTER DEFAULT PRIVILEGES + FOR ROLE admin + IN SCHEMA public + GRANT ALL ON TABLES TO admin; + + +INSERT INTO usr_to_group +SELECT workspace_id, 'all', username FROM (SELECT workspace_id, username from usr) as usernames +; + +DROP POLICY audit_log_see_own on audit; +GRANT ALL ON audit TO app; +CREATE POLICY see_own ON audit FOR ALL +USING (audit.username = current_setting('session.user')); + + +GRANT ALL ON queue TO app; +ALTER TABLE queue ENABLE ROW LEVEL SECURITY; + +CREATE POLICY see_own ON queue FOR ALL +USING (SPLIT_PART(queue.permissioned_as, '/', 1) = 'u' AND SPLIT_PART(queue.permissioned_as, '/', 2) = current_setting('session.user')); + +CREATE POLICY see_member ON queue FOR ALL +USING (SPLIT_PART(queue.permissioned_as, '/', 1) = 'g' AND SPLIT_PART(queue.permissioned_as, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[])); + +GRANT ALL ON completed_job TO app; +ALTER TABLE completed_job ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY see_starter ON completed_job FOR SELECT +USING (completed_job.workspace_id = 'starter'); + +CREATE POLICY see_own ON completed_job FOR ALL +USING (SPLIT_PART(completed_job.permissioned_as, '/', 1) = 'u' AND SPLIT_PART(completed_job.permissioned_as, '/', 2) = current_setting('session.user')); + +CREATE POLICY see_member ON completed_job FOR ALL +USING (SPLIT_PART(completed_job.permissioned_as, '/', 1) = 'g' AND SPLIT_PART(completed_job.permissioned_as, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[])); + +GRANT SELECT ON pipenv to app; +GRANT SELECT (email, username, is_admin, workspace_id) ON usr to app; + +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public to app; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public to admin; +GRANT SELECT, INSERT ON resource_type to app; + +GRANT SELECT ON worker_ping to app; +GRANT SELECT ON worker_ping to admin; + +CREATE POLICY schedule ON audit FOR INSERT +WITH CHECK (audit.username LIKE 'schedule-%'); + + +DO +$do$ + DECLARE + i text; + arr text[] := array['resource', 'script', 'variable', 'schedule', 'flow']; + BEGIN + FOREACH i IN ARRAY arr + LOOP + EXECUTE FORMAT( + $$ + + GRANT ALL ON %1$I TO app; + ALTER TABLE %1$I ENABLE ROW LEVEL SECURITY; + + CREATE POLICY see_starter ON %1$I FOR SELECT + USING (%1$I.workspace_id = 'starter'); + + CREATE POLICY see_own ON %1$I FOR ALL + USING (SPLIT_PART(%1$I.path, '/', 1) = 'u' AND SPLIT_PART(%1$I.path, '/', 2) = current_setting('session.user')); + + CREATE POLICY see_member ON %1$I FOR ALL + USING (SPLIT_PART(%1$I.path, '/', 1) = 'g' AND SPLIT_PART(%1$I.path, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[])); + + ALTER TABLE %1$I + ADD COLUMN extra_perms JSONB NOT NULL DEFAULT '{}'; + + CREATE INDEX %1$I_extra_perms ON %1$I USING GIN (extra_perms); + + CREATE POLICY see_extra_perms_user ON %1$I FOR ALL + USING (extra_perms ? CONCAT('u/', current_setting('session.user'))) + WITH CHECK ((extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean); + + CREATE POLICY see_extra_perms_groups ON %1$I FOR ALL + USING (extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]) + WITH CHECK (exists( + SELECT key, value FROM jsonb_each_text(extra_perms) + WHERE SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]) + AND value::boolean)); + $$, + i + ); + END LOOP; + END +$do$; + +GRANT ALL ON group_ TO app; +ALTER TABLE group_ +ADD COLUMN extra_perms JSONB NOT NULL DEFAULT '{}'; + +CREATE INDEX group_extra_perms ON group_ USING GIN (extra_perms); + +GRANT ALL ON usr_to_group TO app; +ALTER TABLE usr_to_group ENABLE ROW LEVEL SECURITY; + +CREATE POLICY see_extra_perms_user ON usr_to_group FOR ALL +USING (true) +WITH CHECK (EXISTS(SELECT 1 FROM group_ WHERE usr_to_group.group_ = group_.name AND usr_to_group.workspace_id = group_.workspace_id AND (group_.extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean)); + + +CREATE POLICY see_extra_perms_groups ON usr_to_group FOR ALL +USING (true) +WITH CHECK (exists( + SELECT f.* FROM group_ g, jsonb_each_text(g.extra_perms) f + WHERE usr_to_group.group_ = g.name AND usr_to_group.workspace_id = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]) + AND value::boolean)); + +DO +$do$ +BEGIN + IF NOT EXISTS ( + SELECT FROM pg_catalog.pg_roles -- SELECT list can be empty for this + WHERE rolname = 'admin') THEN + CREATE ROLE admin WITH BYPASSRLS LOGIN PASSWORD 'changeme'; + END IF; +END +$do$; diff --git a/backend/migrations/20220316135622_raw_flow.down.sql b/backend/migrations/20220316135622_raw_flow.down.sql new file mode 100644 index 0000000000..8dd6f8ca5a --- /dev/null +++ b/backend/migrations/20220316135622_raw_flow.down.sql @@ -0,0 +1,6 @@ +-- Add down migration script here +ALTER TABLE queue DROP COLUMN raw_flow; +ALTER TABLE completed_job DROP COLUMN raw_flow; + +ALTER TABLE queue DROP COLUMN is_flow_step NOT NULL DEFAULT false; +ALTER TABLE completed_job DROP COLUMN is_flow_step NOT NULL DEFAULT false; diff --git a/backend/migrations/20220316135622_raw_flow.up.sql b/backend/migrations/20220316135622_raw_flow.up.sql new file mode 100644 index 0000000000..a17ea81668 --- /dev/null +++ b/backend/migrations/20220316135622_raw_flow.up.sql @@ -0,0 +1,6 @@ +-- Add up migration script here +ALTER TABLE queue ADD COLUMN raw_flow JSONB; +ALTER TABLE completed_job ADD COLUMN raw_flow JSONB; + +ALTER TABLE queue ADD COLUMN is_flow_step boolean DEFAULT false; +ALTER TABLE completed_job ADD COLUMN is_flow_step boolean DEFAULT false; diff --git a/backend/migrations/20220320122733_schedule_flow.down.sql b/backend/migrations/20220320122733_schedule_flow.down.sql new file mode 100644 index 0000000000..0c36f6ab14 --- /dev/null +++ b/backend/migrations/20220320122733_schedule_flow.down.sql @@ -0,0 +1,4 @@ +-- Add down migration script here +ALTER TABLE schedule ADD COLUMN script_hash BIGINT; +ALTER TABLE schedule DROP COLUMN is_flow; + diff --git a/backend/migrations/20220320122733_schedule_flow.up.sql b/backend/migrations/20220320122733_schedule_flow.up.sql new file mode 100644 index 0000000000..1178d6834b --- /dev/null +++ b/backend/migrations/20220320122733_schedule_flow.up.sql @@ -0,0 +1,4 @@ +-- Add up migration script here +ALTER TABLE schedule DROP COLUMN script_hash; +ALTER TABLE schedule ADD COLUMN is_flow boolean NOT NULL DEFAULT false; +ALTER TABLE schedule ALTER COLUMN script_path SET NOT NULL;; diff --git a/backend/migrations/20220321004844_flow_preview.down.sql b/backend/migrations/20220321004844_flow_preview.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20220321004844_flow_preview.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20220321004844_flow_preview.up.sql b/backend/migrations/20220321004844_flow_preview.up.sql new file mode 100644 index 0000000000..6066aca51b --- /dev/null +++ b/backend/migrations/20220321004844_flow_preview.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TYPE JOB_KIND ADD VALUE 'flowpreview'; diff --git a/backend/migrations/20220406141754_custom_env.down.sql b/backend/migrations/20220406141754_custom_env.down.sql new file mode 100644 index 0000000000..0feebde915 --- /dev/null +++ b/backend/migrations/20220406141754_custom_env.down.sql @@ -0,0 +1,20 @@ +-- Add down migration script here +CREATE TABLE pipenv ( + id SERIAL PRIMARY KEY, + timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + created_by VARCHAR(50) NOT NULL, + python_version VARCHAR(20), + dependencies VARCHAR(255)[] NOT NULL DEFAULT array[]::varchar[], + pipfile_lock TEXT, + job_id UUID +); + +ALTER TABLE script +DROP COLUMN lock; + +ALTER TABLE script +DROP COLUMN lock_error_logs; + +ALTER TABLE worker_ping +ADD COLUMN env_id INTEGER NOT NULL DEFAULT -1; + diff --git a/backend/migrations/20220406141754_custom_env.up.sql b/backend/migrations/20220406141754_custom_env.up.sql new file mode 100644 index 0000000000..31970c24f5 --- /dev/null +++ b/backend/migrations/20220406141754_custom_env.up.sql @@ -0,0 +1,11 @@ +-- Add up migration script here +DROP TABLE pipenv; + +ALTER TABLE script +ADD COLUMN lock TEXT; + +ALTER TABLE script +ADD COLUMN lock_error_logs TEXT; + +ALTER TABLE worker_ping +DROP COLUMN env_id; diff --git a/backend/migrations/20220421061414_jsonb.down.sql b/backend/migrations/20220421061414_jsonb.down.sql new file mode 100644 index 0000000000..3b12d1236d --- /dev/null +++ b/backend/migrations/20220421061414_jsonb.down.sql @@ -0,0 +1,6 @@ +-- Add down migration script here +ALTER TABLE script +ALTER COLUMN schema TYPE jsonb; + +ALTER TABLE flow +ALTER COLUMN schema TYPE jsonb; diff --git a/backend/migrations/20220421061414_jsonb.up.sql b/backend/migrations/20220421061414_jsonb.up.sql new file mode 100644 index 0000000000..5cd40d8587 --- /dev/null +++ b/backend/migrations/20220421061414_jsonb.up.sql @@ -0,0 +1,6 @@ +-- Add up migration script here +ALTER TABLE script +ALTER COLUMN schema TYPE json; + +ALTER TABLE flow +ALTER COLUMN schema TYPE json; diff --git a/backend/migrations/20220428085013_private_key.down.sql b/backend/migrations/20220428085013_private_key.down.sql new file mode 100644 index 0000000000..e1dc71d137 --- /dev/null +++ b/backend/migrations/20220428085013_private_key.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +DROP TABLE workspace_key; +DROP TYPE WORKSPACE_KEY_KIND; diff --git a/backend/migrations/20220428085013_private_key.up.sql b/backend/migrations/20220428085013_private_key.up.sql new file mode 100644 index 0000000000..dd6dcdee70 --- /dev/null +++ b/backend/migrations/20220428085013_private_key.up.sql @@ -0,0 +1,16 @@ +-- Add up migration script here + +CREATE TYPE WORKSPACE_KEY_KIND AS ENUM ('cloud'); + +CREATE TABLE workspace_key ( + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + kind WORKSPACE_KEY_KIND NOT NULL, + key VARCHAR(255) NOT NULL DEFAULT 'changeme', + PRIMARY KEY (workspace_id, kind) +); + +GRANT SELECT ON workspace_key TO app; +GRANT SELECT ON workspace_key TO admin; + +INSERT INTO workspace_key SELECT id as workspace_id, 'cloud' as kind, 'changeme' as key FROM workspace; + diff --git a/backend/migrations/20220503085923_slack_resource.down.sql b/backend/migrations/20220503085923_slack_resource.down.sql new file mode 100644 index 0000000000..b2b4e4b233 --- /dev/null +++ b/backend/migrations/20220503085923_slack_resource.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DELETE FROM resource_type WHERE name = 'slack' AND workspace_id = 'starter'; diff --git a/backend/migrations/20220503085923_slack_resource.up.sql b/backend/migrations/20220503085923_slack_resource.up.sql new file mode 100644 index 0000000000..12110e7fff --- /dev/null +++ b/backend/migrations/20220503085923_slack_resource.up.sql @@ -0,0 +1,14 @@ +-- Add up migration script here +INSERT INTO resource_type(workspace_id, name, schema, description) VALUES + ('starter', 'slack', '{ + "type": "object", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "required": [], + "properties": { + "token": { + "type": "string", + "description": "The slack token" + } + } +}', 'A slack token to interact with a specific workspace. Can be obtained from the OAuth integration in the workspace settings.') + ; diff --git a/backend/openapi.yaml b/backend/openapi.yaml new file mode 100644 index 0000000000..74b7b5d513 --- /dev/null +++ b/backend/openapi.yaml @@ -0,0 +1,3406 @@ +openapi: "3.0.3" + +info: + version: 1.5.0 + title: Windmill server API + contact: + name: Windmill contact + email: contact@windmill.dev + url: https://windmill.dev + + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + +externalDocs: + description: documentation portal + url: https://docs.windmill.dev + +servers: + - url: /api + +security: + - bearerAuth: [] + - cookieAuth: [] + +paths: + /version: + get: + summary: get backend version + operationId: backendVersion + tags: + - settings + responses: + "200": + description: git version of backend + content: + text/plain: + schema: + type: string + + /openapi.yaml: + get: + summary: get openapi yaml spec + operationId: getOpenApiYaml + tags: + - settings + responses: + "200": + description: openapi yaml file content + content: + text/plain: + schema: + type: string + + /w/{workspace}/audit/get/{id}: + get: + summary: get audit log (requires admin privilege) + operationId: getAuditLog + tags: + - audit + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/PathId" + responses: + "200": + description: an audit log + content: + application/json: + schema: + $ref: "#/components/schemas/AuditLog" + + /w/{workspace}/audit/list: + get: + summary: list audit logs (requires admin privilege) + operationId: listAuditLogs + tags: + - audit + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + - $ref: "#/components/parameters/Before" + - $ref: "#/components/parameters/After" + - $ref: "#/components/parameters/Username" + - $ref: "#/components/parameters/Operation" + - $ref: "#/components/parameters/Resource" + - $ref: "#/components/parameters/ActionKind" + + responses: + "200": + description: a list of audit logs + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/AuditLog" + + /auth/login: + post: + security: [] + summary: login with password + operationId: login + tags: + - user + requestBody: + description: Partially filled script + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Login" + + responses: + "200": + description: > + Successfully authenticated. + The session ID is returned in a cookie named `token` and as plaintext response. + Preferred method of authorization is through the bearer token. The cookie is only for browser convenience. + + headers: + Set-Cookie: + schema: + type: string + example: token=abcde12345; Path=/; HttpOnly + content: + text/plain: + schema: + type: string + + /w/{workspace}/users/add: + post: + summary: create user (require admin privilege) + operationId: createUser + tags: + - user + - admin + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new user + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewUser" + responses: + "201": + description: user created + content: + text/plain: + schema: + type: string + + /w/{workspace}/users/update/{username}: + post: + summary: update user (require admin privilege) + operationId: updateUser + tags: + - user + - admin + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: username + in: path + required: true + schema: + type: string + requestBody: + description: new user + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EditWorkspaceUser" + responses: + "200": + description: edited user + content: + text/plain: + schema: + type: string + + /users/setpassword: + post: + summary: set password + operationId: setPassword + tags: + - user + requestBody: + description: set password + required: true + content: + application/json: + schema: + type: object + properties: + password: + type: string + required: + - password + responses: + "200": + description: password set + content: + text/plain: + schema: + type: string + + /w/{workspace}/users/delete/{username}: + delete: + summary: delete user (require admin privilege) + operationId: deleteUser + tags: + - user + - admin + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: username + in: path + required: true + schema: + type: string + responses: + "200": + description: delete user + content: + text/plain: + schema: + type: string + + /users/logout: + post: + summary: logout + operationId: logout + tags: + - user + responses: + "200": + description: logout + content: + text/plain: + schema: + type: string + + /workspaces/list: + get: + summary: list all workspaces visible to me + operationId: listWorkspaces + tags: + - workspace + responses: + "200": + description: all workspaces + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Workspace" + + /workspaces/users: + get: + summary: list all workspaces visible to me with user info + operationId: listUserWorkspaces + tags: + - workspace + responses: + "200": + description: workspace with associated username + content: + application/json: + schema: + $ref: "#/components/schemas/UserWorkspaceList" + + /workspaces/list_as_superadmin: + get: + summary: list all workspaces as super admin (require to be super amdin) + operationId: listWorkspacesAsSuperAdmin + tags: + - workspace + parameters: + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: workspaces + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Workspace" + + /workspaces/create: + post: + summary: create workspace + operationId: createWorkspace + tags: + - workspace + requestBody: + description: new token + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateWorkspace" + responses: + "201": + description: token created + content: + text/plain: + schema: + type: string + + /workspaces/validate_id: + post: + summary: validate id + operationId: validateId + tags: + - workspace + requestBody: + description: id of workspace + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: string + required: + - id + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /workspaces/validate_username: + post: + summary: validate username + operationId: validateUsername + tags: + - workspace + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + id: + type: string + username: + type: string + required: + - id + - username + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /users/email: + get: + summary: get current user email (if logged in) + operationId: getCurrentEmail + tags: + - user + responses: + "200": + description: user email + content: + text/plain: + schema: + type: string + + /users/whoami: + get: + summary: get current global whoami (if logged in) + operationId: globalWhoami + tags: + - user + responses: + "200": + description: user email + content: + application/json: + schema: + $ref: "#/components/schemas/GlobalWhoami" + + /users/list_invites: + get: + summary: list all workspace invites + operationId: listWorkspaceInvites + tags: + - user + responses: + "200": + description: list all workspace invites + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkspaceInvite" + + /w/{workspace}/users/whoami: + get: + summary: whoami + operationId: whoami + tags: + - user + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: user + content: + application/json: + schema: + $ref: "#/components/schemas/User" + + /w/{workspace}/users/leave_workspace: + post: + summary: leave workspace + operationId: leaveWorkspace + tags: + - user + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /users/accept_invite: + post: + summary: accept invite to workspace + operationId: acceptInvite + tags: + - user + requestBody: + description: accept invite + required: true + content: + application/json: + schema: + type: object + properties: + workspace_id: + type: string + username: + type: string + required: + - workspace_id + - username + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /users/decline_invite: + post: + summary: decline invite to workspace + operationId: declineInvite + tags: + - user + requestBody: + description: decline invite + required: true + content: + application/json: + schema: + type: object + properties: + workspace_id: + type: string + required: + - workspace_id + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /w/{workspace}/workspaces/invite_user: + post: + summary: invite user to workspace + operationId: inviteUser + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: WorkspaceInvite + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + is_admin: + type: boolean + required: + - email + - is_admin + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /w/{workspace}/workspaces/delete_invite: + post: + summary: delete user invite + operationId: delete invite + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: WorkspaceInvite + required: true + content: + application/json: + schema: + type: object + properties: + email: + type: string + is_admin: + type: boolean + required: + - email + - is_admin + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /w/{workspace}/workspaces/delete: + delete: + summary: delete workspace + operationId: deleteWorkspace + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /w/{workspace}/users/whois/{username}: + get: + summary: whois + operationId: whois + tags: + - user + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: username + in: path + required: true + schema: + type: string + responses: + "200": + description: user + content: + application/json: + schema: + $ref: "#/components/schemas/User" + + /users/list_as_super_admin: + get: + summary: list all users as super admin (require to be super amdin) + operationId: listUsersAsSuperAdmin + tags: + - user + parameters: + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: user + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + + /w/{workspace}/workspaces/list_pending_invites: + get: + summary: list pending invites for a workspace + operationId: listPendingInvites + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: user + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkspaceInvite" + + /w/{workspace}/workspaces/get_settings: + get: + summary: get settings + operationId: getSettings + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + + responses: + "200": + description: status + content: + application/json: + schema: + type: object + properties: + workspace_id: + type: string + slack_name: + type: string + slack_team_id: + type: string + slack_command_script: + type: string + + /w/{workspace}/workspaces/edit_slack_command: + post: + summary: edit slack command + operationId: editSlackCommand + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: WorkspaceInvite + required: true + content: + application/json: + schema: + type: object + properties: + slack_command_script: + type: string + + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + + /w/{workspace}/users/list: + get: + summary: list users + operationId: listUsers + tags: + - user + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: user + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + + /w/{workspace}/users/list_usernames: + get: + summary: list usernames + operationId: listUsernames + tags: + - user + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: user + content: + application/json: + schema: + type: array + items: + type: string + + /users/tokens/create: + post: + summary: create token + operationId: createToken + tags: + - user + requestBody: + description: new token + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewToken" + responses: + "201": + description: token created + content: + text/plain: + schema: + type: string + + /users/tokens/delete/{token_prefix}: + delete: + summary: delete token + operationId: deleteToken + tags: + - user + parameters: + - name: token_prefix + in: path + required: true + schema: + type: string + responses: + "200": + description: delete token + content: + text/plain: + schema: + type: string + + /users/tokens/list: + get: + summary: list token + operationId: listTokens + tags: + - user + responses: + "200": + description: truncated token + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TruncatedToken" + + /w/{workspace}/variables/create: + post: + summary: create variable + operationId: createVariable + tags: + - variable + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new variable + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateVariable" + responses: + "201": + description: variable created + content: + text/plain: + schema: + type: string + + /w/{workspace}/variables/delete/{path}: + delete: + summary: delete variable + operationId: deleteVariable + tags: + - variable + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: variable deleted + content: + text/plain: + schema: + type: string + + /w/{workspace}/variables/update/{path}: + post: + summary: update variable + operationId: updateVariable + tags: + - variable + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: updated variable + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EditVariable" + responses: + "200": + description: variable updated + content: + text/plain: + schema: + type: string + + /w/{workspace}/variables/get/{path}: + get: + summary: get variable + operationId: getVariable + tags: + - variable + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + - name: decrypt_secret + description: | + ask to decrypt secret if this variable is secret + (if not secret no effect, default: true) + in: query + schema: + type: boolean + responses: + "200": + description: variable + content: + application/json: + schema: + $ref: "#/components/schemas/ListableVariable" + + /w/{workspace}/variables/list: + get: + summary: list variables + operationId: listVariable + tags: + - variable + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: variable list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ListableVariable" + + /w/{workspace}/variables/list_contextual: + get: + summary: list contextual variables + operationId: listContextualVariables + tags: + - variable + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: contextual variable list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ContextualVariable" + + /w/{workspace}/oauth/disconnect/{client_name}: + post: + summary: disconnect client + operationId: disconnectClient + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ClientName" + responses: + "200": + description: disconnected client + content: + text/plain: + schema: + type: string + + /w/{workspace}/resources/create: + post: + summary: create resource + operationId: createResource + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new resource + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateResource" + responses: + "201": + description: resource created + content: + text/plain: + schema: + type: string + + /w/{workspace}/resources/delete/{path}: + delete: + summary: delete resource + operationId: deleteResource + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: resource deleted + content: + text/plain: + schema: + type: string + + /w/{workspace}/resources/update/{path}: + post: + summary: update resource + operationId: updateResource + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: updated resource + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EditResource" + responses: + "200": + description: resource updated + content: + text/plain: + schema: + type: string + + /w/{workspace}/resources/get/{path}: + get: + summary: get resource + operationId: getResource + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: resource deleted + content: + application/json: + schema: + $ref: "#/components/schemas/Resource" + + /w/{workspace}/resources/list: + get: + summary: list resources + operationId: listResource + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + - name: resource_type + description: resource_type to list from + in: query + schema: + type: string + responses: + "200": + description: resource list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Resource" + + /w/{workspace}/resources/type/create: + post: + summary: create resource_type + operationId: createResourceType + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new resource_type + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ResourceType" + responses: + "201": + description: resource_type created + content: + text/plain: + schema: + type: string + + /w/{workspace}/resources/type/delete/{path}: + delete: + summary: delete resource_type + operationId: deleteResourceType + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: resource_type deleted + content: + text/plain: + schema: + type: string + + /w/{workspace}/resources/type/update/{path}: + post: + summary: update resource_type + operationId: updateResourceType + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: updated resource_type + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EditResourceType" + responses: + "200": + description: resource_type updated + content: + text/plain: + schema: + type: string + + /w/{workspace}/resources/type/get/{path}: + get: + summary: get resource_type + operationId: getResourceType + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: resource_type deleted + content: + application/json: + schema: + $ref: "#/components/schemas/ResourceType" + + /w/{workspace}/resources/type/list: + get: + summary: list resource_types + operationId: listResourceType + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: resource_type list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ResourceType" + + /w/{workspace}/resources/type/listnames: + get: + summary: list resource_types names + operationId: listResourceTypeNames + tags: + - resource + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: resource_type list + content: + application/json: + schema: + type: array + items: + type: string + + /w/{workspace}/scripts/list: + get: + summary: list all available scripts + operationId: listScripts + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + - $ref: "#/components/parameters/OrderDesc" + - $ref: "#/components/parameters/CreatedBy" + - name: path_start + description: mask to filter matching starting parh + in: query + schema: + type: string + - name: path_exact + description: mask to filter exact matching path + in: query + schema: + type: string + - name: first_parent_hash + description: mask to filter scripts whom first direct parent has exact hash + in: query + schema: + type: string + - name: last_parent_hash + description: | + mask to filter scripts whom last parent in the chain has exact hash. + Beware that each script stores only a limited number of parents. Hence + the last parent hash for a script is not necessarily its top-most parent. + To find the top-most parent you will have to jump from last to last hash + until finding the parent + in: query + schema: + type: string + - name: parent_hash + description: | + is the hash present in the array of stored parent hashes for this script. + The same warning applies than for last_parent_hash. A script only store a + limited number of direct parent + in: query + schema: + type: string + - name: show_archived + description: | + (default false) + show also the archived files. + when multiple archived hash share the same path, only the ones with the latest create_at + are displayed. + in: query + schema: + type: boolean + - name: is_template + description: | + (default regardless) + if true show only the templates + if false show only the non templates + if not defined, show all regardless of if the script is a template + in: query + schema: + type: boolean + responses: + "200": + description: All available scripts + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Script" + + /w/{workspace}/scripts/create: + post: + summary: create script + operationId: createScript + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: Partially filled script + required: true + content: + application/json: + schema: + type: object + properties: + path: + type: string + parent_hash: + type: string + summary: + type: string + description: + type: string + content: + type: string + schema: + type: object + is_template: + type: boolean + lock: + type: array + items: + type: string + required: + - path + - summary + - description + - content + responses: + "201": + description: script created + content: + text/plain: + schema: + type: string + + /scripts/tojsonschema: + post: + summary: inspect code to infer jsonschema of arguments + operationId: toJsonschema + tags: + - script + requestBody: + description: code with the main function + required: true + content: + application/json: + schema: + type: string + responses: + "200": + description: parsed args + content: + application/json: + schema: + $ref: "#/components/schemas/MainArgSignature" + + /w/{workspace}/scripts/archive/p/{path}: + post: + summary: archive script by path + operationId: archiveScriptByPath + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: script archived + content: + text/plain: + schema: + type: string + + /w/{workspace}/scripts/archive/h/{hash}: + post: + summary: archive script by hash + operationId: archiveScriptByHash + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptHash" + responses: + "200": + description: script details + content: + application/json: + schema: + $ref: "#/components/schemas/Script" + + /w/{workspace}/scripts/delete/h/{hash}: + post: + summary: delete script by hash (erase content but keep hash) + operationId: deleteScriptByHash + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptHash" + responses: + "200": + description: script details + content: + application/json: + schema: + $ref: "#/components/schemas/Script" + + /w/{workspace}/scripts/get/p/{path}: + get: + summary: get script by path + operationId: getScriptByPath + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: script details + content: + application/json: + schema: + $ref: "#/components/schemas/Script" + + /w/{workspace}/scripts/get/h/{hash}: + get: + summary: get script by hash + operationId: getScriptByHash + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptHash" + responses: + "200": + description: script details + content: + application/json: + schema: + $ref: "#/components/schemas/Script" + + /w/{workspace}/scripts/deployment_status/h/{hash}: + get: + summary: get script deployment status + operationId: getScriptDeploymentStatus + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptHash" + responses: + "200": + description: script details + content: + application/json: + schema: + type: object + properties: + lock: + type: string + lock_error_logs: + type: string + + /w/{workspace}/jobs/run/p/{path}: + post: + summary: run script by path + operationId: runScriptByPath + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + - name: scheduled_for + description: when to schedule this job (leave empty for immediate run) + in: query + schema: + type: string + format: date-time + - name: scheduled_in_secs + description: schedule the script to execute in the number of seconds starting now + in: query + schema: + type: number + format: int64 + - $ref: "#/components/parameters/ParentJob" + + requestBody: + description: script args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "201": + description: job created + content: + text/plain: + schema: + type: string + format: uuid + + /w/{workspace}/flows/list: + get: + summary: list all available flows + operationId: listFlows + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + - $ref: "#/components/parameters/OrderDesc" + - $ref: "#/components/parameters/CreatedBy" + - name: path_start + description: mask to filter matching starting parh + in: query + schema: + type: string + - name: path_exact + description: mask to filter exact matching path + in: query + schema: + type: string + - name: show_archived + description: | + (default false) + show also the archived files. + when multiple archived hash share the same path, only the ones with the latest create_at + are displayed. + in: query + schema: + type: boolean + responses: + "200": + description: All available flow + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Flow" + + /w/{workspace}/flows/get/{path}: + get: + summary: get flow by path + operationId: getFlowByPath + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: flow details + content: + application/json: + schema: + $ref: "#/components/schemas/Flow" + + /w/{workspace}/flows/create: + post: + summary: create flow + operationId: createFlow + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: Partially filled flow + required: true + content: + application/json: + schema: + type: object + properties: + path: + type: string + summary: + type: string + description: + type: string + value: + $ref: "#/components/schemas/FlowValue" + schema: + type: object + required: + - path + - summary + - description + - content + - value + responses: + "201": + description: flow created + content: + text/plain: + schema: + type: string + + /w/{workspace}/flows/update/{path}: + post: + summary: update flow + operationId: updateFlow + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + requestBody: + description: Partially filled flow + required: true + content: + application/json: + schema: + type: object + properties: + path: + type: string + summary: + type: string + description: + type: string + value: + $ref: "#/components/schemas/FlowValue" + schema: + type: object + required: + - path + - summary + - description + - content + - value + responses: + "200": + description: flow updated + content: + text/plain: + schema: + type: string + + /w/{workspace}/flows/archive/{path}: + post: + summary: archive flow by path + operationId: archiveFlowByPath + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: flow archived + content: + text/plain: + schema: + type: string + + /w/{workspace}/jobs/run/f/{path}: + post: + summary: run flow by path + operationId: runFlowByPath + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + - name: scheduled_for + description: when to schedule this job (leave empty for immediate run) + in: query + schema: + type: string + format: date-time + - name: scheduled_in_secs + description: schedule the script to execute in the number of seconds starting now + in: query + schema: + type: number + format: int64 + - $ref: "#/components/parameters/ParentJob" + + requestBody: + description: flow args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "201": + description: job created + content: + text/plain: + schema: + type: string + format: uuid + + /w/{workspace}/jobs/run/h/{hash}: + post: + summary: run script by hash + operationId: runScriptByHash + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptHash" + - name: scheduled_for + description: when to schedule this job (leave empty for immediate run) + in: query + schema: + type: string + format: date-time + - name: scheduled_in_secs + description: schedule the script to execute in the number of seconds starting now + in: query + schema: + type: number + format: int64 + - $ref: "#/components/parameters/ParentJob" + + requestBody: + description: Partially filled args + required: true + content: + application/json: + schema: + type: object + + responses: + "201": + description: job created + content: + text/plain: + schema: + type: string + format: uuid + + /w/{workspace}/jobs/run/preview: + post: + summary: run script preview + operationId: runScriptPreview + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: previw + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Preview" + + responses: + "201": + description: job created + content: + text/plain: + schema: + type: string + format: uuid + + /w/{workspace}/jobs/run/preview_flow: + post: + summary: run flow preview + operationId: runFlowPreview + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: preview + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/FlowPreview" + + responses: + "201": + description: job created + content: + text/plain: + schema: + type: string + format: uuid + + /w/{workspace}/jobs/queue/list: + get: + summary: list all available queued jobs + operationId: listQueue + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/OrderDesc" + - $ref: "#/components/parameters/CreatedBy" + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/ScriptExactPath" + - $ref: "#/components/parameters/ScriptStartPath" + - $ref: "#/components/parameters/ScriptExactHash" + - $ref: "#/components/parameters/CreatedBefore" + - $ref: "#/components/parameters/CreatedAfter" + - $ref: "#/components/parameters/Success" + - $ref: "#/components/parameters/JobKinds" + + responses: + "200": + description: All available queued jobs + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/QueuedJob" + + /w/{workspace}/jobs/completed/list: + get: + summary: list all available completed jobs + operationId: listCompletedJobs + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/OrderDesc" + - $ref: "#/components/parameters/CreatedBy" + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/ScriptExactPath" + - $ref: "#/components/parameters/ScriptStartPath" + - $ref: "#/components/parameters/ScriptExactHash" + - $ref: "#/components/parameters/CreatedBefore" + - $ref: "#/components/parameters/CreatedAfter" + - $ref: "#/components/parameters/Success" + - $ref: "#/components/parameters/JobKinds" + + responses: + "200": + description: All available completed jobs + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CompletedJob" + + /w/{workspace}/jobs/list: + get: + summary: list all available jobs + operationId: listJobs + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/CreatedBy" + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/ScriptExactPath" + - $ref: "#/components/parameters/ScriptStartPath" + - $ref: "#/components/parameters/ScriptExactHash" + - $ref: "#/components/parameters/CreatedBefore" + - $ref: "#/components/parameters/CreatedAfter" + - $ref: "#/components/parameters/JobKinds" + + - name: success + description: filter on successful jobs + in: query + schema: + type: boolean + responses: + "200": + description: All jobs + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Job" + + /w/{workspace}/jobs/get/{id}: + get: + summary: get job + operationId: getJob + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + responses: + "200": + description: job details + content: + application/json: + schema: + $ref: "#/components/schemas/Job" + + /w/{workspace}/jobs/getupdate/{id}: + get: + summary: get job updates + operationId: getJobUpdates + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + - name: running + in: query + schema: + type: boolean + - name: log_offset + in: query + schema: + type: number + format: i32 + + responses: + "200": + description: job details + content: + application/json: + schema: + type: object + properties: + running: + type: boolean + completed: + type: boolean + new_logs: + type: string + + /w/{workspace}/jobs/completed/get/{id}: + get: + summary: get completed job + operationId: getCompletedJob + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + responses: + "200": + description: job details + content: + application/json: + schema: + $ref: "#/components/schemas/CompletedJob" + + /w/{workspace}/jobs/completed/delete/{id}: + post: + summary: delete completed job (erase content but keep run id) + operationId: deleteCompletedJob + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + responses: + "200": + description: job details + content: + application/json: + schema: + $ref: "#/components/schemas/CompletedJob" + + /w/{workspace}/jobs/queue/cancel/{id}: + post: + summary: cancel queued job + operationId: cancelQueuedJob + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + requestBody: + description: reason + required: true + content: + application/json: + schema: + type: object + properties: + reason: + type: string + + responses: + "200": + description: job canceled + content: + text/plain: + schema: + type: string + + /schedules/preview: + post: + summary: preview schedule + operationId: previewSchedule + tags: + - schedule + requestBody: + description: schedule + required: true + content: + application/json: + schema: + type: object + properties: + schedule: + type: string + offset: + type: integer + required: + - schedule + responses: + "200": + description: the preview of the next 10 time this schedule would apply to + content: + application/json: + schema: + type: array + items: + type: string + format: date-time + + /w/{workspace}/schedules/create: + post: + summary: create schedule + operationId: createSchedule + tags: + - schedule + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new schedule + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewSchedule" + responses: + "201": + description: schedule created + content: + text/plain: + schema: + type: string + + /w/{workspace}/schedules/update/{path}: + post: + summary: update schedule + operationId: updateSchedule + tags: + - schedule + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: updated schedule + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EditSchedule" + responses: + "200": + description: schedule updated + content: + text/plain: + schema: + type: string + + /w/{workspace}/schedules/setenabled/{path}: + post: + summary: set enabled schedule + operationId: setScheduleEnabled + tags: + - schedule + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + requestBody: + description: updated schedule enable + required: true + content: + application/json: + schema: + type: object + properties: + enabled: + type: boolean + required: + - enabled + + responses: + "200": + description: schedule enabled set + content: + text/plain: + schema: + type: string + + /w/{workspace}/schedules/get/{path}: + get: + summary: get schedule + operationId: getSchedule + tags: + - schedule + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: schedule deleted + content: + application/json: + schema: + $ref: "#/components/schemas/Schedule" + + /w/{workspace}/schedules/list: + get: + summary: list schedules + operationId: listSchedules + tags: + - schedule + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: schedule list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Schedule" + + /w/{workspace}/groups/list: + get: + summary: list groups + operationId: listGroups + tags: + - group + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: group list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Group" + + /w/{workspace}/groups/listnames: + get: + summary: list group names + operationId: listGroupNames + tags: + - group + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: group list + content: + application/json: + schema: + type: array + items: + type: string + + /w/{workspace}/groups/create: + post: + summary: create group + operationId: createGroup + tags: + - group + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: create group + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + summary: + type: string + required: + - name + responses: + "200": + description: group created + content: + text/plain: + schema: + type: string + + /w/{workspace}/groups/update/{name}: + post: + summary: update group + operationId: updateGroup + tags: + - group + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Name" + requestBody: + description: updated group + required: true + content: + application/json: + schema: + type: object + properties: + summary: + type: string + responses: + "200": + description: group updated + content: + text/plain: + schema: + type: string + + /w/{workspace}/groups/delete/{name}: + delete: + summary: delete group + operationId: deleteGroup + tags: + - group + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Name" + responses: + "200": + description: group deleted + content: + text/plain: + schema: + type: string + + /w/{workspace}/groups/get/{name}: + get: + summary: get group + operationId: getGroup + tags: + - group + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Name" + responses: + "200": + description: group + content: + application/json: + schema: + $ref: "#/components/schemas/Group" + + /w/{workspace}/groups/adduser/{name}: + post: + summary: add user to group + operationId: addUserToGroup + tags: + - group + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Name" + requestBody: + description: added user to group + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + responses: + "200": + description: user added to group + content: + text/plain: + schema: + type: string + + /w/{workspace}/groups/removeuser/{name}: + post: + summary: remove user to group + operationId: removeUserToGroup + tags: + - group + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Name" + requestBody: + description: added user to group + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + responses: + "200": + description: user removed from group + content: + text/plain: + schema: + type: string + + /workers/list: + get: + summary: list workers + operationId: listWorkers + tags: + - worker + parameters: + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: a list of workers + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WorkerPing" + + /w/{workspace}/acls/get/{kind}/{path}: + get: + summary: get granular acls + operationId: getGranularAcls + tags: + - granular_acl + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + - name: kind + in: path + required: true + schema: + type: string + enum: [script, group_, resource, schedule, variable, flow] + responses: + "200": + description: acls + content: + application/json: + schema: + type: object + additionalProperties: + type: boolean + + /w/{workspace}/acls/add/{kind}/{path}: + post: + summary: add granular acls + operationId: addGranularAcls + tags: + - granular_acl + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + - name: kind + in: path + required: true + schema: + type: string + enum: [script, group_, resource, schedule, variable, flow] + requestBody: + description: acl to add + required: true + content: + application/json: + schema: + type: object + properties: + owner: + type: string + write: + type: boolean + required: [owner] + responses: + "200": + description: granular acl added + content: + text/plain: + schema: + type: string + + /w/{workspace}/acls/remove/{kind}/{path}: + post: + summary: remove granular acls + operationId: removeGranularAcls + tags: + - granular_acl + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + - name: kind + in: path + required: true + schema: + type: string + enum: [script, group_, resource, schedule, variable, flow] + requestBody: + description: acl to add + required: true + content: + application/json: + schema: + type: object + properties: + owner: + type: string + required: [owner] + responses: + "200": + description: granular acl removed + content: + text/plain: + schema: + type: string +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + cookieAuth: + type: apiKey + in: cookie + name: token + parameters: + WorkspaceId: + name: workspace + in: path + required: true + schema: + type: string + ClientName: + name: client_name + in: path + required: true + schema: + type: string + ScriptPath: + name: path + in: path + required: true + schema: + type: string + ScriptHash: + name: hash + in: path + required: true + schema: + type: string + JobId: + name: id + in: path + required: true + schema: + type: string + format: uuid + Path: + name: path + in: path + required: true + schema: + type: string + PathId: + name: id + in: path + required: true + schema: + type: integer + format: int32 + Name: + name: name + in: path + required: true + schema: + type: string + Page: + name: page + description: which page to return (start at 1, default 1) + in: query + schema: + type: integer + format: int32 + PerPage: + name: per_page + description: number of items to return for a given page (default 30, max 100) + in: query + schema: + type: integer + format: int32 + OrderDesc: + name: order_desc + description: order by desc order (default true) + in: query + schema: + type: boolean + CreatedBy: + name: created_by + description: mask to filter exact matching user creator + in: query + schema: + type: string + ParentJob: + name: parent_job + description: The parent job that is at the origin and responsible for the execution of this script if any + in: query + schema: + type: string + format: uuid + ScriptStartPath: + name: script_path_start + description: mask to filter matching starting path + in: query + schema: + type: string + ScriptExactPath: + name: script_path_exact + description: mask to filter exact matching path + in: query + schema: + type: string + ScriptExactHash: + name: script_hash + description: mask to filter exact matching path + in: query + schema: + type: string + CreatedBefore: + name: created_before + description: filter on created before (inclusive) timestamp + in: query + schema: + type: string + format: date-time + CreatedAfter: + name: created_after + description: filter on created after (exclusive) timestamp + in: query + schema: + type: string + format: date-time + Success: + name: success + description: filter on successful jobs + in: query + schema: + type: boolean + After: + name: after + description: filter on created after (exclusive) timestamp + in: query + schema: + type: string + format: date-time + Before: + name: before + description: filter on created before (exclusive) timestamp + in: query + schema: + type: string + format: date-time + Username: + name: username + description: filter on exact username of user + in: query + schema: + type: string + Operation: + name: operation + description: filter on exact or prefix name of operation + in: query + schema: + type: string + Resource: + name: resource + description: filter on exact or prefix name of resource + in: query + schema: + type: string + ActionKind: + name: action_kind + description: filter on type of operation + in: query + schema: + type: string + enum: [Create, Update, Delete, Execute] + JobKinds: + name: job_kinds + description: filter on job kind (values 'preview', 'script', 'dependencies', 'flow') separated by, + in: query + schema: + type: string + # correct type is below but explode not supported by our codegen + # type: array + # items: + # type: string + # enum: ["preview", "script", "dependencies"] + # explode: false + + schemas: + Script: + type: object + properties: + workspace_id: + type: string + hash: + type: string + path: + type: string + parent_hashes: + type: array + description: | + The first element is the direct parent of the script, the second is the parent of the first, etc + items: + type: string + summary: + type: string + description: + type: string + content: + type: string + created_by: + type: string + created_at: + type: string + format: date-time + archived: + type: boolean + schema: + type: object + deleted: + type: boolean + is_template: + type: boolean + extra_perms: + type: object + additionalProperties: + type: boolean + lock: + type: string + lock_error_logs: + type: string + required: + - hash + - path + - summary + - content + - created_by + - created_at + - archived + - deleted + - is_template + - extra_perms + + ScriptArgs: + type: object + additionalProperties: true + + QueuedJob: + type: object + properties: + id: + type: string + format: uuid + parent_job: + type: string + format: uuid + created_by: + type: string + created_at: + type: string + format: date-time + started_at: + type: string + format: date-time + scheduled_for: + type: string + format: date-time + running: + type: boolean + script_path: + type: string + script_hash: + type: string + args: + $ref: "#/components/schemas/ScriptArgs" + logs: + type: string + raw_code: + type: string + canceled: + type: boolean + canceled_by: + type: string + canceled_reason: + type: string + last_ping: + type: string + format: date-time + job_kind: + type: string + enum: ["script", "preview", "dependencies", "flow", "flowpreview"] + schedule_path: + type: string + permissioned_as: + type: string + description: | + The user (u/userfoo) or group (g/groupfoo) whom + the execution of this script will be permissioned_as and by extension its DT_TOKEN. + flow_status: + $ref: "#/components/schemas/FlowStatus" + raw_flow: + $ref: "#/components/schemas/FlowValue" + is_flow_step: + type: boolean + required: + - id + - running + - canceled + - job_kind + - permissioned_as + - is_flow_step + + CompletedJob: + type: object + properties: + id: + type: string + format: uuid + parent_job: + type: string + format: uuid + created_by: + type: string + created_at: + type: string + format: date-time + duration: + type: integer + success: + type: boolean + script_path: + type: string + script_hash: + type: string + args: + $ref: "#/components/schemas/ScriptArgs" + result: + type: object + logs: + type: string + deleted: + type: boolean + raw_code: + type: string + canceled: + type: boolean + canceled_by: + type: string + canceled_reason: + type: string + job_kind: + type: string + enum: ["script", "preview", "dependencies", "flow", "flowpreview"] + schedule_path: + type: string + permissioned_as: + type: string + description: | + The user (u/userfoo) or group (g/groupfoo) whom + the execution of this script will be permissioned_as and by extension its DT_TOKEN. + flow_status: + $ref: "#/components/schemas/FlowStatus" + raw_flow: + $ref: "#/components/schemas/FlowValue" + is_flow_step: + type: boolean + required: + - id + - success + - canceled + - job_kind + - permissioned_as + - is_flow_step + + Job: + allOf: + - oneOf: + - $ref: "#/components/schemas/CompletedJob" + - $ref: "#/components/schemas/QueuedJob" + - type: object + properties: + type: + type: string + enum: [CompletedJob, QueuedJob] + discriminator: + propertyName: type + + User: + type: object + properties: + email: + type: string + username: + type: string + is_admin: + type: boolean + is_super_admin: + type: boolean + created_at: + type: string + format: date-time + operator: + type: boolean + disabled: + type: boolean + groups: + type: array + items: + type: string + required: + - email + - username + - is_admin + - is_super_admin + - created_at + - operator + - disabled + + Login: + type: object + properties: + email: + type: string + password: + type: string + required: + - email + - password + + NewUser: + type: object + properties: + email: + type: string + username: + type: string + is_admin: + type: boolean + required: + - email + - username + - is_admin + + EditWorkspaceUser: + type: object + properties: + is_admin: + type: boolean + + TruncatedToken: + type: object + properties: + label: + type: string + expiration: + type: string + format: date-time + token_prefix: + type: string + created_at: + type: string + format: date-time + last_used_at: + type: string + format: date-time + required: + - token_prefix + - created_at + - last_used_at + + NewToken: + type: object + properties: + label: + type: string + expiration: + type: string + format: date-time + + ListableVariable: + type: object + properties: + workspace_id: + type: string + path: + type: string + value: + type: string + is_secret: + type: boolean + description: + type: string + extra_perms: + type: object + additionalProperties: + type: boolean + required: + - workspace_id + - path + - is_secret + - extra_perms + + ContextualVariable: + type: object + properties: + name: + type: string + value: + type: string + description: + type: string + required: + - name + - value + - description + + CreateVariable: + type: object + properties: + path: + type: string + value: + type: string + is_secret: + type: boolean + description: + type: string + required: + - path + - value + - is_secret + - description + + EditVariable: + type: object + properties: + path: + type: string + value: + type: string + is_secret: + type: boolean + description: + type: string + + AuditLog: + type: object + properties: + id: + type: integer + format: int32 + timestamp: + type: string + format: date-time + username: + type: string + operation: + type: string + enum: + - "jobs.run" + - "scripts.create" + - "scripts.update" + - "users.create" + - "users.delete" + - "users.setpassword" + - "users.update" + - "users.login" + - "users.token.create" + - "users.token.delete" + - "variables.create" + - "variables.delete" + - "variables.update" + action_kind: + type: string + enum: ["Created", "Updated", "Delete", "Execute"] + resource: + type: string + parameters: + type: object + required: + - id + - timestamp + - username + - operation + - action_kind + + MainArgSignature: + type: object + properties: + star_args: + type: boolean + star_kwargs: + type: boolean + args: + type: array + items: + type: object + properties: + name: + type: string + typ: + type: string + enum: ["str", "float", "int", "bool", "unknown"] + has_default: + type: boolean + default: {} + required: + - name + - typ + required: + - star_args + - start_kwargs + - args + + Preview: + type: object + properties: + content: + type: string + path: + type: string + args: + $ref: "#/components/schemas/ScriptArgs" + + required: + - content + - args + + CreateResource: + type: object + properties: + path: + type: string + value: + type: object + description: + type: string + resource_type: + type: string + required: + - path + - value + - resource_type + + EditResource: + type: object + properties: + path: + type: string + description: + type: string + value: + type: object + + Resource: + type: object + properties: + workspace_id: + type: string + path: + type: string + description: + type: string + resource_type: + type: string + value: + type: object + extra_perms: + type: object + additionalProperties: + type: boolean + required: + - path + - resource_type + + ResourceType: + type: object + properties: + workspace_id: + type: string + name: + type: string + schema: {} + description: + type: string + required: + - name + + EditResourceType: + type: object + properties: + schema: + type: string + description: + type: string + + Schedule: + type: object + properties: + path: + type: string + edited_by: + type: string + edited_at: + type: string + format: date-time + schedule: + type: string + offset_: + type: integer + enabled: + type: boolean + script_path: + type: string + is_flow: + type: boolean + args: + $ref: "#/components/schemas/ScriptArgs" + extra_perms: + type: object + additionalProperties: + type: boolean + required: + - path + - edited_by + - edited_at + - schedule + - script_path + - offset_ + - extra_perms + - is_flow + + NewSchedule: + type: object + properties: + path: + type: string + schedule: + type: string + offset: + type: integer + script_path: + type: string + is_flow: + type: boolean + args: + $ref: "#/components/schemas/ScriptArgs" + required: + - path + - schedule + - script_path + - is_flow + - args + + EditSchedule: + type: object + properties: + schedule: + type: string + script_path: + type: string + is_flow: + type: boolean + args: + $ref: "#/components/schemas/ScriptArgs" + required: + - schedule + - script_path + - is_flow + - args + + Group: + type: object + properties: + name: + type: string + summary: + type: string + members: + type: array + items: + type: string + extra_perms: + type: object + additionalProperties: + type: boolean + required: + - name + + WorkerPing: + type: object + properties: + worker: + type: string + worker_instance: + type: string + ping_at: + type: string + format: date-time + started_at: + type: string + format: date-time + ip: + type: string + jobs_executed: + type: number + format: int32 + required: + - worker + - worker_instance + - ping_at + - started_at + - ip + - jobs_executed + + UserWorkspaceList: + type: object + properties: + email: + type: string + workspaces: + type: array + items: + type: object + properties: + id: + type: string + name: + type: string + username: + type: string + required: + - id + - name + - username + required: + - email + - workspaces + + CreateWorkspace: + type: object + properties: + id: + type: string + name: + type: string + username: + type: string + domain: + type: string + required: + - id + - name + - username + - domain + + Workspace: + type: object + properties: + id: + type: string + name: + type: string + owner: + type: string + domain: + type: string + required: + - id + - name + - owner + + WorkspaceInvite: + type: object + properties: + workspace_id: + type: string + email: + type: string + is_admin: + type: boolean + required: + - workspace_id + - email + - is_admin + + GlobalWhoami: + type: object + properties: + email: + type: string + login_type: + type: string + enum: ["password", "github"] + super_admin: + type: boolean + verified: + type: boolean + name: + type: string + company: + type: string + + required: + - email + - login_type + - super_admin + - verified + + Flow: + type: object + properties: + workspace_id: + type: string + path: + type: string + summary: + type: string + description: + type: string + value: + $ref: "#/components/schemas/FlowValue" + edited_by: + type: string + edited_at: + type: string + format: date-time + archived: + type: boolean + schema: + type: object + extra_perms: + type: object + additionalProperties: + type: boolean + required: + - path + - summary + - value + - edited_by + - edited_at + - archived + - extra_perms + + FlowValue: + type: object + properties: + modules: + type: array + items: + $ref: "#/components/schemas/FlowModule" + failure_module: + $ref: "#/components/schemas/FlowModule" + required: + - modules + + FlowModule: + type: object + properties: + input_transform: + type: object + additionalProperties: + $ref: "#/components/schemas/InputTransform" + value: + $ref: "#/components/schemas/FlowModuleValue" + required: + - input_transform + - value + + InputTransform: + type: object + properties: + type: + type: string + enum: + - static + - javascript + step: + type: number + value: {} + expr: + type: string + + FlowModuleValue: + type: object + properties: + path: + type: string + type: + type: string + enum: + - script + - flow + required: + - type + - path + + FlowPreview: + type: object + properties: + value: + $ref: "#/components/schemas/FlowValue" + path: + type: string + args: + $ref: "#/components/schemas/ScriptArgs" + + required: + - value + - content + - args + + FlowStatus: + type: object + properties: + step: + type: integer + modules: + type: array + items: + $ref: "#/components/schemas/FlowStatusModule" + failure_module: + $ref: "#/components/schemas/FlowStatusModule" + required: + - step + - modules + - failure_module + + FlowStatusModule: + type: object + properties: + type: + type: string + enum: + - WaitingForPriorSteps + - WaitingForEvent + - WaitingForExecutor + - InProgress + - Success + - Failure + job: + type: string + format: uuid + event: + type: string + + required: [type] diff --git a/backend/rustfmt.toml b/backend/rustfmt.toml new file mode 100644 index 0000000000..c3bdd32db2 --- /dev/null +++ b/backend/rustfmt.toml @@ -0,0 +1,7 @@ +imports_granularity = "Crate" +max_width = 100 +use_small_heuristics = "Default" +indent_style = "Block" +fn_single_line = false +force_multiline_blocks = true +format_strings = true diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json new file mode 100644 index 0000000000..6497d7a312 --- /dev/null +++ b/backend/sqlx-data.json @@ -0,0 +1,2972 @@ +{ + "db": "PostgreSQL", + "0355b53b1d45955ca56b2829372ce9c656d7f0ad7b8d0709161047f0d8cdc4f4": { + "query": "DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + null + ] + } + }, + "04a15674cf66f2822085e65ca33ee42b8e7f3f9d63ffac0585a12368d0322252": { + "query": "SELECT group_ FROM usr_to_group where usr = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "group_", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "09246e9ff5b2beb61ab51a5f73d980f7638904d5a18a415e52d5e1c94dffd0aa": { + "query": "SELECT SUM(duration) FROM completed_job WHERE created_by = $1 AND created_at > NOW() - INTERVAL '1200 seconds' AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "sum", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "097a576938eac385ddc2f16a00ddc69c3ca54f5a66923291730980eeeea1f8c1": { + "query": "DELETE FROM variable WHERE path = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "0a7212dd507ed8f7a311724185e39ecc1809abb208a681ad711614c27baadd83": { + "query": "SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "flow_status", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + true + ] + } + }, + "0a76ed47629cac693ba7f169a1229b62bd900bc007a63fbae3fa7374ba66df65": { + "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Bool" + ] + }, + "nullable": [] + } + }, + "0ae9160591ae00117d20a616cfe07e38f0c32953c7e881e916c389255190b72d": { + "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Bool" + ] + }, + "nullable": [] + } + }, + "103e321fbaa847831682b5cba2fd94f12c508ddf372f9facd18e30d00afd1ea3": { + "query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at FROM token WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "label", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "token_prefix", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "expiration", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "last_used_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true, + null, + true, + false, + false + ] + } + }, + "11eb4dd4a2c9b0b759294dde5e8b505c5a4391aa0d8cb629c665711ee0fc04a0": { + "query": "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "logs", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int4", + "Text", + "Uuid" + ] + }, + "nullable": [ + null + ] + } + }, + "122090a0f89e5248a0a0f199ebd24582fdb302883aebd2da187ac0084e767ea3": { + "query": "SELECT set_config('session.pgroups', $1, true)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "set_config", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "1289e7278d2a289bfaa53f00e0b6dceb195df0fb43a8ac03bc8b35939fc941dd": { + "query": "SELECT * FROM workspace LIMIT $1 OFFSET $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "owner", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "domain", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "deleted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false + ] + } + }, + "15de975d9be141c9ed9647935a508492aabbbddbf986d5c5c0f0c415293c432d": { + "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'), \n ($3, 'g/all/not_secret', $4, false, 'This item is not secret')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597": { + "query": "SELECT * FROM workspace_settings WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "slack_team_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "slack_name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "slack_command_script", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true, + true, + true + ] + } + }, + "1ad8677694aca94ee0e6da287d7cc028dcf673583a0e3e4fedd0e5d6766c5860": { + "query": "DELETE FROM usr WHERE email = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "1b31847d6187d6969deac5aa7b2feb169ef963449ac2d3ea06e1ed785f6d42e7": { + "query": "SELECT * from workspace_invite WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "is_admin", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + } + }, + "1bf2ca894246bd716875635b2d0c294a1ce2ed21916097ea165df240f7421a1e": { + "query": "UPDATE queue SET logs = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + } + }, + "21cd7cbab7799baf5c381427d9b373c0bb144715eddfe54e3b01f6049d7966a2": { + "query": "SELECT workspace.id, workspace.name, usr.username\n FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "username", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + } + }, + "230d58732a08164268ca10d248a93cced646632a76864b693ed2325d85b36c45": { + "query": "SELECT canceled FROM queue WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "canceled", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + } + }, + "255aafff962738317f3227ae4eb871830d89b4c12c73d8dbabe6836da124e54d": { + "query": "select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "28c042adef65c3055edc324fbbd2f267285d3566cbec58404983323d410ace27": { + "query": "SELECT super_admin FROM password WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "super_admin", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "28c1afcce0446817543fc47dde29b1137b2550bac4a2b6e81c72c74a84bb84fb": { + "query": "UPDATE workspace_settings SET slack_command_script = $1 WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + } + }, + "2a4be8334db7d39f3d954193a8b0169cc4a4a07e081d2fa61d8764879d6a8ff5": { + "query": "UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [] + } + }, + "2bf44d998d7acd17ec6d98f81395f8bdac49f58880fbbb9350bf0142cd2efdc7": { + "query": "DELETE FROM workspace_invite WHERE\n workspace_id = $1 AND email = $2 AND is_admin = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Bool" + ] + }, + "nullable": [] + } + }, + "37d3ee8009055e869941e548a6d5a352053a5d7782f662c34b94706488abccb6": { + "query": "UPDATE queue SET running = false WHERE last_ping < $1 RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [ + false + ] + } + }, + "3d363466d79075df3f74f946eff43ca89faefca3bcdf2c533425ca3868b0369a": { + "query": "SELECT * FROM usr where username = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "is_admin", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "operator", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "disabled", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "role", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true + ] + } + }, + "453501fbd61efd26647baf9b6ef702ce0bc2e920914e9f08fe5f2a5f4ab03f02": { + "query": "SELECT substr(logs, $1) as logs FROM completed_job WHERE workspace_id = $2 AND id = $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "logs", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int4", + "Text", + "Uuid" + ] + }, + "nullable": [ + null + ] + } + }, + "486f181a9ced2bdc7c8d93da22c9d3e229ef106174bff2c472dbe82622f382b6": { + "query": "UPDATE queue SET logs = concat(logs, $1::text) WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + } + }, + "4ad5fa2f08236507aad911a95697e84fc0c3a274ba0e928da28c4d146cf8f1a8": { + "query": "SELECT is_admin FROM usr where username = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "is_admin", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "4e0c8cd36dfa71d0d7b79cba4289177770ccfbccc83f82477229a74e9c95bb2e": { + "query": "UPDATE worker_ping SET ping_at = $1, jobs_executed = $2 WHERE worker = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Int4", + "Text" + ] + }, + "nullable": [] + } + }, + "4e8ff055a80cc2e6b7fd6653c0c8e9a0e4b223a4bdeab0cf43dc79ce10f635b9": { + "query": "INSERT INTO workspace\n (id, name, owner, domain)\n VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "4eb6b80c410e00e8a72eced6c25cdd3ed941e7a46a0619173f272eec7f28a3c1": { + "query": "UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path = $5 AND workspace_id = $6 RETURNING *", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "edited_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "schedule", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "offset_", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "args", + "type_info": "Jsonb" + }, + { + "ordinal": 9, + "name": "extra_perms", + "type_info": "Jsonb" + }, + { + "ordinal": 10, + "name": "is_flow", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Bool", + "Jsonb", + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ] + } + }, + "5061c0d054bf4f028e7fe51a8f9389024c6ae4492755cadac0f7167e5300bda0": { + "query": "INSERT INTO resource_type\n (workspace_id, name, schema, description)\n VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Jsonb", + "Text" + ] + }, + "nullable": [] + } + }, + "50d1d62e1a0044168ec485c7f69bfb88ad4ecf200b33cd99f92da969628fb9f4": { + "query": "SELECT key FROM workspace_key WHERE workspace_id = $1 AND kind = 'cloud'", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "key", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "514c5feb8a29a2a6b577553d8577a46bcbb196c62d046a0568c573ce96ff3c43": { + "query": "DELETE FROM usr_to_group WHERE group_ = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e": { + "query": "SELECT * FROM workspace_settings WHERE slack_team_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "slack_team_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "slack_name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "slack_command_script", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true, + true, + true + ] + } + }, + "54756c6c39888feb2206b056df1c84c3bb44adc490309954359845c06b6e607c": { + "query": "INSERT INTO token\n (token, email, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, $5)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Bool" + ] + }, + "nullable": [] + } + }, + "58efbf34ba014b4853ef20ae400929b57c1d9de4262189274badf100d29e0649": { + "query": "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "schema", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + true, + true + ] + } + }, + "5b9b58612ca0f703a5d154a76fab82ac2329aef965fa937bfab2810b6e1336a4": { + "query": "DELETE FROM group_ WHERE name = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "5da22b7f44b631740697e49d5766c31668233fe2453d51e8d9d4c45974492616": { + "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, $2, $3, true, $4) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "6199e8be5cb13db71108e555ea20f0b76dc38476670f9fc0667b057d2766d42e": { + "query": "SELECT set_config('session.groups', $1, true)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "set_config", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "62432110a09e68593ac52b3174c8bfa8736d5c7c1d8c7d6bed68f0dd9e06db7b": { + "query": "SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "63c4b9320681fac84ea92c25c0f6da5c9ac154dfccf575cea8145692246205c4": { + "query": "SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "63f330e98051ae9d0cf5617a553f769cc98b05fdc8643839b504023b26f38aab": { + "query": "UPDATE flow SET archived = true WHERE path = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "64d2318064711c2cfbaf7c5c2b02d92cc98ac8e33eb560b829c462c3159115eb": { + "query": "INSERT INTO workspace_key\n (workspace_id, kind, key)\n VALUES ($1, 'cloud', $2)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "6c63bbcb45d3f51eccaea52ec862700e1f1c2426d823abd951e1eea4fd9b85aa": { + "query": "UPDATE script SET lock_error_logs = $1 WHERE hash = $2 AND workspace_id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int8", + "Text" + ] + }, + "nullable": [] + } + }, + "6c7186de56bcd9983a64de0c01a733e818ebc30af2377158c8a92ec66c06464c": { + "query": "UPDATE password SET super_admin = $1 WHERE email = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Text" + ] + }, + "nullable": [] + } + }, + "6c90d5ea2a09b47b81fdab465062c25f8768b220e53bf469550a3a3697ab756a": { + "query": "SELECT set_config('session.user', $1, true)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "set_config", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "6eacfedfc1ab2431c318996d2ff480a65d55fc43d8fa95aa4e2e8430722dc82d": { + "query": "SELECT email FROM usr WHERE username = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "6f1ff39324639d0dced3731df5732a9d4ae3dbed660ca088de6471bd3b448a12": { + "query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = $6, schema = $7 WHERE path = $8 AND workspace_id = $9 RETURNING path", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Text", + "Jsonb", + "Varchar", + "Timestamptz", + "Json", + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "6fc2cfae9df83eb24ea33e4c9567740100f4dd2285afc3ef474fc70041b0567b": { + "query": "SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "worker", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "worker_instance", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "ping_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "ip", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "jobs_executed", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + } + }, + "701f215eb14ba67a79afea15d7effc0dd394ba6c4a72c95d4560c5a377015d4e": { + "query": "UPDATE workspace SET deleted = true WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "70e6ca3b5ad81f70376d31c75ba5f2b5dd94dd2f7f4cacd5a64029ccc3315d6c": { + "query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "765c18d77412cbb4474f4074d583b9b44681f3b9f58754662ac07a3a3470a3c5": { + "query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "is_admin", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "765d7a0562fec2a76355856df1b69a4536af25f0b5589b5d3828a75713d0331d": { + "query": "UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE id = $3 AND schedule_path IS NULL AND workspace_id = $4RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Uuid", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "76941665bf3591c15eda9d3b3d6e0f15a2aff9b7baf2c9c71e9a542e4c0bf8dd": { + "query": "INSERT INTO token\n (workspace_id, token, owner, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, $5, $6)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Bool" + ] + }, + "nullable": [] + } + }, + "7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234": { + "query": "SELECT * FROM resource_type WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "schema", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true, + true + ] + } + }, + "7eeac533a0d63f4e3af9d3e3123b0a73f44543e618e29e4c6a6d573852339933": { + "query": "SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "808ff76fdb74482d5d3201417c8a2470e2867cb08b9a46d244653eb366d8ee5e": { + "query": "INSERT INTO group_ (workspace_id, name, summary, extra_perms) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Text", + "Jsonb" + ] + }, + "nullable": [] + } + }, + "8114333781add802159cd2c26d57cea40e27143606c71bf4d87dc14bb7040cd6": { + "query": "UPDATE workspace SET name = $1, owner = $2, domain = $3 WHERE id = $4", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Text" + ] + }, + "nullable": [] + } + }, + "826c6a36020f402169cab2ebe097e9f38f416f6f080a2fe8a9b83e97c4e15150": { + "query": "UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "hash", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "82f3c4cd1c1f6aea86d66f675442587684391bc32be9ab55ae20aab549b7bba5": { + "query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "858906ef1a5da30956823b56d28389146af50bfe206355abf921e7258d75510a": { + "query": "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::text::json)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Text", + "Text", + "Jsonb", + "Varchar", + "Timestamptz", + "Text" + ] + }, + "nullable": [] + } + }, + "87f08f146fc899d317728f21468a1474ed7c58aa5da19bef642efe66fabe5118": { + "query": "SELECT * from usr WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "is_admin", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "operator", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "disabled", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "role", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true + ] + } + }, + "88b7589a6416da8be4b26af3bf30fcfcd6aeae7bc5a37e9a735cabbe2691c570": { + "query": "SELECT login_type::TEXT FROM password WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "login_type", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "894336809c161cdbb42ea235eb88498db6b1715386f78f57568f01c9954f05af": { + "query": "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "8a80333c2fbf7b50fed305882de6e4ffda985d5c648cd617add6c9e6a9c03f34": { + "query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Jsonb", + "Text", + "Varchar" + ] + }, + "nullable": [] + } + }, + "8ad6a17eecce77f61236e0585ba89b99a32e07ad37b97662007db35acb59d139": { + "query": "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "schema", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true, + true + ] + } + }, + "8caa01546506f42740b7973a3a45e40093a06714b8524570c5143b77af4a8e19": { + "query": "SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "edited_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "schedule", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "offset_", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "args", + "type_info": "Jsonb" + }, + { + "ordinal": 9, + "name": "extra_perms", + "type_info": "Jsonb" + }, + { + "ordinal": 10, + "name": "is_flow", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ] + } + }, + "8d631abe38ae964edb357217463a2c1617d4b3b18a7ed0724ad1c65b95980a2c": { + "query": "SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "value", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "8dbab3cc7d25a38301c54756a26827a0957c4edb5e68b788c19d3e60b5f038ea": { + "query": "SELECT COUNT(id) FROM queue WHERE created_by = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "8ede6fb740b145b3a8320adb789500870c7a8ec807a7b156b9ff7a15791b78f8": { + "query": "DELETE FROM usr WHERE workspace_id = $1 AND username = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "902961f15b8c7603dddf2933b5fc7cdd6e5af3545835763ccb29cdf3ac273ef0": { + "query": "UPDATE password SET password_hash = $1 WHERE email = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + } + }, + "904457944cbfdcefa1934059bbfea015a411c739124cf06367975cfd9cd0dd0d": { + "query": "SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "value", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "resource_type", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "extra_perms", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + false + ] + } + }, + "90719f6230467b08e5f2cc89271bcf5e4a6cec39e9d9b42ef3b90f09b3135b83": { + "query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "login_type", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "super_admin", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "verified", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "company", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + null, + false, + false, + true, + true + ] + } + }, + "9490a4388f43e45e32911f1129e623f2d73ce2da7a948fe134bea1c87cdbefd1": { + "query": "SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "value", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true + ] + } + }, + "9a581f49d34d62550e58e6210b4bd24b7db499cc5e0350c0ce7024b3d59b13ab": { + "query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name)\n VALUES ($1, $2, $3) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "a151ceeddfd4a2825d4528542d3adc5c0a6947573558a2fd62429ba5da369617": { + "query": "INSERT INTO queue\n (workspace_id, id, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Timestamptz", + "Int8", + "Varchar", + "Text", + "Jsonb", + { + "Custom": { + "name": "job_kind", + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview" + ] + } + } + }, + "Varchar", + "Jsonb", + "Jsonb", + "Bool" + ] + }, + "nullable": [ + false + ] + } + }, + "a1d46b44718a63d6ce5a9054d493dadbffb205500dc8fb55e9816bcdb613e0d5": { + "query": "DELETE FROM queue WHERE schedule_path = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "a34066d4a1578a13b2e322e6936ae80a0239a79148f3edce65f51a93910a1a4b": { + "query": "SELECT email FROM usr where username = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "a8507084c0f45f4c08c0b317d69db26f97295bb2d410e3e5b8da2cd420536145": { + "query": "INSERT INTO completed_job as cj\n (workspace_id, id, parent_job, created_by, created_at, duration, success, script_hash, script_path, args, result, logs, \n raw_code, canceled, canceled_by, canceled_reason, job_kind, schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Timestamptz", + "Int4", + "Bool", + "Int8", + "Varchar", + "Jsonb", + "Jsonb", + "Text", + "Text", + "Bool", + "Varchar", + "Text", + { + "Custom": { + "name": "job_kind", + "kind": { + "Enum": [ + "script", + "preview", + "flow", + "dependencies", + "flowpreview" + ] + } + } + }, + "Varchar", + "Varchar", + "Jsonb", + "Jsonb", + "Bool" + ] + }, + "nullable": [ + false + ] + } + }, + "a98b2d68f023f46ab91167d3147416df672c2aed2ba5ab70e98a9da5fa47255a": { + "query": "INSERT INTO workspace_settings\n (workspace_id)\n VALUES ($1)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar" + ] + }, + "nullable": [] + } + }, + "aa2800113a8a8805f47cdc1dd0f29d94c546fe531e7edd3e91da4978af5442fb": { + "query": "SELECT * FROM schedule WHERE path = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "edited_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "schedule", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "offset_", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "args", + "type_info": "Jsonb" + }, + { + "ordinal": 9, + "name": "extra_perms", + "type_info": "Jsonb" + }, + { + "ordinal": 10, + "name": "is_flow", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ] + } + }, + "abc9f034e62ac224894173356aa69e09f3647a45d176e253e4fc8f7206f6a18d": { + "query": "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "summary", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "extra_perms", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + false + ] + } + }, + "add01e9e31d64e88b84c9505fe3de553031e581b1bb173413a9a3e3eb0817b43": { + "query": "INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "afe9ea97d6e4c45453e21dc3d218fe8927f9faa58921666ceb7014e1f695d900": { + "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, $2, $3, $4, $5)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Varchar" + ] + }, + "nullable": [] + } + }, + "b20977e70ebac7ccbaec5a2a1e940301dd331a5f9a4be67a27cfbff8619ac8f0": { + "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, true)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "b24c1a2587812d2fd40063328ce393377826b170a7970ba02f4057454058321b": { + "query": "UPDATE usr SET is_admin = $1 WHERE username = $2 AND workspace_id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "b3b80de52d0931a2fdb5d38b7603a2d69cc25ab1cda413228c363a5ffd777113": { + "query": "SELECT * from workspace_invite WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "is_admin", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + } + }, + "b7dd791cd69748ef51b7520f505c0c8bb1b4014a273476eddfecf1ab658a18b4": { + "query": "select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')) AND\n deleted = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "hash", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "b87f19d248b7c5dbb57e9c6c3e4dd8a5bbb70815c06ee8e5bc8d57324eea1617": { + "query": "SELECT (flow_status->'step')::integer FROM queue WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "int4", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + } + }, + "bf1d8e043338867e1da1ed236ff6c85a566d5fd58d4b0d5c3a10454513811ba3": { + "query": "UPDATE workspace_settings\n SET slack_team_id = null, slack_name = null WHERE workspace_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "bf2aeb9a1e649106d2a084c1d628690a44573c1869a206474811215714ba97c2": { + "query": "DELETE FROM resource WHERE path = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "c213427a32e8903fff649a3e7aa8392d2215665ed04f5a8f2178861e9dba298a": { + "query": "UPDATE schedule SET enabled = $1 WHERE path = $2 AND workspace_id = $3 RETURNING *", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "edited_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "schedule", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "offset_", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "args", + "type_info": "Jsonb" + }, + { + "ordinal": 9, + "name": "extra_perms", + "type_info": "Jsonb" + }, + { + "ordinal": 10, + "name": "is_flow", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Bool", + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ] + } + }, + "c2d6cb56c1dea4498e2aab9ea9301dbbaa127602a38f57f5add4108fdc209b1a": { + "query": "SELECT usr.username \n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username \n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "username", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "c59dd666b9a316c027e8c319b80ccbab3a220d93b64357981bd4a03324dad1d0": { + "query": "SELECT is_secret from variable WHERE path = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "is_secret", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "c68d39492686d0fd275acf4eca3844c37d0f58647a100d24afcf090b4b13f85d": { + "query": "SELECT username from usr WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "username", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "cb12aee5f8e04cb196d4b8fad81699fcbb1ae7b0c84090d5705b14eac76074ff": { + "query": "INSERT INTO group_\n VALUES ($1, 'all', 'The group that always contains all users of this workspace')", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar" + ] + }, + "nullable": [] + } + }, + "d2dcf69b20488d610599c309862722f805049e479035be6a416d05d73528a8e1": { + "query": "INSERT INTO group_\n (workspace_id, name, summary)\n VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Text" + ] + }, + "nullable": [] + } + }, + "d4eb7aea60894b65498144b9bf522beba612f36368d62fe4e94b5b9e26349d32": { + "query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'demo')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + } + }, + "d5d97005eebcb2760216dbd10872acdb42868fd5f7a27f71836e7a6ff1c69856": { + "query": "DELETE FROM resource_type WHERE name = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "d697b7311430e7bd5375ec5494179f4071e3bfe123d9600249cfa80c1103edd8": { + "query": "UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int8", + "Text" + ] + }, + "nullable": [] + } + }, + "d9c8f6ec7bd10e533876526255c15e376ccb4f898b9c0ab8840b2930bda24fdc": { + "query": "SELECT * FROM group_ WHERE name = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "summary", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "extra_perms", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + true, + false + ] + } + }, + "dd60eb23701e97460e307b6152404e939e5bbf22d425cadd876f629134c4a683": { + "query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ('demo', $1, false)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar" + ] + }, + "nullable": [] + } + }, + "dd7940ec390357b268d616e1880516ecc08d506db2109efcce840f096d7a594e": { + "query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE id = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "df8f9b2e601b0157759e9507308ea7df562a7a42516be0fcb465b8e34de0f438": { + "query": "SELECT EXISTS(SELECT 1 FROM workspace WHERE workspace.id = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + } + }, + "e3eeda2e19bfbfd5aadd71c40774f7e93c8479a777fdb2828607b1db36361726": { + "query": "INSERT INTO usr_to_group\n VALUES ($1, 'all', $2)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "e5231634fbb37d45e5cf3e5dfb39cb829d224caad1c3ca1712d0fc4c495aa4fc": { + "query": "SELECT hash FROM script WHERE parent_hashes[1] = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "hash", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "e541e2d255e39cc3372769e3ccbd610441871e9f2af6249a8599a2a720913baa": { + "query": "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "edited_by", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "edited_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "schedule", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "offset_", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "script_path", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "args", + "type_info": "Jsonb" + }, + { + "ordinal": 9, + "name": "extra_perms", + "type_info": "Jsonb" + }, + { + "ordinal": 10, + "name": "is_flow", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Int4", + "Varchar", + "Varchar", + "Bool", + "Jsonb" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false + ] + } + }, + "e94abd39ec51b7e0c48c190d47ed766fd4f401187c3b60b3e599426c95232f7f": { + "query": "UPDATE queue SET last_ping = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Uuid" + ] + }, + "nullable": [] + } + }, + "ea8ebb8d972fe99c960b5a69f794ee2b57bfb1914bf370c5b10313e45fa9b65f": { + "query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Jsonb", + "Text", + "Varchar" + ] + }, + "nullable": [] + } + }, + "ef8b14d0feb4bda6a1f9834712ab47bd21e07f0a743f74a8d1f84cb3d54bfdae": { + "query": "SELECT * from usr LIMIT $1 OFFSET $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "is_admin", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "operator", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "disabled", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "role", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true + ] + } + }, + "f056b5f3e66a764748925f1bfd3180923fde8c7fdf69088d0e4a5555cc049545": { + "query": "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "result", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + true + ] + } + }, + "f12e710a0c2b2e13b98fd522028b6af8be74a9126a8207aa2974b47fd36e1845": { + "query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8", + "Varchar", + "Int8Array", + "Text", + "Text", + "Text", + "Varchar", + "Text", + "Bool", + "Jsonb", + "Text" + ] + }, + "nullable": [] + } + }, + "f325a1262084bd3468e12dc8bcc289a96536f172b679af54dd0fbc82d4d7c987": { + "query": "DELETE FROM usr_to_group WHERE usr = $1 AND group_ = $2 AND workspace_id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + } + }, + "f7906298e4204ad55ec84021bb2461f369386493519637279f1188227230c580": { + "query": "SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "lock", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "lock_error_logs", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + true, + true + ] + } + }, + "f7f2598ac824e9b5719b96b1d1873ad728f7f786c7f447df6cac3e65d5a53bf1": { + "query": "SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "owner", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "domain", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "deleted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true, + false + ] + } + }, + "f98046a2ee4ac10d9e507c033391dfb0c704dcd513d8b1e5564def4e85f9e80b": { + "query": "INSERT INTO worker_ping (worker_instance, worker, ip) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "fa258894bd90ea6586669e5810c5c6bcb42d5e1e68fab27fb185d06962b6454a": { + "query": "SELECT * FROM resource WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "path", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "value", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "resource_type", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "extra_perms", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + false + ] + } + } +} \ No newline at end of file diff --git a/backend/src/audit.rs b/backend/src/audit.rs new file mode 100644 index 0000000000..441fdbd56c --- /dev/null +++ b/backend/src/audit.rs @@ -0,0 +1,159 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use sql_builder::prelude::*; + +use std::collections::HashMap; + +use crate::{ + db::UserDB, + error::{Error, JsonResult, Result}, + users::Authed, + utils::Pagination, +}; +use axum::{ + extract::{Extension, Path, Query}, + routing::get, + Json, Router, +}; + +use serde::{Deserialize, Serialize}; +use sql_builder::SqlBuilder; +use sqlx::{FromRow, Postgres, Transaction}; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/list", get(list_audit)) + .route("/get/:id", get(get_audit)) +} + +#[derive(sqlx::Type, Serialize, Deserialize, Debug)] +#[sqlx(type_name = "ACTION_KIND", rename_all = "lowercase")] +pub enum ActionKind { + Create, + Update, + Delete, + Execute, +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct AuditLog { + pub workspace_id: String, + pub id: i32, + pub timestamp: chrono::DateTime, + pub username: String, + pub operation: String, + pub action_kind: ActionKind, + pub resource: Option, + pub parameters: Option, +} + +pub async fn audit_log<'c>( + db: &mut Transaction<'c, Postgres>, + username: &str, + operation: &str, + action_kind: ActionKind, + w_id: &str, + resource: Option<&str>, + parameters: Option>, +) -> Result<()> { + let p_json: serde_json::Value = serde_json::to_value(¶meters).unwrap(); + + tracing::info!( + username = username, + kind = "audit", + operation = operation, + workspace = w_id, + action_kind = ?action_kind, + resource = resource, + parameters = %p_json + ); + sqlx::query( + "INSERT INTO audit + (workspace_id, username, operation, action_kind, resource, parameters) + VALUES ($1, $2, $3, $4, $5, $6)", + ) + .bind(w_id) + .bind(username) + .bind(operation) + .bind(action_kind) + .bind(resource) + .bind(p_json) + .execute(db) + .await?; + Ok(()) +} + +#[derive(Deserialize)] +pub struct ListAuditLogQuery { + pub username: Option, + pub operation: Option, + pub action_kind: Option, + pub resource: Option, + pub before: Option>, + pub after: Option>, +} + +async fn list_audit( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(pagination): Query, + Query(lq): Query, +) -> JsonResult> { + let (per_page, offset) = crate::utils::paginate(pagination); + + let mut sqlb = SqlBuilder::select_from("audit") + .field("*") + .order_by("id", true) + .and_where_eq("workspace_id", "?".bind(&w_id)) + .offset(offset) + .limit(per_page) + .clone(); + + if let Some(u) = &lq.username { + sqlb.and_where_eq("username", "?".bind(u)); + } + if let Some(o) = &lq.operation { + sqlb.and_where_eq("operation", "?".bind(o)); + } + if let Some(ak) = &lq.action_kind { + sqlb.and_where_eq("action_kind", "?".bind(ak)); + } + if let Some(r) = &lq.resource { + sqlb.and_where_eq("resource", "?".bind(r)); + } + if let Some(b) = &lq.before { + sqlb.and_where_le("timestamp", format!("to_timestamp({})", b.timestamp())); + } + if let Some(a) = &lq.after { + sqlb.and_where_gt("timestamp", format!("to_timestamp({})", a.timestamp())); + } + + 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::<_, AuditLog>(&sql) + .fetch_all(&mut tx) + .await?; + tx.commit().await?; + Ok(Json(rows)) +} + +async fn get_audit( + authed: Authed, + Extension(user_db): Extension, + Path(id): Path, +) -> JsonResult { + let mut tx = user_db.begin(&authed).await?; + let audit_o = sqlx::query_as::<_, AuditLog>("SELECT * FROM audit WHERE id = $1") + .bind(id) + .fetch_optional(&mut tx) + .await?; + tx.commit().await?; + let audit = crate::utils::not_found_if_none(audit_o, "AuditLog", &id.to_string())?; + Ok(Json(audit)) +} diff --git a/backend/src/client.rs b/backend/src/client.rs new file mode 100644 index 0000000000..4206900fd8 --- /dev/null +++ b/backend/src/client.rs @@ -0,0 +1,44 @@ +use crate::{error::Error, variables::ListableVariable}; + +pub async fn get_variable( + workspace: &str, + path: &str, + token: &str, + base_url: &str, +) -> Result { + let client = reqwest::Client::new(); + let res = client + .get(format!("{base_url}/api/w/{workspace}/variables/get/{path}")) + .bearer_auth(token) + .send() + .await?; + if res.status().is_success() { + let value = res + .json::() + .await? + .value + .unwrap_or_else(|| "".to_string()); + Ok(value) + } else { + Err(Error::NotFound(format!("Variable not found at {path}")))? + } +} + +pub async fn get_resource( + workspace: &str, + path: &str, + token: &str, + base_url: &str, +) -> Result, anyhow::Error> { + let client = reqwest::Client::new(); + let result = client + .get(format!( + "{base_url}/api/w/{workspace}/resources/get_value/{path}" + )) + .bearer_auth(token) + .send() + .await? + .json::>() + .await?; + Ok(result) +} diff --git a/backend/src/db.rs b/backend/src/db.rs new file mode 100644 index 0000000000..01848f3b56 --- /dev/null +++ b/backend/src/db.rs @@ -0,0 +1,93 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use crate::{error::Error, users::Authed}; +use sqlx::{postgres::PgPoolOptions, Pool, Postgres, Transaction}; +use std::time::Duration; + +pub type DB = Pool; + +pub async fn connect(database_url: &str) -> Result { + PgPoolOptions::new() + .max_connections(100) + .max_lifetime(Duration::from_secs(30 * 60)) // 30 mins + .connect(database_url) + .await + .map_err(|err| Error::ConnectingToDatabase(err.to_string())) +} + +pub async fn migrate(db: &DB) -> Result<(), Error> { + match sqlx::migrate!("./migrations").run(db).await { + Ok(_) => Ok(()), + Err(err) => Err(err), + }?; + + Ok(()) +} + +pub async fn setup_app_user(db: &DB, password: &str) -> Result<(), Error> { + let mut tx = db.begin().await?; + + sqlx::query(&format!("ALTER USER app WITH PASSWORD '{}'", password)) + .execute(&mut tx) + .await?; + sqlx::query(&format!("ALTER USER admin WITH PASSWORD '{}'", password)) + .execute(&mut tx) + .await?; + tx.commit().await?; + + Ok(()) +} +#[derive(Clone)] +pub struct UserDB { + db: DB, +} + +impl UserDB { + pub fn new(db: DB) -> Self { + Self { db } + } + + pub async fn begin( + self, + authed: &Authed, + ) -> Result, sqlx::Error> { + let mut tx = self.db.begin().await?; + let user = if authed.is_admin { "admin" } else { "app" }; + + sqlx::query(&format!("SET LOCAL SESSION AUTHORIZATION {}", user)) + .execute(&mut tx) + .await?; + + sqlx::query!( + "SELECT set_config('session.user', $1, true)", + authed.username + ) + .fetch_optional(&mut tx) + .await?; + + sqlx::query!( + "SELECT set_config('session.groups', $1, true)", + &authed.groups.join(",") + ) + .fetch_optional(&mut tx) + .await?; + + sqlx::query!( + "SELECT set_config('session.pgroups', $1, true)", + &authed + .groups + .iter() + .map(|x| format!("g/{}", x)) + .collect::>() + .join(",") + ) + .fetch_optional(&mut tx) + .await?; + Ok(tx) + } +} diff --git a/backend/src/email.rs b/backend/src/email.rs new file mode 100644 index 0000000000..b0d7d9c526 --- /dev/null +++ b/backend/src/email.rs @@ -0,0 +1,31 @@ +use lettre::{ + transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, + Tokio1Executor, +}; + +use crate::error::{Error, Result}; + +pub struct EmailSender { + pub from: String, + pub server: String, + pub password: String, +} + +impl EmailSender { + pub async fn send_email(&self, email: Message) -> Result<()> { + let creds = Credentials::new(self.from.to_string(), self.password.to_string()); + + // Open a remote connection to gmail + let mailer: AsyncSmtpTransport = + AsyncSmtpTransport::::relay(&self.server) + .unwrap() + .credentials(creds) + .build(); + + mailer + .send(email) + .await + .map_err(|x| Error::InternalErr(format!("Impossible to send email {x}")))?; + Ok(()) + } +} diff --git a/backend/src/error.rs b/backend/src/error.rs new file mode 100644 index 0000000000..a58bf34861 --- /dev/null +++ b/backend/src/error.rs @@ -0,0 +1,67 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use axum::{ + body::{self, BoxBody}, + response::IntoResponse, + Json, +}; +use hyper::{Response, StatusCode}; +use sqlx::migrate::MigrateError; +use thiserror::Error; +use tokio::io; + +pub type Result = std::result::Result; +pub type JsonResult = std::result::Result, Error>; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Uuid Error {0}")] + UuidErr(#[from] uuid::Error), + #[error("Bad config: {0}")] + BadConfig(String), + #[error("Connecting to database: {0}")] + ConnectingToDatabase(String), + #[error("Not found: {0}")] + NotFound(String), + #[error("Not authorized: {0}")] + NotAuthorized(String), + #[error("{0}")] + ExecutionErr(String), + #[error("IO error: {0}")] + IoErr(#[from] io::Error), + #[error("Sql error: {0}")] + SqlErr(#[from] sqlx::Error), + #[error("Bad request: {0}")] + BadRequest(String), + #[error("Internal: {0}")] + InternalErr(String), + #[error("Hexadecimal decoding error: {0}")] + HexErr(#[from] hex::FromHexError), + #[error("Migrating database: {0}")] + DatabaseMigration(#[from] MigrateError), + #[error("{0}")] + Anyhow(#[from] anyhow::Error), +} + +pub fn to_anyhow(e: T) -> anyhow::Error { + From::from(e) +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let e = &self; + let body = body::boxed(body::Full::from(e.to_string())); + let status = match self { + Self::NotFound(_) => StatusCode::NOT_FOUND, + Self::NotAuthorized(_) => StatusCode::UNAUTHORIZED, + Self::SqlErr(_) | Self::BadRequest(_) => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + Response::builder().status(status).body(body).unwrap() + } +} diff --git a/backend/src/flow.rs b/backend/src/flow.rs new file mode 100644 index 0000000000..42e9d7698a --- /dev/null +++ b/backend/src/flow.rs @@ -0,0 +1,326 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use std::collections::HashMap; + +use sql_builder::prelude::*; + +use axum::{ + extract::{Extension, Path, Query}, + routing::{get, post}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use sql_builder::SqlBuilder; +use sqlx::FromRow; + +use crate::{ + audit::{audit_log, ActionKind}, + db::UserDB, + error::{Error, JsonResult, Result}, + scripts::Schema, + users::Authed, + utils::{Pagination, StripPath}, +}; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/list", get(list_flows)) + .route("/create", post(create_flow)) + .route("/update/*path", post(update_flow)) + .route("/archive/*path", post(archive_flow_by_path)) + .route("/get/*path", get(get_flow_by_path)) +} + +#[derive(FromRow, Serialize)] +pub struct Flow { + pub workspace_id: String, + pub path: String, + pub summary: String, + pub description: String, + pub value: serde_json::Value, + pub edited_by: String, + pub edited_at: chrono::DateTime, + pub archived: bool, + pub schema: Option, + pub extra_perms: serde_json::Value, +} + +#[derive(FromRow, Deserialize)] +pub struct NewFlow { + pub path: String, + pub summary: String, + pub description: String, + pub value: serde_json::Value, + pub schema: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct FlowValue { + pub modules: Vec, + pub failure_module: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct FlowModule { + pub input_transform: HashMap, + pub value: FlowModuleValue, +} + +#[derive(Deserialize, Serialize)] +#[serde( + tag = "type", + rename_all(serialize = "lowercase", deserialize = "lowercase") +)] +pub enum InputTransform { + Static { value: serde_json::Value }, + Javascript { expr: String }, + Resource { path: String }, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde( + tag = "type", + rename_all(serialize = "lowercase", deserialize = "lowercase") +)] +pub enum FlowModuleValue { + Script { path: String }, + Flow { path: String }, +} + +#[derive(Deserialize)] +pub struct ListFlowQuery { + pub path_start: Option, + pub path_exact: Option, + pub edited_by: Option, + pub show_archived: Option, + pub order_by: Option, + pub order_desc: Option, +} + +async fn list_flows( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(pagination): Query, + Query(lq): Query, +) -> JsonResult> { + let (per_page, offset) = crate::utils::paginate(pagination); + + let mut sqlb = SqlBuilder::select_from("flow as o") + .fields(&[ + "workspace_id", + "path", + "summary", + "description", + "'{}'::jsonb as value", + "edited_by", + "edited_at", + "archived", + "schema", + "extra_perms", + ]) + .order_by("edited_at", lq.order_desc.unwrap_or(true)) + .and_where("workspace_id = ? OR workspace_id = 'starter'".bind(&w_id)) + .offset(offset) + .limit(per_page) + .clone(); + + if !lq.show_archived.unwrap_or(false) { + sqlb.and_where_eq("archived", false); + } + if let Some(ps) = &lq.path_start { + sqlb.and_where_like_left("path", "?".bind(ps)); + } + if let Some(p) = &lq.path_exact { + sqlb.and_where_eq("path", "?".bind(p)); + } + if let Some(cb) = &lq.edited_by { + sqlb.and_where_eq("edited_by", "?".bind(cb)); + } + + 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::<_, Flow>(&sql).fetch_all(&mut tx).await?; + tx.commit().await?; + Ok(Json(rows)) +} + +async fn create_flow( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(nf): Json, +) -> Result { + // cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; + let mut tx = user_db.begin(&authed).await?; + + sqlx::query!( + "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::text::json)", + w_id, + nf.path, + nf.summary, + nf.description, + nf.value, + &authed.username, + &chrono::Utc::now(), + nf.schema.and_then(|x| serde_json::to_string(&x.0).ok()), + ) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &authed.username, + "flows.create", + ActionKind::Create, + &w_id, + Some(&nf.path.to_string()), + Some( + [Some(("flow", nf.path.as_str()))] + .into_iter() + .flatten() + .collect(), + ), + ) + .await?; + + tx.commit().await?; + Ok(nf.path.to_string()) +} + +async fn update_flow( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, flow_path)): Path<(String, StripPath)>, + Json(nf): Json, +) -> Result { + let mut tx = user_db.begin(&authed).await?; + + let flow_path = flow_path.to_path(); + let schema = nf.schema.map(|x| x.0); + let flow = sqlx::query_scalar!( + "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = $6, schema = $7 WHERE path = $8 AND workspace_id = $9 RETURNING path", + nf.path, + nf.summary, + nf.description, + nf.value, + &authed.username, + &chrono::Utc::now(), + schema, + flow_path, + w_id, + ) + .fetch_optional(&mut tx) + .await?; + crate::utils::not_found_if_none(flow, "Flow", flow_path)?; + + audit_log( + &mut tx, + &authed.username, + "flows.update", + ActionKind::Create, + &w_id, + Some(&nf.path.to_string()), + Some( + [Some(("flow", nf.path.as_str()))] + .into_iter() + .flatten() + .collect(), + ), + ) + .await?; + + tx.commit().await?; + Ok(nf.path.to_string()) +} + +async fn get_flow_by_path( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + + let flow_o = sqlx::query_as::<_, Flow>( + "SELECT * FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + ) + .bind(path) + .bind(w_id) + .fetch_optional(&mut tx) + .await?; + tx.commit().await?; + + let flow = crate::utils::not_found_if_none(flow_o, "Flow", path)?; + Ok(Json(flow)) +} + +async fn archive_flow_by_path( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> Result { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + + sqlx::query!( + "UPDATE flow SET archived = true WHERE path = $1 AND workspace_id = $2", + path, + &w_id + ) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &authed.username, + "flows.archive", + ActionKind::Delete, + &w_id, + Some(path), + Some([("workspace", w_id.as_str())].into()), + ) + .await?; + tx.commit().await?; + + Ok(format!("Flow {path} archived")) +} + +#[cfg(test)] +mod tests { + + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + + #[test] + fn test_serialize() -> anyhow::Result<()> { + let mut hm = HashMap::new(); + hm.insert( + "test".to_owned(), + InputTransform::Static { + value: serde_json::json!("test2"), + }, + ); + let fv = FlowValue { + modules: vec![FlowModule { + input_transform: hm, + value: FlowModuleValue::Script { + path: "test".to_string(), + }, + }], + failure_module: Some(FlowModule { + input_transform: HashMap::new(), + value: FlowModuleValue::Flow { + path: "test".to_string(), + }, + }), + }; + println!("{}", serde_json::json!(fv).to_string()); + Ok(()) + } +} diff --git a/backend/src/granular_acls.rs b/backend/src/granular_acls.rs new file mode 100644 index 0000000000..82b622cbda --- /dev/null +++ b/backend/src/granular_acls.rs @@ -0,0 +1,116 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use crate::{ + db::UserDB, + error::{Error, JsonResult, Result}, + users::Authed, + utils::StripPath, +}; +use axum::{ + extract::{Extension, Path}, + routing::{get, post}, + Json, Router, +}; + +use serde::{Deserialize, Serialize}; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/get/*path", get(get_granular_acls)) + .route("/add/*path", post(add_granular_acl)) + .route("/remove/*path", post(remove_granular_acl)) +} + +#[derive(Serialize, Deserialize)] +pub struct GranularAcl { + pub owner: String, + pub write: Option, +} + +async fn add_granular_acl( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + Json(GranularAcl { owner, write }): Json, +) -> Result { + let path = path.to_path(); + 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 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" + )) + .bind(write.unwrap_or(false)) + .bind(path) + .bind(&w_id) + .fetch_optional(&mut tx) + .await?; + + let _ = crate::utils::not_found_if_none(obj_o, &kind, &path)?; + tx.commit().await?; + + Ok("Successfully modified granular acl".to_string()) +} + +async fn remove_granular_acl( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + Json(GranularAcl { owner, write: _ }): Json, +) -> Result { + let path = path.to_path(); + 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 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" + )) + .bind(owner) + .bind(path) + .bind(w_id) + .fetch_optional(&mut tx) + .await?; + + let _ = crate::utils::not_found_if_none(obj_o, &kind, &path)?; + tx.commit().await?; + + Ok("Successfully removed granular acl".to_string()) +} + +async fn get_granular_acls( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + 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 obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!( + "SELECT extra_perms from {kind} WHERE {identifier} = $1 AND workspace_id = $2" + )) + .bind(path) + .bind(w_id) + .fetch_optional(&mut tx) + .await?; + + let obj = crate::utils::not_found_if_none(obj_o, &kind, &path)?; + tx.commit().await?; + + Ok(Json(obj)) +} diff --git a/backend/src/groups.rs b/backend/src/groups.rs new file mode 100644 index 0000000000..70eee7d6ff --- /dev/null +++ b/backend/src/groups.rs @@ -0,0 +1,330 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use crate::{ + audit::{audit_log, ActionKind}, + db::{UserDB, DB}, + error::{Error, JsonResult, Result}, + users::{owner_to_token_owner, Authed}, + utils::Pagination, +}; +use axum::{ + extract::{Extension, Path, Query}, + routing::{delete, get, post}, + Json, Router, +}; + +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, Postgres, Transaction}; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/list", get(list_groups)) + .route("/listnames", get(list_group_names)) + .route("/create", post(create_group)) + .route("/get/:name", get(get_group)) + .route("/update/:name", post(update_group)) + .route("/delete/:name", delete(delete_group)) + .route("/adduser/:name", post(add_user)) + .route("/removeuser/:name", post(remove_user)) +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Group { + pub workspace_id: String, + pub name: String, + pub summary: Option, + pub extra_perms: serde_json::Value, +} + +#[derive(Deserialize)] +pub struct NewGroup { + pub name: String, + pub summary: Option, +} + +#[derive(Serialize)] +pub struct GroupInfo { + pub workspace_id: String, + pub name: String, + pub summary: Option, + pub members: Vec, + pub extra_perms: serde_json::Value, +} + +#[derive(Deserialize)] +pub struct EditGroup { + pub summary: Option, +} + +#[derive(Deserialize)] +pub struct Username { + pub username: String, +} + +async fn list_groups( + Extension(db): Extension, + Path(w_id): Path, + Query(pagination): Query, +) -> JsonResult> { + let (per_page, offset) = crate::utils::paginate(pagination); + + let rows = sqlx::query_as!( + Group, + "SELECT * FROM group_ WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3", + w_id, + per_page as i64, + offset as i64 + ) + .fetch_all(&db) + .await?; + + Ok(Json(rows)) +} + +async fn list_group_names( + Extension(db): Extension, + Path(w_id): Path, +) -> JsonResult> { + let rows = sqlx::query_scalar!( + "SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc", + w_id + ) + .fetch_all(&db) + .await?; + + Ok(Json(rows)) +} + +async fn create_group( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(ng): Json, +) -> Result { + let mut tx = user_db.begin(&authed).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}) + ) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &authed.username, + "group.create", + ActionKind::Create, + &w_id, + Some(&ng.name.to_string()), + None, + ) + .await?; + + tx.commit().await?; + Ok(format!("Created group {}", ng.name)) +} + +pub async fn get_group_opt<'c>( + db: &mut Transaction<'c, Postgres>, + w_id: &str, + name: &str, +) -> Result> { + let group_opt = sqlx::query_as!( + Group, + "SELECT * FROM group_ WHERE name = $1 AND workspace_id = $2", + name, + w_id + ) + .fetch_optional(db) + .await?; + Ok(group_opt) +} + +async fn get_group( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, +) -> JsonResult { + let mut tx = user_db.begin(&authed).await?; + + let group = crate::utils::not_found_if_none( + get_group_opt(&mut tx, &w_id, &name).await?, + "Group", + &name, + )?; + + let members = sqlx::query_scalar!( + "SELECT usr.username + FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username + WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2", + name, + w_id + ) + .fetch_all(&mut tx) + .await?; + + tx.commit().await?; + Ok(Json(GroupInfo { + workspace_id: group.workspace_id, + name: group.name, + summary: group.summary, + members, + extra_perms: group.extra_perms, + })) +} + +async fn delete_group( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, +) -> Result { + let mut tx = user_db.begin(&authed).await?; + + crate::utils::not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?; + + sqlx::query!( + "DELETE FROM usr_to_group WHERE group_ = $1 AND workspace_id = $2", + name, + w_id + ) + .execute(&mut tx) + .await?; + sqlx::query!( + "DELETE FROM group_ WHERE name = $1 AND workspace_id = $2", + name, + w_id + ) + .execute(&mut tx) + .await?; + audit_log( + &mut tx, + &authed.username, + "group.delete", + ActionKind::Delete, + &w_id, + Some(&name.to_string()), + None, + ) + .await?; + tx.commit().await?; + Ok(format!("delete group at name {}", name)) +} + +async fn update_group( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, + Json(eg): Json, +) -> Result { + let mut tx = user_db.begin(&authed).await?; + + crate::utils::not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?; + + sqlx::query_as!( + Group, + "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3", + eg.summary, + &name, + &w_id + ) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &authed.username, + "group.edit", + ActionKind::Update, + &w_id, + Some(&name.to_string()), + None, + ) + .await?; + tx.commit().await?; + Ok(format!("Edited group {}", name)) +} + +async fn add_user( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, + Json(Username { + username: user_username, + }): Json, +) -> Result { + let mut tx = user_db.begin(&authed).await?; + + crate::utils::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)", + &w_id, + user_username, + name, + ) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &authed.username, + "group.adduser", + ActionKind::Update, + &w_id, + Some(&name.to_string()), + Some([("user", user_username.as_str())].into()), + ) + .await?; + tx.commit().await?; + Ok(format!("Added {} to group {}", user_username, name)) +} + +async fn remove_user( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, + Json(Username { + username: user_username, + }): Json, +) -> Result { + let mut tx = user_db.begin(&authed).await?; + + crate::utils::not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?; + if &name == "all" { + return Err(Error::BadRequest(format!("Cannot delete users from all"))); + } + sqlx::query_as!( + Group, + "DELETE FROM usr_to_group WHERE usr = $1 AND group_ = $2 AND workspace_id = $3", + user_username, + name, + &w_id, + ) + .execute(&mut tx) + .await?; + + audit_log( + &mut tx, + &authed.username, + "group.removeuser", + ActionKind::Update, + &w_id, + Some(&name.to_string()), + Some([("user", user_username.as_str())].into()), + ) + .await?; + + tx.commit().await?; + Ok(format!("Removed {} to group {}", user_username, name)) +} diff --git a/backend/src/jobs.rs b/backend/src/jobs.rs new file mode 100644 index 0000000000..719c2b2040 --- /dev/null +++ b/backend/src/jobs.rs @@ -0,0 +1,1525 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use chrono::Duration; + +use sql_builder::prelude::*; +use sqlx::{query_scalar, Postgres, Transaction}; +use std::collections::HashMap; + +use crate::js_eval::eval_timeout; +use crate::users::create_token_for_owner; +use crate::{ + audit::{audit_log, ActionKind}, + db::{UserDB, DB}, + error, + error::Error, + flow::{FlowModuleValue, FlowValue, InputTransform}, + schedule::get_schedule_opt, + scripts::ScriptHash, + users::{owner_to_token_owner, Authed}, + utils::{require_admin, Pagination, StripPath}, +}; +use axum::{ + extract::{Extension, Path, Query}, + routing::{get, post}, + Json, Router, +}; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; +use sql_builder::SqlBuilder; + +use ulid::Ulid; +use uuid::Uuid; + +const MAX_NB_OF_JOBS_IN_Q_PER_USER: i64 = 10; +const MAX_DURATION_LAST_1200: i64 = 400; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/run/f/*script_path", post(run_flow_by_path)) + .route("/run/p/*script_path", post(run_job_by_path)) + .route("/run/h/:hash", post(run_job_by_hash)) + .route("/run/preview", post(run_preview_job)) + .route("/run/preview_flow", post(run_preview_flow_job)) + .route("/list", get(list_jobs)) + .route("/queue/list", get(list_queue_jobs)) + .route("/queue/cancel/:id", post(cancel_job)) + .route("/completed/list", get(list_completed_jobs)) + .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)) +} + +#[derive(Debug, sqlx::FromRow, Serialize, Clone)] +pub struct QueuedJob { + pub workspace_id: String, + pub id: Uuid, + pub parent_job: Option, + pub created_by: String, + pub created_at: chrono::DateTime, + pub started_at: Option>, + pub scheduled_for: chrono::DateTime, + pub running: bool, + pub script_hash: Option, + pub script_path: Option, + pub args: Option, + pub logs: Option, + pub raw_code: Option, + pub canceled: bool, + pub canceled_by: Option, + pub canceled_reason: Option, + pub last_ping: Option>, + pub job_kind: JobKind, + pub schedule_path: Option, + pub permissioned_as: String, + pub flow_status: Option, + pub raw_flow: Option, + pub is_flow_step: bool, +} + +#[derive(Debug, sqlx::FromRow, Serialize)] +struct CompletedJob { + workspace_id: String, + id: Uuid, + parent_job: Option, + created_by: String, + created_at: chrono::DateTime, + duration: i32, + success: bool, + script_hash: Option, + script_path: Option, + args: Option, + result: Option, + logs: Option, + deleted: bool, + raw_code: Option, + canceled: bool, + canceled_by: Option, + canceled_reason: Option, + job_kind: JobKind, + schedule_path: Option, + permissioned_as: String, + flow_status: Option, + raw_flow: Option, + is_flow_step: bool, +} + +#[derive(Deserialize, Clone, Copy)] +pub struct RunJobQuery { + scheduled_for: Option>, + scheduled_in_secs: Option, + parent_job: Option, +} + +impl RunJobQuery { + fn get_scheduled_for(self) -> Option> { + self.scheduled_for.or_else(|| { + self.scheduled_in_secs + .map(|s| chrono::Utc::now() + Duration::seconds(s)) + }) + } +} + +pub async fn run_flow_by_path( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, flow_path)): Path<(String, StripPath)>, + axum::Json(args): axum::Json>>, + Query(run_query): Query, +) -> error::Result<(StatusCode, String)> { + let flow_path = flow_path.to_path(); + let tx = user_db.begin(&authed).await?; + let (uuid, tx) = push( + tx, + &w_id, + JobPayload::Flow(flow_path.to_string()), + args, + &authed.username, + owner_to_token_owner(&authed.username, false), + run_query.get_scheduled_for(), + None, + run_query.parent_job, + false, + ) + .await?; + tx.commit().await?; + Ok((StatusCode::CREATED, uuid.to_string())) +} + +pub async fn run_job_by_path( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, script_path)): Path<(String, StripPath)>, + axum::Json(args): axum::Json>>, + Query(run_query): Query, +) -> error::Result<(StatusCode, String)> { + let script_path = script_path.to_path(); + let mut tx = user_db.begin(&authed).await?; + let script_hash = get_latest_hash_for_path(&mut tx, &w_id, script_path).await?; + let (uuid, tx) = push( + tx, + &w_id, + JobPayload::ScriptHash { + hash: script_hash, + path: script_path.to_owned(), + }, + args, + &authed.username, + owner_to_token_owner(&authed.username, false), + run_query.get_scheduled_for(), + None, + run_query.parent_job, + false, + ) + .await?; + tx.commit().await?; + Ok((StatusCode::CREATED, uuid.to_string())) +} + +pub async fn get_latest_hash_for_path<'c>( + db: &mut Transaction<'c, Postgres>, + w_id: &str, + script_path: &str, +) -> error::Result { + let script_hash_o = sqlx::query_scalar!( + "select hash from script where path = $1 AND (workspace_id = $2 OR workspace_id = 'starter') AND + created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')) AND + deleted = false", + script_path, + w_id + ) + .fetch_optional(db) + .await?; + + let script_hash = crate::utils::not_found_if_none(script_hash_o, "ScriptHash", script_path)?; + + Ok(ScriptHash(script_hash)) +} + +pub async fn run_job_by_hash( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, script_hash)): Path<(String, ScriptHash)>, + axum::Json(args): axum::Json>>, + Query(run_query): Query, +) -> 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 (uuid, tx) = push( + tx, + &w_id, + JobPayload::ScriptHash { + hash: ScriptHash(hash), + path, + }, + args, + &authed.username, + owner_to_token_owner(&authed.username, false), + run_query.get_scheduled_for(), + None, + run_query.parent_job, + false, + ) + .await?; + tx.commit().await?; + Ok((StatusCode::CREATED, uuid.to_string())) +} + +pub async fn get_path_for_hash<'c>( + db: &mut Transaction<'c, Postgres>, + w_id: &str, + hash: i64, +) -> error::Result { + let path = sqlx::query_scalar!( + "select path from script where hash = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + hash, + w_id + ) + .fetch_one(db) + .await?; + Ok(path) +} + +async fn run_preview_job( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(preview): Json, + Query(sch_query): Query, +) -> error::Result<(StatusCode, String)> { + let tx = user_db.begin(&authed).await?; + let (uuid, tx) = push( + tx, + &w_id, + JobPayload::Code(RawCode { + content: preview.content, + path: preview.path, + }), + preview.args, + &authed.username, + owner_to_token_owner(&authed.username, false), + sch_query.get_scheduled_for(), + None, + None, + false, + ) + .await?; + tx.commit().await?; + Ok((StatusCode::CREATED, uuid.to_string())) +} + +async fn run_preview_flow_job( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(raw_flow): Json, + Query(sch_query): Query, +) -> error::Result<(StatusCode, String)> { + let tx = user_db.begin(&authed).await?; + let (uuid, tx) = push( + tx, + &w_id, + JobPayload::RawFlow { + value: raw_flow.value, + path: raw_flow.path, + }, + raw_flow.args, + &authed.username, + owner_to_token_owner(&authed.username, false), + sch_query.get_scheduled_for(), + None, + None, + false, + ) + .await?; + tx.commit().await?; + Ok((StatusCode::CREATED, uuid.to_string())) +} +#[derive(Deserialize)] +pub struct ListQueueQuery { + pub script_path_start: Option, + pub script_path_exact: Option, + pub script_hash: Option, + pub created_by: Option, + pub created_before: Option>, + pub created_after: Option>, + pub running: Option, + pub parent_job: Option, + pub order_desc: Option, + pub job_kinds: Option, +} + +fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder { + let mut sqlb = SqlBuilder::select_from("queue") + .fields(fields) + .order_by("created_at", lq.order_desc.unwrap_or(true)) + .limit(1000) + .and_where_eq("workspace_id", "?".bind(&w_id)) + .clone(); + + if let Some(ps) = &lq.script_path_start { + sqlb.and_where_like_left("script_path", "?".bind(ps)); + } + if let Some(p) = &lq.script_path_exact { + sqlb.and_where_eq("script_path", "?".bind(p)); + } + if let Some(h) = &lq.script_hash { + sqlb.and_where_eq("script_hash", "?".bind(h)); + } + if let Some(cb) = &lq.created_by { + sqlb.and_where_eq("created_by", "?".bind(cb)); + } + if let Some(r) = &lq.running { + sqlb.and_where_eq("running", &r); + } + if let Some(pj) = &lq.parent_job { + sqlb.and_where_eq("parent_job", "?".bind(pj)); + } + if let Some(dt) = &lq.created_before { + sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp())); + } + if let Some(dt) = &lq.created_after { + sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp())); + } + if let Some(jk) = &lq.job_kinds { + sqlb.and_where_in( + "job_kind", + &jk.split(',').into_iter().map(quote).collect::>(), + ); + } + + sqlb +} + +async fn list_queue_jobs( + Extension(db): Extension, + Path(w_id): Path, + Query(lq): Query, +) -> error::JsonResult> { + let sql = list_queue_jobs_query(&w_id, &lq, &["*"]).sql()?; + let jobs = sqlx::query_as::<_, QueuedJob>(&sql).fetch_all(&db).await?; + Ok(Json(jobs)) +} + +async fn list_jobs( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(pagination): Query, + Query(lq): Query, +) -> error::JsonResult> { + let (per_page, offset) = crate::utils::paginate(pagination); + let lqc = lq.clone(); + let sqlq = list_queue_jobs_query( + &w_id, + &ListQueueQuery { + script_path_start: lq.script_path_start, + script_path_exact: lq.script_path_exact, + script_hash: lq.script_hash, + created_by: lq.created_by, + created_before: lq.created_before, + created_after: lq.created_after, + running: None, + parent_job: lq.parent_job, + order_desc: Some(true), + job_kinds: lq.job_kinds, + }, + &[ + "'QueuedJob' as typ", + "id", + "workspace_id", + "parent_job", + "created_by", + "created_at", + "started_at", + "scheduled_for", + "running", + "script_hash", + "script_path", + "args", + "null as duration", + "null as success", + "false as deleted", + "canceled", + "canceled_by", + "job_kind", + "schedule_path", + "permissioned_as", + "flow_status", + "is_flow_step", + ], + ); + let sqlc = list_completed_jobs_query( + &w_id, + per_page + offset, + 0, + &ListCompletedQuery { + order_desc: Some(true), + ..lqc + }, + &[ + "'CompletedJob' as typ", + "id", + "workspace_id", + "parent_job", + "created_by", + "created_at", + "null as started_at", + "null as scheduled_for", + "null as running", + "script_hash", + "script_path", + "args", + "duration", + "success", + "deleted", + "canceled", + "canceled_by", + "job_kind", + "schedule_path", + "permissioned_as", + "flow_status", + "is_flow_step", + ], + ); + let sql = format!( + "{} UNION ALL {} ORDER BY created_at DESC LIMIT {} OFFSET {};", + &sqlq.subquery()?, + &sqlc.subquery()?, + per_page, + offset + ); + let mut tx = user_db.begin(&authed).await?; + let jobs: Vec = sqlx::query_as(&sql).fetch_all(&mut tx).await?; + tx.commit().await?; + Ok(Json(jobs.into_iter().map(From::from).collect())) +} +#[derive(Deserialize, Clone)] +pub struct ListCompletedQuery { + pub script_path_start: Option, + pub script_path_exact: Option, + pub script_hash: Option, + pub created_by: Option, + pub created_before: Option>, + pub created_after: Option>, + pub success: Option, + pub parent_job: Option, + pub order_desc: Option, + pub job_kinds: Option, +} +fn list_completed_jobs_query( + w_id: &str, + per_page: usize, + offset: usize, + lq: &ListCompletedQuery, + fields: &[&str], +) -> SqlBuilder { + let mut sqlb = SqlBuilder::select_from("completed_job") + .fields(fields) + .order_by("created_at", lq.order_desc.unwrap_or(true)) + .and_where_eq("workspace_id", "?".bind(&w_id)) + .offset(offset) + .limit(per_page) + .clone(); + + if let Some(ps) = &lq.script_path_start { + sqlb.and_where_like_left("script_path", "?".bind(ps)); + } + if let Some(p) = &lq.script_path_exact { + sqlb.and_where_eq("script_path", "?".bind(p)); + } + if let Some(h) = &lq.script_hash { + sqlb.and_where_eq("script_hash", "?".bind(h)); + } + if let Some(cb) = &lq.created_by { + sqlb.and_where_eq("created_by", "?".bind(cb)); + } + if let Some(r) = &lq.success { + sqlb.and_where_eq("success", r); + } + if let Some(pj) = &lq.parent_job { + sqlb.and_where_eq("parent_job", "?".bind(pj)); + } + if let Some(dt) = &lq.created_before { + sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp())); + } + if let Some(dt) = &lq.created_after { + sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp())); + } + if let Some(jk) = &lq.job_kinds { + sqlb.and_where_in( + "job_kind", + &jk.split(',').into_iter().map(quote).collect::>(), + ); + } + + sqlb +} + +async fn list_completed_jobs( + Extension(db): Extension, + Path(w_id): Path, + Query(pagination): Query, + Query(lq): Query, +) -> error::JsonResult> { + let (per_page, offset) = crate::utils::paginate(pagination); + + let sql = list_completed_jobs_query( + &w_id, + per_page, + offset, + &lq, + &[ + "id", + "workspace_id", + "parent_job", + "created_by", + "created_at", + "duration", + "success", + "script_hash", + "script_path", + "args", + "result", + "null as logs", + "deleted", + "canceled", + "canceled_by", + "canceled_reason", + "job_kind", + "schedule_path", + "permissioned_as", + "null as raw_code", + "null as flow_status", + "null as raw_flow", + "is_flow_step", + ], + ) + .sql()?; + let jobs = sqlx::query_as::<_, CompletedJob>(&sql) + .fetch_all(&db) + .await?; + Ok(Json(jobs)) +} + +async fn get_completed_job( + Extension(db): Extension, + Path((w_id, id)): Path<(String, Uuid)>, +) -> error::JsonResult { + let job_o = sqlx::query_as::<_, CompletedJob>( + "SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2", + ) + .bind(id) + .bind(w_id) + .fetch_optional(&db) + .await?; + + let job = crate::utils::not_found_if_none(job_o, "Completed Job", id.to_string())?; + Ok(Json(job)) +} + +async fn get_completed_job_result( + Extension(db): Extension, + Path((w_id, id)): Path<(String, Uuid)>, +) -> error::JsonResult> { + let result_o = sqlx::query_scalar!( + "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2", + id, + w_id, + ) + .fetch_optional(&db) + .await?; + + let result = crate::utils::not_found_if_none(result_o, "Completed Job", id.to_string())?; + Ok(Json(result)) +} + +async fn cancel_job( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, id)): Path<(String, Uuid)>, + Json(CancelJob { reason }): Json, +) -> error::Result { + let mut tx = user_db.begin(&authed).await?; + + let job_option = sqlx::query_scalar!( + "UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 \ + WHERE id = $3 AND schedule_path IS NULL AND workspace_id = $4\ + RETURNING id", + &authed.username, + reason, + id, + w_id + ) + .fetch_optional(&mut tx) + .await?; + + if let Some(id) = job_option { + audit_log( + &mut tx, + &authed.username, + "jobs.delete", + ActionKind::Delete, + &w_id, + Some(&id.to_string()), + None, + ) + .await?; + Ok(id.to_string()) + } else { + let (job_o, tx) = get_job_from_id(tx, &w_id, id).await?; + tx.commit().await?; + let err = match job_o { + Some(Job::CompletedJob(_)) => error::Error::BadRequest(format!( + "queued job id {} exists but is already completed and cannot be canceled", + id + )), + Some(Job::QueuedJob(job)) if job.schedule_path.is_some() => { + error::Error::BadRequest(format!( + "queued job id {} exists but has been created by a scheduler + and can only be only canceled by disabling the parent scheduler", + id + )) + } + _ => error::Error::NotFound(format!("queued job id {} does not exist", id)), + }; + Err(err) + } +} + +async fn delete_completed_job( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, id)): Path<(String, Uuid)>, +) -> error::JsonResult { + let mut tx = user_db.begin(&authed).await?; + + 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 RETURNING *", + ) + .bind(id) + .bind(&w_id) + .fetch_optional(&mut tx) + .await?; + + let job = crate::utils::not_found_if_none(job_o, "Completed Job", id.to_string())?; + + audit_log( + &mut tx, + &authed.username, + "jobs.delete", + ActionKind::Delete, + &w_id, + Some(&id.to_string()), + None, + ) + .await?; + + tx.commit().await?; + Ok(Json(job)) +} + +#[derive(Deserialize)] +pub struct JobUpdateQuery { + pub running: bool, + pub log_offset: i32, +} + +#[derive(Serialize)] +pub struct JobUpdate { + pub running: Option, + pub completed: Option, + pub new_logs: Option, +} + +async fn get_job_update( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, id)): Path<(String, Uuid)>, + Query(JobUpdateQuery { + running, + log_offset, + }): Query, +) -> error::JsonResult { + let mut tx = user_db.begin(&authed).await?; + + let logs = query_scalar!( + "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3", + log_offset, + &w_id, + &id + ) + .fetch_optional(&mut tx) + .await?; + + if let Some(logs) = logs { + tx.commit().await?; + Ok(Json(JobUpdate { + running: if !running { Some(true) } else { None }, + completed: None, + new_logs: logs, + })) + } else { + let logs = query_scalar!( + "SELECT substr(logs, $1) as logs FROM completed_job WHERE workspace_id = $2 AND id = $3", + log_offset, + &w_id, + &id + ) + .fetch_optional(&mut tx) + .await?; + let logs = crate::utils::not_found_if_none(logs, "Job", id.to_string())?; + tx.commit().await?; + Ok(Json(JobUpdate { + running: Some(false), + completed: Some(true), + new_logs: logs, + })) + } +} + +async fn get_job( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, id)): Path<(String, Uuid)>, +) -> error::JsonResult { + let tx = user_db.begin(&authed).await?; + let (job_o, tx) = get_job_from_id(tx, &w_id, id).await?; + let job = crate::utils::not_found_if_none(job_o, "Completed Job", id.to_string())?; + tx.commit().await?; + Ok(Json(job)) +} + +async fn get_job_from_id<'c>( + mut tx: Transaction<'c, Postgres>, + w_id: &str, + id: Uuid, +) -> error::Result<(Option, Transaction<'c, Postgres>)> { + let cjob_option = sqlx::query_as::<_, CompletedJob>( + "SELECT * FROM completed_job WHERE id = $1 AND workspace_id = $2", + ) + .bind(id) + .bind(w_id) + .fetch_optional(&mut tx) + .await?; + let job_option = match cjob_option { + Some(job) => Some(Job::CompletedJob(job)), + None => get_queued_job(id, w_id, &mut tx).await?.map(Job::QueuedJob), + }; + Ok((job_option, tx)) +} + +async fn get_queued_job<'c>( + id: Uuid, + w_id: &str, + tx: &mut Transaction<'c, Postgres>, +) -> error::Result> { + let r = sqlx::query_as::<_, QueuedJob>( + "SELECT * + FROM queue WHERE id = $1 AND workspace_id = $2", + ) + .bind(id) + .bind(w_id) + .fetch_optional(tx) + .await?; + Ok(r) +} + +#[derive(Serialize)] +#[serde(tag = "type")] +enum Job { + QueuedJob(QueuedJob), + CompletedJob(CompletedJob), +} + +#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)] +#[sqlx(type_name = "JOB_KIND", rename_all = "lowercase")] +#[serde(rename_all(serialize = "lowercase"))] +pub enum JobKind { + Script, + Preview, + Dependencies, + Flow, + FlowPreview, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FlowStatus { + pub step: i32, + pub modules: Vec, + pub failure_module: FlowStatusModule, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +pub enum FlowStatusModule { + WaitingForPriorSteps, + WaitingForEvent { event: String }, + WaitingForExecutor { job: Uuid }, + InProgress { job: Uuid }, + Success { job: Uuid }, + Failure { job: Uuid }, +} + +#[derive(sqlx::FromRow)] +struct UnifiedJob { + workspace_id: String, + typ: String, + id: Uuid, + parent_job: Option, + created_by: String, + created_at: chrono::DateTime, + started_at: Option>, + scheduled_for: Option>, + running: Option, + script_hash: Option, + script_path: Option, + args: Option, + duration: Option, + success: Option, + deleted: bool, + canceled: bool, + canceled_by: Option, + job_kind: JobKind, + schedule_path: Option, + permissioned_as: String, + flow_status: Option, + is_flow_step: bool, +} + +impl From for Job { + fn from(uj: UnifiedJob) -> Self { + match uj.typ.as_ref() { + "CompletedJob" => Job::CompletedJob(CompletedJob { + workspace_id: uj.workspace_id, + id: uj.id, + parent_job: uj.parent_job, + created_by: uj.created_by, + created_at: uj.created_at, + duration: uj.duration.unwrap(), + success: uj.success.unwrap(), + script_hash: uj.script_hash, + script_path: uj.script_path, + args: uj.args, + result: None, + logs: None, + deleted: uj.deleted, + canceled: uj.canceled, + canceled_by: uj.canceled_by, + raw_code: None, + canceled_reason: None, + 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, + }), + "QueuedJob" => Job::QueuedJob(QueuedJob { + workspace_id: uj.workspace_id, + id: uj.id, + parent_job: uj.parent_job, + created_by: uj.created_by, + created_at: uj.created_at, + started_at: uj.started_at, + script_hash: uj.script_hash, + script_path: uj.script_path, + args: uj.args, + running: uj.running.unwrap(), + scheduled_for: uj.scheduled_for.unwrap(), + logs: None, + raw_code: None, + canceled: uj.canceled, + canceled_by: uj.canceled_by, + canceled_reason: None, + last_ping: None, + 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, + }), + t => panic!("job type {} not valid", t), + } + } +} +#[derive(Deserialize)] +struct CancelJob { + reason: Option, +} + +pub struct RawCode { + content: String, + path: Option, +} + +#[derive(Deserialize)] +struct Preview { + content: String, + path: Option, + args: Option>, +} + +#[derive(Deserialize)] +struct PreviewFlow { + value: FlowValue, + path: Option, + args: Option>, +} + +pub enum JobPayload { + ScriptHash { + hash: ScriptHash, + path: String, + }, + Code(RawCode), + Dependencies { + hash: ScriptHash, + dependencies: Vec, + }, + Flow(String), + RawFlow { + value: FlowValue, + path: Option, + }, +} + +pub async fn push<'c>( + mut tx: Transaction<'c, Postgres>, + workspace_id: &str, + job_payload: JobPayload, + args: Option>, + user: &str, + permissioned_as: String, + scheduled_for_o: Option>, + schedule_path: Option, + parent_job: Option, + is_flow_step: bool, +) -> Result<(Uuid, Transaction<'c, Postgres>), Error> { + let scheduled_for = scheduled_for_o.unwrap_or_else(chrono::Utc::now); + let args_json = args.map(serde_json::Value::Object); + let job_id: Uuid = Ulid::new().into(); + + let rate_limiting_queue = sqlx::query_scalar!( + "SELECT COUNT(id) FROM queue WHERE created_by = $1 AND workspace_id = $2", + user, + workspace_id + ) + .fetch_one(&mut tx) + .await?; + + if let Some(nb_jobs) = rate_limiting_queue { + if nb_jobs > MAX_NB_OF_JOBS_IN_Q_PER_USER { + return Err(error::Error::ExecutionErr(format!( + "You have exceeded the number of authorized elements of queue at any given time: {}", MAX_NB_OF_JOBS_IN_Q_PER_USER))); + } + } + + let rate_limiting_duration = sqlx::query_scalar!( + "SELECT SUM(duration) FROM completed_job WHERE created_by = $1 AND created_at > NOW() - INTERVAL '1200 seconds' AND workspace_id = $2", + user, + workspace_id + ) + .fetch_one(&mut tx) + .await?; + + if let Some(sum_duration) = rate_limiting_duration { + if sum_duration > MAX_DURATION_LAST_1200 { + return Err(error::Error::ExecutionErr(format!( + "You have exceeded the scripts cumulative duration limit over the last 20m which is: {}", MAX_DURATION_LAST_1200))); + } + } + + let (script_hash, script_path, raw_code, job_kind, raw_flow) = match job_payload { + JobPayload::ScriptHash { hash, path } => { + (Some(hash.0), Some(path), None, JobKind::Script, None) + } + JobPayload::Code(RawCode { content, path }) => { + (None, path, Some(content), JobKind::Preview, None) + } + JobPayload::Dependencies { hash, dependencies } => ( + Some(hash.0), + None, + Some(dependencies.join("\n")), + JobKind::Dependencies, + None, + ), + JobPayload::RawFlow { value, path } => { + (None, path, None, JobKind::FlowPreview, Some(value)) + } + JobPayload::Flow(flow) => { + let value_json = sqlx::query_scalar!("SELECT value FROM flow WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + flow, workspace_id) + .fetch_optional(&mut tx) + .await? + .ok_or_else(|| Error::InternalErr(format!("not found flow at path {:?}", flow)))?; + let value = serde_json::from_value::(value_json).map_err(|err| { + Error::InternalErr(format!( + "could not convert json to flow for {flow}: {err:?}" + )) + })?; + (None, Some(flow), None, JobKind::Flow, Some(value)) + } + }; + + let flow_status = raw_flow.as_ref().map(|f| FlowStatus { + step: 0, + modules: (0..f.modules.len()) + .map(|_| FlowStatusModule::WaitingForPriorSteps) + .collect(), + failure_module: FlowStatusModule::WaitingForPriorSteps, + }); + let uuid = sqlx::query_scalar!( + "INSERT INTO queue + (workspace_id, id, parent_job, created_by, permissioned_as, scheduled_for, + script_hash, script_path, raw_code, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id", + workspace_id, + job_id, + parent_job, + user, + permissioned_as, + scheduled_for, + script_hash, + script_path.clone(), + raw_code, + args_json, + job_kind: JobKind, + schedule_path, + raw_flow.map(|f| serde_json::json!(f)), + flow_status.map(|f| serde_json::json!(f)), + is_flow_step + ) + .fetch_one(&mut tx) + .await?; + let uuid_string = job_id.to_string(); + let uuid_str = uuid_string.as_str(); + let mut hm = HashMap::from([("uuid", uuid_str), ("permissioned_as", &permissioned_as)]); + { + let s: String; + let audit_o = match job_kind { + JobKind::Preview => { + s = format!("preview {:?}", script_path); + Some(("jobs.run.preview", Some(s))) + } + JobKind::Script => { + s = ScriptHash(script_hash.unwrap()).to_string(); + hm.insert("hash", s.as_str()); + Some(("jobs.run.script", script_path)) + } + JobKind::Flow => Some(("jobs.run.flow", script_path)), + JobKind::FlowPreview => Some(("jobs.run.flow_preview", script_path)), + _ => None, + }; + + if let Some((operation_name, resource)) = audit_o { + audit_log( + &mut tx, + &user, + operation_name, + ActionKind::Execute, + workspace_id, + resource.as_ref().map(|x| x.as_str()), + Some(hm), + ) + .await?; + } + } + Ok((uuid, tx)) +} + +pub async fn add_completed_job_error( + db: &DB, + queued_job: &QueuedJob, + logs: String, + e: E, +) -> Result<(Uuid, Map), Error> { + let mut output_map = serde_json::Map::new(); + output_map.insert( + "error".to_string(), + serde_json::Value::String(e.to_string()), + ); + let a = add_completed_job( + db, + &queued_job, + false, + Some(output_map.clone()), + format!("{}\n{}", logs, e.to_string()), + ) + .await?; + Ok((a, output_map)) +} + +pub async fn add_completed_job( + db: &DB, + queued_job: &QueuedJob, + success: bool, + result: Option>, + logs: String, +) -> Result { + let result_json = result.map(serde_json::Value::Object); + let duration = (chrono::Utc::now() - queued_job.started_at.unwrap_or(queued_job.created_at)) + .num_seconds() as i32; + let _ = sqlx::query!( + "INSERT INTO completed_job as cj + (workspace_id, id, parent_job, created_by, created_at, duration, success, script_hash, script_path, \ + args, result, logs, + raw_code, canceled, canceled_by, canceled_reason, job_kind, schedule_path, permissioned_as, flow_status, raw_flow, \ + is_flow_step) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22) \ + ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12) \ + RETURNING id", + queued_job.workspace_id, + queued_job.id, + queued_job.parent_job, + queued_job.created_by, + queued_job.created_at, + duration, + success, + queued_job.script_hash.map(|x| x.0), + queued_job.script_path, + queued_job.args, + result_json, + logs, + queued_job.raw_code, + queued_job.canceled, + queued_job.canceled_by, + queued_job.canceled_reason, + queued_job.job_kind: JobKind, + queued_job.schedule_path, + queued_job.permissioned_as, + queued_job.flow_status, + queued_job.raw_flow, + queued_job.is_flow_step + ) + .fetch_one(db) + .await?; + tracing::debug!("Added completed job {}", queued_job.id); + Ok(queued_job.id) +} + +pub async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result { + let r = sqlx::query_scalar!( + "SELECT (flow_status->'step')::integer FROM queue WHERE id = $1", + id + ) + .fetch_one(db) + .await? + .ok_or_else(|| Error::InternalErr(format!("not found step")))?; + Ok(r) +} +pub async fn update_flow_status_in_progress( + db: &DB, + w_id: &str, + flow: Uuid, + job_in_progress: Uuid, +) -> error::Result<()> { + let step = get_step_of_flow_status(db, flow).await?; + sqlx::query(&format!( + "UPDATE queue + SET flow_status = jsonb_set(flow_status, '{{modules, {}}}', $1) + WHERE id = $2 AND workspace_id = $3", + step + )) + .bind(serde_json::json!(FlowStatusModule::InProgress { + job: job_in_progress + })) + .bind(flow) + .bind(w_id) + .execute(db) + .await?; + Ok(()) +} + +pub async fn update_flow_status_after_job_completion( + db: &DB, + job: &QueuedJob, + success: bool, + result: Option>, +) -> error::Result<()> { + tracing::info!("HANDLE FLOW: {job:?} {success} {result:?}"); + + let mut tx = db.begin().await?; + + let w_id = &job.workspace_id; + + let flow = job + .parent_job + .ok_or_else(|| Error::InternalErr(format!("expected parent job")))?; + + let old_status_json = sqlx::query_scalar!( + "SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2", + flow, + w_id + ) + .fetch_one(&mut tx) + .await? + .ok_or_else(|| Error::InternalErr(format!("requiring a previous status")))?; + + let old_status = serde_json::from_value::(old_status_json) + .ok() + .ok_or_else(|| { + Error::InternalErr(format!("requiring status to be parsabled as FlowStatus")) + })?; + + let last_step = (old_status.step + 1) as usize == old_status.modules.len(); + let new_status = if success { + FlowStatusModule::Success { job: job.id } + } else { + FlowStatusModule::Failure { job: job.id } + }; + + sqlx::query(&format!( + "UPDATE queue + SET + flow_status = jsonb_set(jsonb_set(flow_status, '{{modules, {}}}', $1), '{{\"step\"}}', $2) + WHERE id = $3", + old_status.step, + )) + .bind(serde_json::json!(new_status)) + .bind(serde_json::json!(old_status.step + 1)) + .bind(flow) + .execute(&mut tx) + .await?; + + tracing::info!("UPDATE: {:?}", new_status); + + let flow_job = get_queued_job(flow, w_id, &mut tx) + .await? + .ok_or_else(|| Error::InternalErr(format!("requiring flow to be in the queue")))?; + tx.commit().await?; + + let done = if !success || last_step { + add_completed_job( + db, + &flow_job, + success, + result, + "Flow job completed".to_string(), + ) + .await?; + true + } else { + if let Err(err) = handle_flow(&flow_job, db, result).await { + let _ = add_completed_job_error( + db, + &flow_job, + "Unexpected error during flow chaining:\n".to_string(), + err, + ) + .await; + true + } else { + false + } + }; + + if done { + postprocess_queued_job(flow_job.schedule_path, &w_id, flow, db).await?; + } + + Ok(()) +} + +pub async fn postprocess_queued_job( + schedule_path: Option, + w_id: &str, + job_id: Uuid, + db: &DB, +) -> crate::error::Result<()> { + let _ = delete_job(db, w_id, job_id).await?; + schedule_again_if_scheduled(schedule_path, &w_id, db).await?; + Ok(()) +} + +pub async fn schedule_again_if_scheduled( + schedule_path: Option, + w_id: &str, + db: &DB, +) -> crate::error::Result<()> { + if let Some(schedule_path) = schedule_path { + let mut tx = db.begin().await?; + let schedule = get_schedule_opt(&mut tx, &w_id, &schedule_path) + .await? + .unwrap(); + if schedule.enabled { + tx = crate::schedule::push_scheduled_job(tx, schedule).await?; + } + tx.commit().await?; + } + Ok(()) +} + +pub async fn handle_flow( + job: &QueuedJob, + db: &sqlx::Pool, + last_result: Option>, +) -> anyhow::Result<()> { + let value = job + .raw_flow + .as_ref() + .ok_or_else(|| Error::InternalErr(format!("requiring a raw flow value")))? + .to_owned(); + let flow = serde_json::from_value::(value.to_owned())?; + push_next_flow_job(job, flow, db, last_result).await?; + Ok(()) +} + +async fn transform_input( + flow_args: &Option, + last_result: Option>, + input_transform: &HashMap, + workspace: &str, + token: &str, + steps: Vec, +) -> anyhow::Result>> { + let mut mapped = serde_json::Map::new(); + + for (key, val) in input_transform.into_iter() { + match val { + InputTransform::Static { value } => { + mapped.insert(key.to_string(), value.to_owned()); + () + } + _ => (), + }; + } + + for (key, val) in input_transform.into_iter() { + match val { + InputTransform::Static { value: _ } => (), + InputTransform::Javascript { expr } => { + let previous_result = + serde_json::Value::Object(last_result.clone().unwrap_or_else(|| Map::new())); + let flow_input = flow_args.clone().unwrap_or_else(|| json!({})); + let v = eval_timeout( + expr.to_string(), + vec![ + ("params".to_string(), serde_json::json!(mapped)), + ("previous_result".to_string(), previous_result), + ("flow_input".to_string(), flow_input), + ], + workspace, + token, + steps.clone(), + ) + .await + .map_err(|e| { + Error::ExecutionErr(format!( + "Error during isolated evaluation of expression `{expr}`:\n{e}" + )) + })?; + mapped.insert(key.to_string(), v); + () + } + _ => Err(error::Error::BadRequest(format!( + "impossible to handle unknown input transform" + )))?, + } + } + + Ok(Some(mapped)) +} + +async fn push_next_flow_job( + job: &QueuedJob, + flow: FlowValue, + db: &sqlx::Pool, + last_result: Option>, +) -> anyhow::Result<()> { + let flow_status_json = job + .flow_status + .as_ref() + .ok_or_else(|| Error::InternalErr(format!("not found status for flow job {:?}", job.id)))?; + let status = serde_json::from_value::(flow_status_json.to_owned())?; + let i = status.step as usize; + + if flow.modules.len() > i { + let module = &flow.modules[i]; + let mut tx = db.begin().await?; + let job_payload = match &module.value { + FlowModuleValue::Script { path: script_path } => { + let script_hash = + get_latest_hash_for_path(&mut tx, &job.workspace_id, script_path).await?; + JobPayload::ScriptHash { + hash: script_hash, + path: script_path.to_owned(), + } + } + a @ _ => { + tracing::info!("Unrecognized module values {:?}", a); + Err(Error::BadRequest(format!( + "Unrecognized module values {:?}", + a + )))? + } + }; + + let token = create_token_for_owner( + &db, + &job.workspace_id, + &job.permissioned_as, + crate::users::NewToken { + label: Some("transform-input".to_string()), + expiration: Some(chrono::Utc::now() + chrono::Duration::seconds(10)), + }, + &job.created_by, + ) + .await?; + + let args = transform_input( + &job.args, + last_result, + &module.input_transform, + &job.workspace_id, + &token, + status + .modules + .into_iter() + .map(|x| match x { + FlowStatusModule::Success { job } => job.to_string(), + _ => "invalid step status".to_string(), + }) + .collect(), + ) + .await?; //job.args + let (uuid, mut tx) = push( + tx, + &job.workspace_id, + job_payload, + args, + &job.created_by, + job.permissioned_as.to_owned(), + None, + None, + Some(job.id), + true, + ) + .await?; + + sqlx::query(&format!( + "UPDATE queue + SET + flow_status = jsonb_set(flow_status, '{{modules, {}}}', $1) + WHERE id = $2", + i + )) + .bind(serde_json::json!(FlowStatusModule::WaitingForExecutor { + job: uuid + })) + .bind(job.parent_job) + .execute(&mut tx) + .await?; + tx.commit().await?; + } + Ok(()) +} + +pub async fn pull(db: &DB) -> Result, crate::Error> { + let now = chrono::Utc::now(); + + let job: Option = sqlx::query_as::<_, QueuedJob>( + "UPDATE queue + SET running = true, started_at = $1 + WHERE id IN ( + SELECT id + FROM queue + WHERE running = false AND scheduled_for <= $2 + ORDER BY scheduled_for + FOR UPDATE SKIP LOCKED + LIMIT 1 + ) + RETURNING *", + ) + .bind(now) + .bind(now) + .fetch_optional(db) + .await?; + + Ok(job) +} + +pub async fn delete_job(db: &DB, w_id: &str, job_id: Uuid) -> Result<(), crate::Error> { + let job_removed = sqlx::query_scalar!( + "DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1", + w_id, + job_id + ) + .fetch_one(db) + .await? + .unwrap_or(0) + == 1; + tracing::debug!("Job {job_id} deletion was achieved with success: {job_removed}"); + Ok(()) +} diff --git a/backend/src/js_eval.rs b/backend/src/js_eval.rs new file mode 100644 index 0000000000..90e49c346b --- /dev/null +++ b/backend/src/js_eval.rs @@ -0,0 +1,300 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use std::cell::RefCell; +use std::rc::Rc; + +use deno_core::serde_v8; +use deno_core::v8; +use deno_core::v8::IsolateHandle; +use deno_core::JsRuntime; +use deno_core::OpState; +use deno_core::RuntimeOptions; +use deno_core::Snapshot; +use deno_core::ZeroCopyBuf; +use itertools::Itertools; +use regex::Regex; +use serde_json::Value; +use tokio::sync::oneshot; +use tokio::time::timeout; + +use crate::client; +use crate::error::Error; + +pub async fn eval_timeout( + expr: String, + env: Vec<(String, serde_json::Value)>, + workspace: &str, + token: &str, + steps: Vec, +) -> anyhow::Result { + let expr2 = expr.clone(); + let (sender, mut receiver) = oneshot::channel::(); + let (workspace, token) = (workspace.to_string().clone(), token.to_string().clone()); + timeout( + std::time::Duration::from_millis(2000), + tokio::task::spawn_blocking(move || { + let buffer = include_bytes!("../v8.snap"); + + // Use our snapshot to provision our new runtime + let options = RuntimeOptions { + startup_snapshot: Some(Snapshot::Static(buffer)), + ..Default::default() + }; + let mut js_runtime = JsRuntime::new(options); + js_runtime.register_op("variable", deno_core::op_async(op_variable)); + js_runtime.register_op("resource", deno_core::op_async(op_resource)); + if !steps.is_empty() { + js_runtime.register_op("result", deno_core::op_async(op_get_result)); + } + js_runtime.sync_ops_cache(); + + sender + .send(js_runtime.v8_isolate().thread_safe_handle()) + .map_err(|_| Error::ExecutionErr("impossible to send v8 isolate".to_string()))?; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let re = Regex::new(r"import (.*)\n").unwrap(); + let expr = re.replace_all(&expr, "").to_string(); + // pretty frail but this it to make the expr more user friendly and not require the user to write await + let expr = ["variable", "step"] + .into_iter() + .fold(expr, replace_with_await); + + let r = + runtime.block_on(eval(&mut js_runtime, &expr, env, &workspace, &token, steps))?; + + Ok(r) as anyhow::Result + }), + ) + .await + .map_err(|_| { + if let Ok(isolate) = receiver.try_recv() { + isolate.terminate_execution(); + }; + Error::ExecutionErr(format!( + "The expression of evaluation `{expr2}` took too long to execute (>2000ms)" + )) + })?? +} + +fn replace_with_await(expr: String, fn_name: &str) -> String { + let sep = format!("{}(", fn_name); + let mut split = expr.split(&sep); + let mut s = split.next().unwrap_or_else(|| "").to_string(); + for x in split { + s.push_str(&format!("(await {}({}", fn_name, add_closing_bracket(x))) + } + s +} + +fn add_closing_bracket(s: &str) -> String { + let mut s = s.to_string(); + let mut level = 1; + let mut idx = 0; + for c in s.chars() { + match c { + '(' => level += 1, + ')' => level -= 1, + _ => (), + }; + if level == 0 { + break; + } + idx += 1; + } + s.insert_str(idx, ")"); + s +} + +const SPLIT_PAT: &str = ";\n"; +async fn eval( + context: &mut JsRuntime, + expr: &str, + env: Vec<(String, serde_json::Value)>, + workspace: &str, + token: &str, + steps: Vec, +) -> anyhow::Result { + let expr = expr.trim(); + let expr = format!( + "{}\nreturn {};", + expr.split(SPLIT_PAT) + .take(expr.split(SPLIT_PAT).count() - 1) + .join("\n"), + expr.split(SPLIT_PAT).last().unwrap_or_else(|| "") + ); + let steps_code = if !steps.is_empty() { + format!( + r#" +let steps = [{}]; +async function step(n) {{ + if (n == 0) {{ + return flow_input; + }} + if (n == -1) {{ + return previous_result; + }} + let token = "{token}"; + if (n < 0) {{ + let steps_length = steps.length; + n = n % steps.length + steps.length; + }} + let id = steps[n]; + return await Deno.core.opAsync("result", [workspace, id, token, base_url]); +}}"#, + steps.into_iter().map(|x| format!("\"{x}\"")).join(",") + ) + } else { + "".to_string() + }; + + let code = format!( + r#" +let workspace = "{workspace}"; +let base_url = "{}"; +async function variable(path) {{ + let token = "{token}"; + return await Deno.core.opAsync("variable", [workspace, path, token, base_url]); +}} +async function resource(path) {{ + let token = "{token}"; + return await Deno.core.opAsync("resource", [workspace, path, token, base_url]); +}} +{} +{steps_code} +(async () => {{ + {expr} +}})() + "#, + std::env::var("BASE_INTERNAL_URL") + .unwrap_or_else(|_| "http://missing-base-url".to_string()), + env.into_iter() + .map(|(a, b)| format!( + "let {a} = {};\n", + serde_json::to_string(&b) + .unwrap_or_else(|_| "\"error serializing value\"".to_string()) + )) + .join(""), + ); + let global = context.execute_script("", &code)?; + let global = context.resolve_value(global).await?; + + let scope = &mut context.handle_scope(); + let local = v8::Local::new(scope, global); + // Deserialize a `v8` object into a Rust type using `serde_v8`, + // in this case deserialize to a JSON `Value`. + Ok(serde_v8::from_v8::(scope, local)?) +} + +// #[warn(dead_code)] +// async fn op_test( +// _state: Rc>, +// path: String, +// _buf: Option, +// ) -> Result { +// tokio::time::sleep(std::time::Duration::from_secs(1)).await; +// Ok(path) +// } + +async fn op_variable( + _state: Rc>, + args: Vec, + _buf: Option, +) -> Result { + let workspace = &args[0]; + let path = &args[1]; + let token = &args[2]; + let base_url = &args[3]; + client::get_variable(workspace, path, token, &base_url).await +} + +async fn op_get_result( + _state: Rc>, + args: Vec, + _buf: Option, +) -> Result, anyhow::Error> { + let workspace = &args[0]; + let id = &args[1]; + let token = &args[2]; + let base_url = &args[3]; + let client = reqwest::Client::new(); + let result = client + .get(format!( + "{base_url}/api/w/{workspace}/jobs/completed/get_result/{id}" + )) + .bearer_auth(token) + .send() + .await? + .json::>() + .await?; + Ok(result) +} + +async fn op_resource( + _state: Rc>, + args: Vec, + _buf: Option, +) -> Result, anyhow::Error> { + let workspace = &args[0]; + let path = &args[1]; + let token = &args[2]; + let base_url = &args[3]; + client::get_resource(workspace, path, token, &base_url).await +} + +#[cfg(test)] +mod tests { + + use serde_json::json; + + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + + #[tokio::test] + async fn test_eval() -> anyhow::Result<()> { + let env = vec![ + ("params".to_string(), json!({"test": 2})), + ("value".to_string(), json!({"test": 2})), + ]; + let code = "value.test + params.test"; + + let mut runtime = JsRuntime::new(RuntimeOptions::default()); + let res = eval(&mut runtime, code, env, "workspace", "token", vec![]).await?; + assert_eq!(res, json!(4)); + Ok(()) + } + + #[tokio::test] + async fn test_eval_multiline() -> anyhow::Result<()> { + let env = vec![]; + let code = "let x = 5; +`my ${x} +multiline template`"; + + let mut runtime = JsRuntime::new(RuntimeOptions::default()); + let res = eval(&mut runtime, code, env, "workspace", "token", vec![]).await?; + assert_eq!(res, json!("my 5\nmultiline template")); + Ok(()) + } + + #[tokio::test] + async fn test_eval_timeout() -> anyhow::Result<()> { + let env = vec![ + ("params".to_string(), json!({"test": 2})), + ("value".to_string(), json!({"test": 2})), + ]; + let code = r#"variable("test")"#; + + let res = eval_timeout(code.to_string(), env, "workspace", "token", vec![]).await?; + assert_eq!(res, json!("test")); + Ok(()) + } +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs new file mode 100644 index 0000000000..ce0609200c --- /dev/null +++ b/backend/src/lib.rs @@ -0,0 +1,342 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use ::oauth2::basic::BasicClient; +use argon2::Argon2; +use axum::{extract::extractor_middleware, handler::Handler, routing::get, Extension, Router}; +use db::DB; +use git_version::git_version; +use hyper::Response; +use slack_http_verifier::SlackVerifier; +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; +use tokio::sync::Mutex; +use tower::ServiceBuilder; +use tower_cookies::CookieManagerLayer; +use tower_http::trace::{MakeSpan, OnResponse, TraceLayer}; +use tracing::{field, Span}; +use tracing_subscriber::{filter::filter_fn, prelude::*, EnvFilter}; +extern crate magic_crypt; + +extern crate dotenv; + +mod audit; +mod client; +mod db; +mod email; +mod error; +mod flow; +mod granular_acls; +mod groups; +mod jobs; +mod js_eval; +mod oauth2; +mod parser; +mod resources; +mod schedule; +mod scripts; +mod static_assets; +mod users; +mod utils; +mod variables; +mod worker; +mod worker_ping; +mod workspaces; + +use error::Error; + +pub use crate::email::EmailSender; +use crate::{db::UserDB, utils::rd_string}; + +const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version"); +pub const DEFAULT_NUM_WORKERS: usize = 3; +pub const DEFAULT_TIMEOUT: i32 = 300; +pub const DEFAULT_SLEEP_QUEUE: u64 = 50; + +#[derive(Clone)] +struct MyOnResponse {} + +impl OnResponse for MyOnResponse { + fn on_response( + self, + response: &Response, + latency: std::time::Duration, + _span: &tracing::Span, + ) { + tracing::info!( + latency = %latency.as_millis(), + status = ?response.status(), + "finished processed request") + } +} + +#[derive(Clone)] +struct MyMakeSpan {} + +impl MakeSpan for MyMakeSpan { + fn make_span(&mut self, request: &hyper::Request) -> Span { + tracing::info_span!( + "request", + method = %request.method(), + uri = %request.uri(), + version = ?request.version(), + username = field::Empty, + ) + } +} + +pub async fn initialize_tracing() -> anyhow::Result<()> { + //let log_level = if std::env::var("RUST_LOG").map(|x| &x == "debug") + let ts_base = tracing_subscriber::registry() + .with( + EnvFilter::from_default_env() + //.add_directive("windmill".parse()?) + .add_directive("runtime=trace".parse()?) + .add_directive("tokio=trace".parse()?), + ) + .with( + tracing_subscriber::fmt::layer() + .json() + .flatten_event(true) + .with_span_list(false) + .with_current_span(true) + .with_filter(filter_fn(|meta| meta.target().starts_with("windmill"))), + ); + + if std::env::var("TOKIO_CONSOLE") + .map(|x| x == "true") + .unwrap_or(false) + { + let console_layer = console_subscriber::spawn(); + ts_base.with(console_layer).init(); + } else { + ts_base.init(); + } + Ok(()) +} + +pub async fn migrate_db(db: &DB) -> anyhow::Result<()> { + let app_password = std::env::var("APP_USER_PASSWORD").unwrap_or_else(|_| "changeme".to_owned()); + + db::migrate(db).await?; + db::setup_app_user(db, &app_password).await?; + Ok(()) +} + +pub async fn connect_db() -> anyhow::Result { + let database_url = std::env::var("DATABASE_URL") + .map_err(|_| Error::BadConfig("DATABASE_URL env var is missing".to_string()))?; + Ok(db::connect(&database_url).await?) +} + +type BasicClientsMap = HashMap; + +pub fn build_oauth_clients(base_url: &str) -> BasicClientsMap { + [( + "github".to_string(), + oauth2::build_gh_client( + &std::env::var("GITHUB_OAUTH_CLIENT_ID").unwrap_or_else(|_| "".to_string()), + &std::env::var("GITHUB_OAUTH_CLIENT_SECRET").unwrap_or_else(|_| "".to_string()), + base_url, + ), + )] + .into() +} + +#[derive(Clone)] +struct BaseUrl(String); + +pub async fn run_server( + db: DB, + addr: SocketAddr, + base_url: &str, + es: EmailSender, + mut rx: tokio::sync::broadcast::Receiver<()>, +) -> anyhow::Result<()> { + let user_db = UserDB::new(db.clone()); + + let auth_cache = Arc::new(users::AuthCache::new(db.clone())); + let argon2 = Arc::new(Argon2::default()); + let email_sender = Arc::new(es); + let basic_clients = Arc::new(build_oauth_clients(base_url)); + let slack_verifier = Arc::new( + std::env::var("SLACK_SIGNING_SECRET") + .ok() + .map(|x| SlackVerifier::new(x).unwrap()), + ); + + let middleware_stack = ServiceBuilder::new() + .layer( + TraceLayer::new_for_http() + .on_response(MyOnResponse {}) + .make_span_with(MyMakeSpan {}) + .on_request(()), + ) + .layer(Extension(db.clone())) + .layer(Extension(user_db)) + .layer(Extension(auth_cache.clone())) + .layer(Extension(basic_clients)) + .layer(Extension(BaseUrl(base_url.to_string()))) + .layer(CookieManagerLayer::new()); + // build our application with a route + let app = Router::new() + .nest( + "/api", + Router::new() + .nest( + "/w/:workspace_id", + Router::new() + .nest("/scripts", scripts::workspaced_service()) + .nest("/jobs", jobs::workspaced_service()) + .nest( + "/users", + users::workspaced_service() + .layer(Extension(argon2.clone())) + .layer(Extension(email_sender)), + ) + .nest("/variables", variables::workspaced_service()) + .nest("/oauth", oauth2::workspaced_service()) + .nest("/resources", resources::workspaced_service()) + .nest("/schedules", schedule::workspaced_service()) + .nest("/groups", groups::workspaced_service()) + .nest("/audit", audit::workspaced_service()) + .nest("/acls", granular_acls::workspaced_service()) + .nest("/workspaces", workspaces::workspaced_service()) + .nest("/flows", flow::workspaced_service()), + ) + .nest("/workspaces", workspaces::global_service()) + .nest( + "/users", + users::global_service().layer(Extension(argon2.clone())), + ) + .nest("/workers", worker_ping::global_service()) + .nest("/scripts", scripts::global_service()) + .nest("/schedules", schedule::global_service()) + .route_layer(extractor_middleware::()) + .route_layer(extractor_middleware::()) + .nest( + "/auth", + users::make_unauthed_service().layer(Extension(argon2)), + ) + .nest( + "/oauth", + oauth2::global_service().layer(Extension(slack_verifier)), + ) + .route("/version", get(git_v)) + .route("/openapi.yaml", get(openapi)), + ) + .fallback(static_assets::static_handler.into_service()) + .layer(middleware_stack); + + let instance_name = rd_string(5); + + tracing::info!(addr = %addr.to_string(), instance = %instance_name, "server started listening"); + let server = axum::Server::bind(&addr) + .serve(app.into_make_service()) + .with_graceful_shutdown(async { + rx.recv().await.ok(); + println!("Graceful shutdown of server"); + }); + + tokio::spawn(async move { auth_cache.monitor().await }); + + server.await?; + Ok(()) +} + +pub fn monitor_db(db: &DB, timeout: i32, tx: tokio::sync::broadcast::Sender<()>) { + let db1 = db.clone(); + let db2 = db.clone(); + + let rx1 = tx.subscribe(); + let rx2 = tx.subscribe(); + + tokio::spawn(async move { worker::restart_zombie_jobs_periodically(&db1, timeout, rx1).await }); + tokio::spawn(async move { users::delete_expired_items_perdiodically(&db2, rx2).await }); +} + +pub async fn run_workers( + db: DB, + addr: SocketAddr, + timeout: i32, + num_workers: i32, + sleep_queue: u64, + base_url: String, + tx: tokio::sync::broadcast::Sender<()>, +) -> anyhow::Result<()> { + let instance_name = rd_string(5); + + let mutex = Arc::new(Mutex::new(0)); + + let sources: external_ip::Sources = external_ip::get_http_sources(); + let consensus = external_ip::ConsensusBuilder::new() + .add_sources(sources) + .build(); + + let ip = consensus + .get_consensus() + .await + .map(|x| x.to_string()) + .unwrap_or_else(|| "Unretrievable ip".to_string()); + + let mut handles = Vec::new(); + for i in 1..(num_workers + 1) { + let db1 = db.clone(); + let instance_name = instance_name.clone(); + let worker_name = format!("dt-worker-{}-{}", &instance_name, rd_string(5)); + let m1 = mutex.clone(); + let ip = ip.clone(); + let tx = tx.clone(); + let base_url = base_url.clone(); + handles.push(tokio::spawn(async move { + tracing::info!(addr = %addr.to_string(), worker = %worker_name, "starting worker"); + worker::run_worker( + &db1, + timeout, + &instance_name, + worker_name, + i as u64, + num_workers as u64, + m1, + &ip, + sleep_queue, + &base_url, + tx, + ) + .await + })); + } + futures::future::try_join_all(handles).await?; + Ok(()) +} + +async fn git_v() -> &'static str { + GIT_VERSION +} + +async fn openapi() -> &'static str { + include_str!("../openapi.yaml") +} + +pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::Result<()> { + use std::io; + use tokio::signal::unix::SignalKind; + + async fn terminate() -> io::Result<()> { + tokio::signal::unix::signal(SignalKind::terminate())? + .recv() + .await; + Ok(()) + } + + tokio::select! { + _ = terminate() => {}, + _ = tokio::signal::ctrl_c() => {}, + } + println!("signal received, starting graceful shutdown"); + let _ = tx.send(()); + Ok(()) +} diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000000..070046ad1c --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,95 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use std::net::SocketAddr; + +use dotenv::dotenv; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenv().ok(); + + windmill::initialize_tracing().await?; + + let db = windmill::connect_db().await?; + + let num_workers = std::env::var("NUM_WORKERS") + .ok() + .and_then(|x| x.parse::().ok()) + .unwrap_or(windmill::DEFAULT_NUM_WORKERS as i32); + + let (server_mode, monitor_mode, migrate_db) = (true, true, true); + + if migrate_db { + windmill::migrate_db(&db).await?; + } + + let (tx, rx) = tokio::sync::broadcast::channel::<()>(3); + let shutdown_signal = windmill::shutdown_signal(tx.clone()); + + if server_mode || monitor_mode || num_workers > 0 { + let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); + + let timeout = std::env::var("TIMEOUT") + .ok() + .and_then(|x| x.parse::().ok()) + .unwrap_or(windmill::DEFAULT_TIMEOUT); + + let server_f = async { + if server_mode { + windmill::run_server( + db.clone(), + addr, + &std::env::var("BASE_URL").unwrap_or("http://localhost".to_string()), + windmill::EmailSender { + from: "bot@windmill.dev".to_string(), + server: "smtp.gmail.com".to_string(), + password: std::env::var("SMTP_PASSWORD").unwrap_or("NOPASS".to_string()), + }, + rx, + ) + .await?; + } + Ok(()) as anyhow::Result<()> + }; + + let base_url = std::env::var("BASE_INTERNAL_URL") + .unwrap_or_else(|_| "http://missing-base-url".to_string()); + + let workers_f = async { + if num_workers > 0 { + let sleep_queue = std::env::var("SLEEP_QUEUE") + .ok() + .and_then(|x| x.parse::().ok()) + .unwrap_or(windmill::DEFAULT_SLEEP_QUEUE); + + windmill::run_workers( + db.clone(), + addr, + timeout, + num_workers, + sleep_queue, + base_url, + tx.clone(), + ) + .await?; + } + Ok(()) as anyhow::Result<()> + }; + + let monitor_f = async { + if monitor_mode { + windmill::monitor_db(&db, timeout, tx.clone()); + } + Ok(()) as anyhow::Result<()> + }; + + futures::try_join!(shutdown_signal, server_f, workers_f, monitor_f)?; + } + + Ok(()) +} diff --git a/backend/src/oauth2.rs b/backend/src/oauth2.rs new file mode 100644 index 0000000000..5c5f689564 --- /dev/null +++ b/backend/src/oauth2.rs @@ -0,0 +1,712 @@ +use std::fmt::Debug; + +use std::sync::Arc; +use std::time::Duration; + +use axum::body::Bytes; +use axum::extract::{Extension, FromRequest, Path, Query, RequestParts}; +use axum::response::Redirect; +use axum::routing::{get, post}; +use axum::{async_trait, Router}; +use futures::TryFutureExt; +use hyper::StatusCode; +use oauth2::basic::{ + BasicClient, BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse, + BasicTokenType, +}; +use oauth2::reqwest::async_http_client; +use oauth2::{helpers, TokenType}; +use oauth2::{AccessToken, Client as OClient, RefreshToken, StandardRevocableToken}; +// Alternatively, this can be `oauth2::curl::http_client` or a custom client. +use oauth2::{ + AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, + TokenResponse, TokenUrl, +}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use slack_http_verifier::SlackVerifier; +use tower_cookies::{Cookie, Cookies}; + +use crate::audit::{audit_log, ActionKind}; +use crate::db::{UserDB, DB}; +use crate::error::{self, to_anyhow, Error, Result}; +use crate::jobs::{get_latest_hash_for_path, JobPayload}; +use crate::users::{Authed, LoginType}; +use crate::variables::build_crypt; +use crate::workspaces::WorkspaceSettings; +use crate::{jobs, BasicClientsMap}; +use crate::{variables, BaseUrl}; + +pub fn global_service() -> Router { + Router::new() + .route("/login/:client", get(login)) + .route("/login_callback/:client", get(login_callback)) + .route( + "/slack_command", + post(slack_command).route_layer(axum::extract::extractor_middleware::()), + ) +} + +pub fn workspaced_service() -> Router { + Router::new() + .route("/connect/:client", get(connect)) + .route("/disconnect/:client", post(disconnect)) + .route("/connect_callback/:client", get(connect_callback)) +} + +pub fn build_gh_client(client_id: &str, client_secret: &str, base_uri: &str) -> BasicClient { + let auth_url = AuthUrl::new("https://github.com/login/oauth/authorize".to_string()) + .expect("Invalid authorization endpoint URL"); + let token_url = TokenUrl::new("https://github.com/login/oauth/access_token".to_string()) + .expect("Invalid token endpoint URL"); + + // Set up the config for the Github OAuth2 process. + BasicClient::new( + ClientId::new(client_id.to_string()), + Some(ClientSecret::new(client_secret.to_string())), + auth_url, + Some(token_url), + ) + .set_redirect_uri( + RedirectUrl::new(format!("{base_uri}/api/oauth/login_callback/github")).unwrap(), + ) +} + +pub fn build_connect_client(w_id: &str, client_name: &str, base_uri: &str) -> Result { + let (auth_str, token_str) = match client_name { + "gmail" => ("", ""), + "slack" => ( + "https://slack.com/oauth/authorize", + "https://slack.com/api/oauth.access", + ), + _ => Err(Error::BadRequest(format!("unrecognized client!")))?, + }; + + let auth_url = AuthUrl::new(auth_str.to_string()).expect("Invalid authorization endpoint URL"); + let token_url = TokenUrl::new(token_str.to_string()).expect("Invalid token endpoint URL"); + + // Set up the config for the Github OAuth2 process. + Ok(BasicClient::new( + ClientId::new( + std::env::var(&format!("{}_OAUTH_CLIENT_ID", client_name.to_uppercase())) + .ok() + .ok_or(Error::BadRequest(format!( + "client id for {} not in env", + client_name + )))?, + ), + Some(ClientSecret::new( + std::env::var(&format!( + "{}_OAUTH_CLIENT_SECRET", + client_name.to_uppercase() + )) + .ok() + .ok_or(Error::BadRequest(format!( + "client secret for {} not in env", + client_name + )))?, + )), + auth_url, + Some(token_url), + ) + .set_redirect_uri( + RedirectUrl::new(format!( + "{base_uri}/api/w/{w_id}/oauth/connect_callback/{client_name}" + )) + .unwrap(), + )) +} + +type SlackClient = OClient< + BasicErrorResponse, + SlackTokenResponse, + BasicTokenType, + BasicTokenIntrospectionResponse, + StandardRevocableToken, + BasicRevocationErrorResponse, +>; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SlackTokenResponse { + access_token: AccessToken, + + team_id: String, + + team_name: String, + + #[serde(rename = "scope")] + #[serde(deserialize_with = "helpers::deserialize_space_delimited_vec")] + #[serde(serialize_with = "helpers::serialize_space_delimited_vec")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + scopes: Option>, + bot: SlackBotToken, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SlackBotToken { + bot_access_token: String, +} + +impl TokenResponse for SlackTokenResponse +where + BasicTokenType: TokenType, +{ + /// + /// REQUIRED. The access token issued by the authorization server. + /// + fn access_token(&self) -> &AccessToken { + &self.access_token + } + /// + /// REQUIRED. The type of the token issued as described in + /// [Section 7.1](https://tools.ietf.org/html/rfc6749#section-7.1). + /// Value is case insensitive and deserialized to the generic `TokenType` parameter. + /// But in this particular case as the service is non compliant, it has a default value + /// + fn token_type(&self) -> &BasicTokenType { + &BasicTokenType::Bearer + } + /// + /// RECOMMENDED. The lifetime in seconds of the access token. For example, the value 3600 + /// denotes that the access token will expire in one hour from the time the response was + /// generated. If omitted, the authorization server SHOULD provide the expiration time via + /// other means or document the default value. + /// + fn expires_in(&self) -> Option { + None + } + /// + /// OPTIONAL. The refresh token, which can be used to obtain new access tokens using the same + /// authorization grant as described in + /// [Section 6](https://tools.ietf.org/html/rfc6749#section-6). + /// + fn refresh_token(&self) -> Option<&RefreshToken> { + None + } + /// + /// OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED. The + /// scipe of the access token as described by + /// [Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3). If included in the response, + /// this space-delimited field is parsed into a `Vec` of individual scopes. If omitted from + /// the response, this field is `None`. + /// + fn scopes(&self) -> Option<&Vec> { + self.scopes.as_ref() + } +} + +pub fn build_slack_client(w_id: &str, client_name: &str, base_uri: &str) -> Result { + let (auth_str, token_str) = ( + "https://slack.com/oauth/authorize", + "https://slack.com/api/oauth.access", + ); + + let auth_url = AuthUrl::new(auth_str.to_string()).expect("Invalid authorization endpoint URL"); + let token_url = TokenUrl::new(token_str.to_string()).expect("Invalid token endpoint URL"); + + // Set up the config for the Github OAuth2 process. + Ok(SlackClient::new( + ClientId::new( + std::env::var(&format!("{}_OAUTH_CLIENT_ID", client_name.to_uppercase())) + .ok() + .ok_or(Error::BadRequest(format!( + "client id for {} not in env", + client_name + )))?, + ), + Some(ClientSecret::new( + std::env::var(&format!( + "{}_OAUTH_CLIENT_SECRET", + client_name.to_uppercase() + )) + .ok() + .ok_or(Error::BadRequest(format!( + "client secret for {} not in env", + client_name + )))?, + )), + auth_url, + Some(token_url), + ) + .set_redirect_uri( + RedirectUrl::new(format!( + "{base_uri}/api/w/{w_id}/oauth/connect_callback/{client_name}" + )) + .unwrap(), + )) +} + +async fn connect( + Path((w_id, client_name)): Path<(String, String)>, + Extension(base_url): Extension, + cookies: Cookies, +) -> error::Result { + let client = build_connect_client(&w_id, &client_name, &base_url.0)?; + + let (authorize_url, csrf_state) = client + .authorize_url(CsrfToken::new_random) + .add_scope(Scope::new("bot".to_string())) + .add_scope(Scope::new("commands".to_string())) + .url(); + + let csrf = csrf_state.secret().to_string(); + let mut cookie = Cookie::new("csrf", csrf); + cookie.set_path("/"); + cookies.add(cookie); + Ok(Redirect::to(authorize_url.as_str())) +} + +async fn disconnect( + authed: Authed, + Path((w_id, client_name)): Path<(String, String)>, + Extension(user_db): Extension, +) -> error::Result { + let mut tx = user_db.begin(&authed).await?; + + match client_name.as_str() { + "slack" => { + sqlx::query!( + "UPDATE workspace_settings + SET slack_team_id = null, slack_name = null WHERE workspace_id = $1", + &w_id + ) + .execute(&mut tx) + .await?; + } + _ => Err(error::Error::BadRequest(format!( + "Not recognized client name {client_name}" + )))?, + } + tx.commit().await?; + Ok(format!("{client_name} disconnected")) +} + +async fn login( + Extension(clients): Extension>, + Path(client_name): Path, + cookies: Cookies, +) -> error::Result { + let client = clients + .get(&client_name) + .ok_or(Error::BadRequest(format!("client {} invalid", client_name)))?; + let (authorize_url, csrf_state) = client + .authorize_url(CsrfToken::new_random) + .add_scope(Scope::new("user:email".to_string())) + // .add_scope(Scope::new("read:user".to_string())) + .url(); + + let csrf = csrf_state.secret().to_string(); + let mut cookie = Cookie::new("csrf", csrf); + cookie.set_path("/"); + cookies.add(cookie); + Ok(Redirect::to(authorize_url.as_str())) +} + +#[derive(Deserialize)] +pub struct CallbackQuery { + code: Option, + state: Option, + error: Option, +} + +async fn connect_callback( + authed: Authed, + Path((w_id, client_name)): Path<(String, String)>, + Query(query): Query, + cookies: Cookies, + Extension(user_db): Extension, + Extension(base_url): Extension, +) -> error::Result { + if let Some(error) = query.error { + return Ok(Redirect::to(&format!( + "/connection_added?error={}", + urlencoding::encode(&error).into_owned() + ))); + } + + let code = AuthorizationCode::new(query.code.unwrap()); + let state = CsrfToken::new(query.state.unwrap()); + + let csrf_state = cookies + .get("csrf") + .map(|x| x.value().to_string()) + .unwrap_or("".to_string()); + + if state.secret().to_string() != csrf_state { + return Err(error::Error::BadRequest("csrf did not match".to_string())); + } + + let mut tx = user_db.begin(&authed).await?; + + let mc = build_crypt(&mut tx, &w_id).await?; + + let token_res = match client_name.as_str() { + "slack" => { + let t = build_slack_client(&w_id, &client_name, &base_url.0)? + .exchange_code(code) + .request_async(async_http_client) + .await; + if let Ok(token) = t { + 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", + &w_id, + token.team_id, + token.team_name + ) + .execute(&mut tx) + .await?; + sqlx::query!( + "INSERT INTO group_ + (workspace_id, name, summary) + VALUES ($1, $2, $3) 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." + ) + .execute(&mut tx) + .await?; + Ok(token.bot.bot_access_token.to_owned()) + } else { + Err(t.unwrap_err()) + } + } + _ => { + build_connect_client(&w_id, &client_name, &base_url.0)? + .exchange_code(code) + .request_async(async_http_client) + .map_ok(|t| t.access_token().secret().to_owned()) + .await + } + }; + + if let Ok(token) = token_res { + tracing::info!("{token}"); + let variable_path = &format!("g/all/{}_token", &client_name); + sqlx::query!( + "INSERT INTO variable + (workspace_id, path, value, is_secret, description) + VALUES ($1, $2, $3, true, $4) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3", + &w_id, + variable_path, + variables::encrypt(&mc, token.to_string()), + format!("OAuth2 token for {client_name}"), + ) + .execute(&mut tx) + .await?; + sqlx::query!( + "INSERT INTO resource + (workspace_id, path, value, description, resource_type) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3", + &w_id, + variable_path, + serde_json::json!({ "token": format!("$var:{variable_path}") }), + format!("OAuth2 token for {client_name}"), + &client_name + ) + .execute(&mut tx) + .await?; + audit_log( + &mut tx, + &authed.username, + "oauth2.connect", + ActionKind::Create, + &w_id, + Some(&client_name), + None, + ) + .await?; + tx.commit().await?; + Ok(Redirect::to( + format!("/connection_added?client_name={}", &client_name).as_str(), + )) + } else { + let error = token_res.unwrap_err().to_string(); + Ok(Redirect::to(&format!( + "/connection_added?error={}", + urlencoding::encode(&format!("error fetching token: {error}")).into_owned() + ))) + } +} + +#[derive(Deserialize, Debug)] +pub struct SlackCommand { + team_id: String, + user_name: String, + text: String, + response_url: String, +} + +#[derive(Clone, Debug)] +pub struct SlackSig { + sig: String, + ts: String, +} + +#[async_trait] +impl FromRequest for SlackSig +where + B: Send, +{ + type Rejection = (StatusCode, String); + + async fn from_request(req: &mut RequestParts) -> std::result::Result { + let hm = req.headers(); + Ok(Self { + sig: hm + .get("X-Slack-Signature") + .map(|x| x.to_str().unwrap_or("")) + .unwrap_or("") + .to_string(), + ts: hm + .get("X-Slack-Request-Timestamp") + .map(|x| x.to_str().unwrap_or("")) + .unwrap_or("") + .to_string(), + }) + } +} + +async fn slack_command( + SlackSig { sig, ts }: SlackSig, + Extension(slack_verifier): Extension>>, + Extension(db): Extension, + Extension(base_url): Extension, + body: Bytes, +) -> error::Result { + let form: SlackCommand = serde_urlencoded::from_bytes(&body) + .map_err(|_| error::Error::BadRequest("invalid payload".to_string()))?; + + let body = String::from_utf8_lossy(&body); + if slack_verifier + .as_ref() + .as_ref() + .map(|sv| sv.verify(&ts, &body, &sig).ok()) + .flatten() + .is_none() + { + return Err(error::Error::BadRequest("verification failed".to_owned())); + } + + let mut tx = db.begin().await?; + let settings = sqlx::query_as!( + WorkspaceSettings, + "SELECT * FROM workspace_settings WHERE slack_team_id = $1", + form.team_id, + ) + .fetch_optional(&mut tx) + .await?; + + if let Some(settings) = settings { + if let Some(script) = &settings.slack_command_script { + let script_hash = + get_latest_hash_for_path(&mut tx, &settings.workspace_id, script).await?; + let mut map = serde_json::Map::new(); + map.insert("text".to_string(), serde_json::Value::String(form.text)); + map.insert( + "response_url".to_string(), + serde_json::Value::String(form.response_url), + ); + + let (uuid, tx) = jobs::push( + tx, + &settings.workspace_id, + JobPayload::ScriptHash { + hash: script_hash, + path: script.to_owned(), + }, + Some(map), + &form.user_name, + "g/slack".to_string(), + None, + None, + None, + false, + ) + .await?; + tx.commit().await?; + let url = base_url.0; + return Ok(format!("Job launched. See details at {url}/run/{uuid}")); + } + } + + return Ok(format!( + "workspace not properly configured (did you set the script to trigger in the settings?)" + )); +} + +#[derive(Deserialize)] +pub struct UserInfo { + name: Option, + company: Option, +} + +async fn login_callback( + Path(client_name): Path, + Query(query): Query, + cookies: Cookies, + Extension(clients): Extension>, + Extension(db): Extension, +) -> error::Result { + if let Some(error) = query.error { + return Ok(Redirect::to(&format!( + "/user/login?error={}", + urlencoding::encode(&error).into_owned() + ))); + } + + let code = AuthorizationCode::new(query.code.unwrap()); + let state = CsrfToken::new(query.state.unwrap()); + + let csrf_state = cookies + .get("csrf") + .map(|x| x.value().to_string()) + .unwrap_or("".to_string()); + + if state.secret().to_string() != csrf_state { + return Err(error::Error::BadRequest("csrf did not match".to_string())); + } + + let client = clients.get(&client_name).unwrap(); + + // Exchange the code with a token. + let token_res = client + .exchange_code(code) + .request_async(async_http_client) + .await; + + if let Ok(token) = token_res { + let token = token.access_token().secret(); + let http_client = reqwest::ClientBuilder::new() + .user_agent("windmill/beta") + .build() + .map_err(to_anyhow)?; + + let email = get_email(&http_client, &client_name, token).await?; + + let mut tx = db.begin().await?; + + let login: Option<(String, LoginType, bool)> = + sqlx::query_as("SELECT email, login_type, super_admin FROM password WHERE email = $1") + .bind(&email) + .fetch_optional(&mut tx) + .await?; + + if let Some((email, login_type, super_admin)) = login { + let login_type = serde_json::json!(login_type); + if login_type == client_name { + crate::users::create_session_token(&email, super_admin, &mut tx, cookies).await?; + } else { + return Err(error::Error::BadRequest(format!( + "an user with the email associated to this login exists but with a different login type {login_type}") + )); + } + } else { + let user = get_user_info(&http_client, &client_name, &token).await?; + + sqlx::query( + &format!("INSERT INTO password (email, name, company, login_type, verified) VALUES ($1, $2, $3, '{}', true)", &client_name) + ) + .bind(&email) + .bind(&user.name) + .bind(user.company) + .execute(&mut tx) + .await?; + crate::users::create_session_token(&email, false, &mut tx, cookies).await?; + audit_log( + &mut tx, + &email, + "oauth.signup", + ActionKind::Create, + "global", + Some("github"), + None, + ) + .await?; + let demo_exists = + sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM workspace WHERE id = 'demo')") + .fetch_one(&mut tx) + .await? + .unwrap_or(false); + if demo_exists { + sqlx::query!( + "INSERT INTO workspace_invite + (workspace_id, email, is_admin) + VALUES ('demo', $1, false)", + &email + ) + .execute(&mut tx) + .await?; + } + } + tx.commit().await?; + Ok(Redirect::to("/user/workspaces")) + } else { + Ok(Redirect::to(&format!( + "/user/login?error={}", + urlencoding::encode("invalid token").into_owned() + ))) + } +} + +#[derive(Deserialize)] +pub struct GHEmailInfo { + email: String, + verified: bool, + primary: bool, +} + +async fn get_email(http_client: &Client, client_name: &str, token: &str) -> error::Result { + let email = match client_name { + "github" => http_client + .get("https://api.github.com/user/emails") + .bearer_auth(token) + .send() + .await + .map_err(to_anyhow)? + .json::>() + .await + .map_err(to_anyhow)? + .iter() + .find(|x| x.primary && x.verified) + .ok_or(error::Error::BadRequest(format!( + "user does not have any primary and verified address" + )))? + .email + .to_string(), + _ => { + return Err(error::Error::BadRequest( + "client name not recognized".to_string(), + )) + } + }; + Ok(email) +} + +async fn get_user_info( + http_client: &Client, + client_name: &str, + token: &str, +) -> error::Result { + let email = match client_name { + "github" => http_client + .get("https://api.github.com/user") + .bearer_auth(token) + .send() + .await + .map_err(to_anyhow)? + .json::() + .await + .map_err(to_anyhow)?, + _ => { + return Err(error::Error::BadRequest( + "client name not recognized".to_string(), + )) + } + }; + Ok(email) +} diff --git a/backend/src/parser.rs b/backend/src/parser.rs new file mode 100644 index 0000000000..63ba7d48c7 --- /dev/null +++ b/backend/src/parser.rs @@ -0,0 +1,592 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use std::collections::HashMap; + +use itertools::Itertools; +use regex::Regex; +use serde::Serialize; +use serde_json::json; + +use crate::error; + +use rustpython_parser::{ + ast::{ExpressionType, Located, Number, StatementType, StringGroup, Varargs}, + parser, +}; +#[derive(Serialize)] +pub struct MainArgSignature { + pub star_args: bool, + pub star_kwargs: bool, + pub args: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all(serialize = "lowercase"))] +pub enum Typ { + Str, + Int, + Float, + Bool, + Dict, + List, + Bytes, + Datetime, + Unknown, +} + +#[derive(Serialize)] +pub struct Arg { + pub name: String, + pub typ: Typ, + pub default: Option, + pub has_default: bool, +} + +pub fn parse_signature(code: &str) -> error::Result { + let ast = parser::parse_program(code) + .map_err(|e| error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string())))? + .statements; + let param = ast.into_iter().find_map(|x| match x { + Located { + location: _, + node: + StatementType::FunctionDef { + is_async: _, + name, + args, + body: _, + decorator_list: _, + returns: _, + }, + } if &name == "main" => Some(*args), + _ => None, + }); + if let Some(params) = param { + //println!("{:?}", params); + let def_arg_start = params.args.len() - params.defaults.len(); + Ok(MainArgSignature { + star_args: params.vararg != Varargs::None, + star_kwargs: params.vararg != Varargs::None, + args: params + .args + .into_iter() + .enumerate() + .map(|(i, x)| { + let default = if i >= def_arg_start { + to_value(¶ms.defaults[i - def_arg_start].node) + } else { + None + }; + Arg { + name: x.arg, + typ: x.annotation.map_or(Typ::Unknown, |e| match *e { + Located { + location: _, + node: ExpressionType::Identifier { name }, + } => match name.as_ref() { + "str" => Typ::Str, + "float" => Typ::Float, + "int" => Typ::Int, + "bool" => Typ::Bool, + "dict" => Typ::Dict, + "list" => Typ::List, + "bytes" => Typ::Bytes, + "datetime" => Typ::Datetime, + "datetime.datetime" => Typ::Datetime, + _ => Typ::Unknown, + }, + _ => Typ::Unknown, + }), + has_default: default.is_some(), + default, + } + }) + .collect(), + }) + } else { + Err(error::Error::ExecutionErr( + "main function was not findable".to_string(), + )) + } +} + +const STDIMPORTS: [&str; 301] = [ + "__future__", + "_abc", + "_aix_support", + "_ast", + "_asyncio", + "_bisect", + "_blake2", + "_bootsubprocess", + "_bz2", + "_codecs", + "_codecs_cn", + "_codecs_hk", + "_codecs_iso2022", + "_codecs_jp", + "_codecs_kr", + "_codecs_tw", + "_collections", + "_collections_abc", + "_compat_pickle", + "_compression", + "_contextvars", + "_crypt", + "_csv", + "_ctypes", + "_curses", + "_curses_panel", + "_datetime", + "_dbm", + "_decimal", + "_elementtree", + "_frozen_importlib", + "_frozen_importlib_external", + "_functools", + "_gdbm", + "_hashlib", + "_heapq", + "_imp", + "_io", + "_json", + "_locale", + "_lsprof", + "_lzma", + "_markupbase", + "_md5", + "_msi", + "_multibytecodec", + "_multiprocessing", + "_opcode", + "_operator", + "_osx_support", + "_overlapped", + "_pickle", + "_posixshmem", + "_posixsubprocess", + "_py_abc", + "_pydecimal", + "_pyio", + "_queue", + "_random", + "_sha1", + "_sha256", + "_sha3", + "_sha512", + "_signal", + "_sitebuiltins", + "_socket", + "_sqlite3", + "_sre", + "_ssl", + "_stat", + "_statistics", + "_string", + "_strptime", + "_struct", + "_symtable", + "_thread", + "_threading_local", + "_tkinter", + "_tracemalloc", + "_uuid", + "_warnings", + "_weakref", + "_weakrefset", + "_winapi", + "_zoneinfo", + "abc", + "aifc", + "antigravity", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "builtins", + "bz2", + "cProfile", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "copy", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "distutils", + "doctest", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "fractions", + "ftplib", + "functools", + "gc", + "genericpath", + "getopt", + "getpass", + "gettext", + "glob", + "graphlib", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "idlelib", + "imaplib", + "imghdr", + "imp", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "nt", + "ntpath", + "nturl2path", + "numbers", + "opcode", + "operator", + "optparse", + "os", + "ossaudiodev", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "posixpath", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "pydoc_data", + "pyexpat", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "sre_compile", + "sre_constants", + "sre_parse", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "textwrap", + "this", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "", +]; + +fn to_value(et: &ExpressionType) -> Option { + match et { + ExpressionType::String { + value: StringGroup::Constant { value }, + } => Some(json!(value)), + ExpressionType::Number { value } => match value { + Number::Integer { value } => Some(json!(value.to_string().parse::().unwrap())), + Number::Float { value } => Some(json!(value)), + _ => None, + }, + ExpressionType::True => Some(json!(true)), + ExpressionType::False => Some(json!(false)), + + ExpressionType::Dict { elements } => { + let v = elements + .into_iter() + .map(|(k, v)| { + let key = k + .as_ref() + .and_then(|x| to_value(&x.node)) + .and_then(|x| match x { + serde_json::Value::String(s) => Some(s), + _ => None, + }) + .unwrap_or_else(|| "no_key".to_string()); + (key, to_value(&v.node)) + }) + .collect::>(); + Some(json!(v)) + } + ExpressionType::List { elements } => { + let v = elements + .into_iter() + .map(|x| to_value(&x.node)) + .collect::>(); + Some(json!(v)) + } + ExpressionType::None => Some(json!(null)), + + ExpressionType::Call { + function: _, + args: _, + keywords: _, + } => Some(json!("")), + + _ => None, + } +} + +pub fn parse_imports(code: &str) -> error::Result> { + let find_requirements = code + .lines() + .find_position(|x| x.starts_with("#requirements:")); + let re = Regex::new(r"^\#(\S+)$").unwrap(); + if let Some((pos, _)) = find_requirements { + let lines = code + .lines() + .skip(pos + 1) + .map_while(|x| { + re.captures(x) + .map(|x| x.get(1).unwrap().as_str().to_string()) + }) + .collect(); + Ok(lines) + } else { + let ast = parser::parse_program(code) + .map_err(|e| { + error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string())) + })? + .statements; + let imports = ast + .into_iter() + .filter_map(|x| match x { + Located { location: _, node } => match node { + StatementType::Import { names } => Some( + names + .into_iter() + .map(|x| x.symbol.split('.').next().unwrap_or("").to_string()) + .collect::>(), + ), + StatementType::ImportFrom { + level: _, + module: Some(mod_), + names: _, + } => Some(vec![mod_ + .split('.') + .next() + .unwrap_or("") + .to_string() + .replace("_", "-")]), + _ => None, + }, + }) + .flatten() + .filter(|x| !STDIMPORTS.contains(&x.as_str())) + .unique() + .collect(); + Ok(imports) + } +} + +#[cfg(test)] +mod tests { + + // Note this useful idiom: importing names from outer (for mod tests) scope. + use super::*; + + #[test] + fn test_parse_sig() -> anyhow::Result<()> { + //let code = "print(2 + 3, fd=sys.stderr)"; + let code = " + +import os + +def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = bytes(1)): + + print(f\"Hello World and a warm welcome especially to {name}\") + print(\"The env variable at `all/pretty_secret`: \", os.environ.get(\"ALL_PRETTY_SECRET\")) + return {\"len\": len(name), \"splitted\": name.split() } + +"; + println!("{}", serde_json::to_string(&parse_signature(code)?)?); + + Ok(()) + } + + #[test] + fn test_parse_imports() -> anyhow::Result<()> { + //let code = "print(2 + 3, fd=sys.stderr)"; + let code = " + +import os +import wmill +from zanzibar.estonie import talin +import matplotlib.pyplot as plt + +def main(): + pass + +"; + let r = parse_imports(code)?; + println!("{}", serde_json::to_string(&r)?); + assert_eq!(r, vec!["wmill", "zanzibar", "matplotlib"]); + Ok(()) + } + + #[test] + fn test_parse_imports2() -> anyhow::Result<()> { + //let code = "print(2 + 3, fd=sys.stderr)"; + let code = " +#requirements: +#burkina=0.4 +#nigeria +# +#congo + +import os +import wmill +from zanzibar.estonie import talin + +def main(): + pass + +"; + let r = parse_imports(code)?; + println!("{}", serde_json::to_string(&r)?); + assert_eq!(r, vec!["burkina=0.4", "nigeria"]); + + Ok(()) + } +} diff --git a/backend/src/resources.rs b/backend/src/resources.rs new file mode 100644 index 0000000000..faefc81144 --- /dev/null +++ b/backend/src/resources.rs @@ -0,0 +1,426 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use crate::{ + audit::{audit_log, ActionKind}, + db::{UserDB, DB}, + error::{Error, JsonResult, Result}, + users::Authed, + utils::{require_admin, Pagination, StripPath}, +}; +use axum::{ + extract::{Extension, Path, Query}, + routing::{delete, get, post}, + Json, Router, +}; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; +use sql_builder::{bind::Bind, SqlBuilder}; +use sqlx::FromRow; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/list", get(list_resources)) + .route("/get/*path", get(get_resource)) + .route("/get_value/*path", get(get_resource_value)) + .route("/update/*path", post(update_resource)) + .route("/delete/*path", delete(delete_resource)) + .route("/create", post(create_resource)) + .route("/type/list", get(list_resource_types)) + .route("/type/listnames", get(list_resource_types_names)) + .route("/type/get/:name", get(get_resource_type)) + .route("/type/update/:name", post(update_resource_type)) + .route("/type/delete/:name", delete(delete_resource_type)) + .route("/type/create", post(create_resource_type)) +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct ResourceType { + pub workspace_id: String, + pub name: String, + pub schema: Option, + pub description: Option, +} + +#[derive(Deserialize)] +pub struct CreateResourceType { + pub name: String, + pub schema: Option, + pub description: Option, +} + +#[derive(Deserialize)] +pub struct EditResourceType { + pub schema: Option, + pub description: Option, +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Resource { + pub workspace_id: String, + pub path: String, + pub value: Option, + pub description: Option, + pub resource_type: String, + pub extra_perms: serde_json::Value, +} + +#[derive(Deserialize)] +pub struct CreateResource { + pub path: String, + pub value: Option, + pub description: Option, + pub resource_type: String, +} +#[derive(Deserialize)] +struct EditResource { + path: Option, + description: Option, + value: Option, +} + +#[derive(Deserialize)] +pub struct ListResourceQuery { + resource_type: Option, +} +async fn list_resources( + authed: Authed, + Query(lq): Query, + Query(pagination): Query, + Extension(user_db): Extension, + Path(w_id): Path, +) -> JsonResult> { + let (per_page, offset) = crate::utils::paginate(pagination); + + let mut sqlb = SqlBuilder::select_from("resource") + .fields(&[ + "workspace_id", + "path", + "null::JSONB as value", + "description", + "resource_type", + "extra_perms", + ]) + .order_by("path", true) + .and_where("workspace_id = ? OR workspace_id = 'starter'".bind(&w_id)) + .offset(offset) + .limit(per_page) + .clone(); + if let Some(rt) = &lq.resource_type { + sqlb.and_where_eq("resource_type", "?".bind(rt)); + } + + 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::<_, Resource>(&sql) + .fetch_all(&mut tx) + .await?; + + tx.commit().await?; + + Ok(Json(rows)) +} + +async fn get_resource( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + + let resource_o = sqlx::query_as!( + Resource, + "SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + path.to_owned(), + &w_id + ) + .fetch_optional(&mut tx) + .await?; + tx.commit().await?; + + let resource = crate::utils::not_found_if_none(resource_o, "Resource", path)?; + Ok(Json(resource)) +} + +async fn get_resource_value( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult> { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + + let value_o = sqlx::query_scalar!( + "SELECT value from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + path.to_owned(), + &w_id + ) + .fetch_optional(&mut tx) + .await?; + tx.commit().await?; + + let value = crate::utils::not_found_if_none(value_o, "Resource", path)?; + Ok(Json(value)) +} + +async fn create_resource( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(resource): Json, +) -> Result<(StatusCode, String)> { + let mut tx = user_db.begin(&authed).await?; + + sqlx::query!( + "INSERT INTO resource + (workspace_id, path, value, description, resource_type) + VALUES ($1, $2, $3, $4, $5)", + w_id, + resource.path, + resource.value, + resource.description, + resource.resource_type, + ) + .execute(&mut tx) + .await?; + audit_log( + &mut tx, + &authed.username, + "resources.create", + ActionKind::Create, + &w_id, + Some(&resource.path), + None, + ) + .await?; + tx.commit().await?; + + Ok(( + StatusCode::CREATED, + format!("resource {} created", resource.path), + )) +} + +async fn delete_resource( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> Result { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + + sqlx::query!( + "DELETE FROM resource WHERE path = $1 AND workspace_id = $2", + path, + w_id + ) + .execute(&mut tx) + .await?; + audit_log( + &mut tx, + &authed.username, + "resources.delete", + ActionKind::Delete, + &w_id, + Some(path), + None, + ) + .await?; + tx.commit().await?; + + Ok(format!("resource {} deleted", path)) +} + +async fn update_resource( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + Json(ns): Json, +) -> Result { + use sql_builder::prelude::*; + + let path = path.to_path(); + + let mut sqlb = SqlBuilder::update_table("resource"); + sqlb.and_where_eq("path", "?".bind(&path)); + sqlb.and_where_eq("workspace_id", "?".bind(&w_id)); + + if let Some(npath) = &ns.path { + sqlb.set_str("path", npath); + } + if let Some(nvalue) = ns.value { + sqlb.set_str("value", nvalue.to_string()); + } + if let Some(ndesc) = ns.description { + sqlb.set_str("description", ndesc); + } + let mut tx = user_db.begin(&authed).await?; + + let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?; + sqlx::query(&sql).execute(&mut tx).await?; + audit_log( + &mut tx, + &authed.username, + "resources.update", + ActionKind::Update, + &w_id, + Some(path), + None, + ) + .await?; + tx.commit().await?; + + Ok(format!("resource {} updated (npath: {:?})", path, ns.path)) +} + +async fn list_resource_types( + Extension(db): Extension, + Path(w_id): Path, +) -> JsonResult> { + let rows = sqlx::query_as!(ResourceType, "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", &w_id) + .fetch_all(&db) + .await?; + + Ok(Json(rows)) +} + +async fn list_resource_types_names( + Extension(db): Extension, + Path(w_id): Path, +) -> JsonResult> { + let rows = sqlx::query_scalar!("SELECT name from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", &w_id) + .fetch_all(&db) + .await?; + + Ok(Json(rows)) +} + +async fn get_resource_type( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, +) -> JsonResult { + let mut tx = user_db.begin(&authed).await?; + + let resource_type_o = sqlx::query_as!( + ResourceType, + "SELECT * from resource_type WHERE name = $1 AND (workspace_id = $2 OR workspace_id = 'starter')", + &name, + &w_id + ) + .fetch_optional(&mut tx) + .await?; + tx.commit().await?; + + let resource_type = crate::utils::not_found_if_none(resource_type_o, "ResourceType", name)?; + Ok(Json(resource_type)) +} + +async fn create_resource_type( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(resource_type): Json, +) -> Result<(StatusCode, String)> { + let mut tx = user_db.begin(&authed).await?; + + sqlx::query!( + "INSERT INTO resource_type + (workspace_id, name, schema, description) + VALUES ($1, $2, $3, $4)", + w_id, + resource_type.name, + resource_type.schema, + resource_type.description, + ) + .execute(&mut tx) + .await?; + audit_log( + &mut tx, + &authed.username, + "resource_types.create", + ActionKind::Create, + &w_id, + Some(&resource_type.name), + None, + ) + .await?; + tx.commit().await?; + + Ok(( + StatusCode::CREATED, + format!("resource_type {} created", resource_type.name), + )) +} + +async fn delete_resource_type( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, +) -> Result { + require_admin(authed.is_admin, &authed.username)?; + + let mut tx = user_db.begin(&authed).await?; + + sqlx::query!( + "DELETE FROM resource_type WHERE name = $1 AND workspace_id = $2", + name, + w_id + ) + .execute(&mut tx) + .await?; + audit_log( + &mut tx, + &authed.username, + "resource_types.delete", + ActionKind::Delete, + &w_id, + Some(&name), + None, + ) + .await?; + tx.commit().await?; + + Ok(format!("resource_type {} deleted", name)) +} + +async fn update_resource_type( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, name)): Path<(String, String)>, + Json(ns): Json, +) -> Result { + use sql_builder::prelude::*; + + let mut sqlb = SqlBuilder::update_table("resource_type"); + sqlb.and_where_eq("name", "?".bind(&name)); + sqlb.and_where_eq("workspace_id", "?".bind(&w_id)); + if let Some(nschema) = ns.schema { + sqlb.set_str("schema", nschema); + } + if let Some(ndesc) = ns.description { + sqlb.set_str("description", ndesc); + } + let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?; + let mut tx = user_db.begin(&authed).await?; + + sqlx::query(&sql).execute(&mut tx).await?; + audit_log( + &mut tx, + &authed.username, + "resource_types.update", + ActionKind::Update, + &w_id, + Some(&name), + None, + ) + .await?; + tx.commit().await?; + + Ok(format!("resource_type {} updated", name)) +} diff --git a/backend/src/schedule.rs b/backend/src/schedule.rs new file mode 100644 index 0000000000..efba8d6ccd --- /dev/null +++ b/backend/src/schedule.rs @@ -0,0 +1,360 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use std::str::FromStr; + +use crate::{ + audit::{audit_log, ActionKind}, + db::UserDB, + error::{self, JsonResult, Result}, + jobs::{self, push, JobPayload}, + users::Authed, + utils::{get_owner_from_path, Pagination, StripPath}, +}; +use axum::{ + extract::{Extension, Path, Query}, + routing::{get, post}, + Json, Router, +}; + +use chrono::{DateTime, Duration, FixedOffset}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use sqlx::{FromRow, Postgres, Transaction}; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/list", get(list_schedule)) + .route("/get/*path", get(get_schedule)) + .route("/create", post(create_schedule)) + .route("/update/*path", post(edit_schedule)) + .route("/setenabled/*path", post(set_enabled)) +} + +pub fn global_service() -> Router { + Router::new().route("/preview", post(preview_schedule)) +} + +#[derive(FromRow, Serialize, Deserialize, Debug)] +pub struct Schedule { + pub workspace_id: String, + pub path: String, + pub edited_by: String, + pub edited_at: DateTime, + pub schedule: String, + pub offset_: i32, + pub enabled: bool, + pub script_path: String, + pub is_flow: bool, + pub args: Option, + pub extra_perms: serde_json::Value, +} + +#[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, +} + +pub async fn push_scheduled_job<'c>( + mut tx: Transaction<'c, Postgres>, + schedule: Schedule, +) -> Result> { + let sched = cron::Schedule::from_str(&schedule.schedule) + .map_err(|e| error::Error::BadRequest(e.to_string()))?; + + let offset = Duration::minutes(schedule.offset_.into()); + let next = sched + .after(&(chrono::Utc::now() - offset + Duration::seconds(1))) + .next() + .expect("a schedule should have a next event") + + offset; + + let mut args: Option> = None; + + if let Some(args_v) = schedule.args { + if let Value::Object(args_m) = args_v { + args = Some(args_m) + } else { + return Err(error::Error::ExecutionErr( + "args of scripts needs to be dict".to_string(), + )); + } + } + + let payload = if schedule.is_flow { + JobPayload::Flow(schedule.script_path) + } else { + JobPayload::ScriptHash { + hash: jobs::get_latest_hash_for_path( + &mut tx, + &schedule.workspace_id, + &schedule.script_path, + ) + .await?, + path: schedule.script_path, + } + }; + let (_, tx) = push( + tx, + &schedule.workspace_id, + payload, + args, + &schedule_to_user(&schedule.path), + get_owner_from_path(&schedule.path), + Some(next), + Some(schedule.path), + None, + false, + ) + .await?; + Ok(tx) +} + +async fn create_schedule( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(ns): Json, +) -> Result { + cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; + let mut tx = user_db.begin(&authed).await?; + + let schedule = sqlx::query_as!(Schedule, + "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *", + w_id, + ns.path, + ns.schedule, + ns.offset, + &authed.username, + ns.script_path, + ns.is_flow, + ns.args + ) + .fetch_one(&mut tx) + .await?; + + 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?; + + let tx = push_scheduled_job(tx, schedule).await?; + tx.commit().await?; + Ok(ns.path.to_string()) +} + +#[derive(Deserialize)] +pub struct EditSchedule { + pub schedule: String, + pub script_path: String, + pub is_flow: bool, + pub args: Option, +} + +async fn clear_schedule<'c>(db: &mut Transaction<'c, Postgres>, path: &str) -> Result<()> { + sqlx::query!("DELETE FROM queue WHERE schedule_path = $1", path) + .execute(db) + .await?; + Ok(()) +} + +async fn edit_schedule( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + Json(es): Json, +) -> Result { + let path = path.to_path(); + + cron::Schedule::from_str(&es.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; + + let mut tx = user_db.begin(&authed).await?; + + clear_schedule(&mut tx, path).await?; + let schedule = sqlx::query_as!(Schedule, + "UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path = $5 AND workspace_id = $6 RETURNING *", + es.schedule, + es.script_path, + es.is_flow, + es.args, + path, + w_id, + ) + .fetch_one(&mut tx) + .await?; + + 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())), + Some(("script_path", es.script_path.as_str())), + ] + .into_iter() + .flatten() + .collect(), + ), + ) + .await?; + + tx.commit().await?; + Ok(path.to_string()) +} + +async fn list_schedule( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(pagination): Query, +) -> JsonResult> { + let (per_page, offset) = crate::utils::paginate(pagination); + let mut tx = user_db.begin(&authed).await?; + + 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)) +} + +pub async fn get_schedule_opt<'c>( + db: &mut Transaction<'c, Postgres>, + w_id: &str, + path: &str, +) -> Result> { + let schedule_opt = sqlx::query_as!( + Schedule, + "SELECT * FROM schedule WHERE path = $1 AND workspace_id = $2", + path, + w_id + ) + .fetch_optional(db) + .await?; + Ok(schedule_opt) +} +async fn get_schedule( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + + let schedule_o = get_schedule_opt(&mut tx, &w_id, path).await?; + let schedule = crate::utils::not_found_if_none(schedule_o, "Schedule", path)?; + tx.commit().await?; + Ok(Json(schedule)) +} + +#[derive(Deserialize)] +pub struct PreviewPayload { + pub schedule: String, + pub offset: Option, +} + +pub async fn preview_schedule( + Json(PreviewPayload { schedule, offset }): Json, +) -> JsonResult>> { + let schedule = + cron::Schedule::from_str(&schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; + let upcoming: Vec> = schedule + .upcoming(get_offset(offset)) + .take(10) + .map(|x| x.into()) + .collect(); + Ok(Json(upcoming)) +} + +fn get_offset(offset: Option) -> FixedOffset { + FixedOffset::west(offset.unwrap_or(0) * 60) +} + +#[derive(Deserialize)] +pub struct SetEnabled { + pub enabled: bool, +} + +pub async fn set_enabled( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + Json(SetEnabled { enabled }): Json, +) -> Result { + let path = path.to_path(); + let mut tx = user_db.begin(&authed).await?; + + let schedule_o = sqlx::query_as!( + Schedule, + "UPDATE schedule SET enabled = $1 WHERE path = $2 AND workspace_id = $3 RETURNING *", + enabled, + path, + w_id + ) + .fetch_optional(&mut tx) + .await?; + + let schedule = crate::utils::not_found_if_none(schedule_o, "Schedule", path)?; + + clear_schedule(&mut tx, path).await?; + + if enabled { + tx = push_scheduled_job(tx, schedule).await?; + } + audit_log( + &mut tx, + &authed.username, + "schedule.setenabled", + ActionKind::Update, + &w_id, + Some(path), + Some([("enabled", enabled.to_string().as_ref())].into()), + ) + .await?; + tx.commit().await?; + Ok(format!( + "succesfully updated schedule at path {} to status {}", + path, enabled + )) +} + +fn schedule_to_user(path: &str) -> String { + format!("schedule-{}", path.replace('/', "-")) +} diff --git a/backend/src/scripts.rs b/backend/src/scripts.rs new file mode 100644 index 0000000000..08cf3288b3 --- /dev/null +++ b/backend/src/scripts.rs @@ -0,0 +1,614 @@ +/* +* Author & Copyright: Ruben Fiszel 2021 + * This file and its contents are licensed under the AGPL License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use serde::Deserializer; +use sql_builder::prelude::*; + +use crate::{ + audit::{audit_log, ActionKind}, + db::{UserDB, DB}, + error::{Error, JsonResult, Result}, + jobs, parser, + users::{owner_to_token_owner, truncate_token, Authed, Tokened}, + utils::{require_admin, Pagination, StripPath}, +}; +use axum::{ + extract::{Extension, Path, Query}, + routing::{get, post}, + Json, Router, +}; +use hyper::StatusCode; +use serde::{de::Error as _, ser::SerializeSeq, Deserialize, Serialize}; +use serde_json::{json, to_string_pretty}; +use sql_builder::SqlBuilder; +use sqlx::{FromRow, Postgres, Transaction}; +use std::{ + collections::hash_map::DefaultHasher, + fmt::Display, + hash::{Hash, Hasher}, +}; + +const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20; + +pub fn global_service() -> Router { + Router::new().route("/tojsonschema", post(parse_code_to_jsonschema)) +} + +pub fn workspaced_service() -> Router { + Router::new() + .route("/list", get(list_scripts)) + .route("/create", post(create_script)) + .route("/archive/p/*path", post(archive_script_by_path)) + .route("/get/p/*path", get(get_script_by_path)) + .route("/archive/h/:hash", post(archive_script_by_hash)) + .route("/delete/h/:hash", post(delete_script_by_hash)) + .route("/get/h/:hash", get(get_script_by_hash)) + .route("/deployment_status/h/:hash", get(get_deployment_status)) +} + +#[derive(sqlx::Type, PartialEq, Debug, Hash, Clone, Copy)] +#[sqlx(transparent)] +pub struct ScriptHash(pub i64); + +#[derive(sqlx::Type, PartialEq)] +#[sqlx(transparent)] +pub struct ScriptHashes(Vec); + +impl Display for ScriptHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", to_hex_string(&self.0)) + } +} +impl Serialize for ScriptHash { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(to_hex_string(&self.0).as_str()) + } +} +impl<'de> Deserialize<'de> for ScriptHash { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let i = to_i64(&s).map_err(|e| D::Error::custom(format!("{}", e)))?; + Ok(ScriptHash(i)) + } +} + +impl Serialize for ScriptHashes { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for element in &self.0 { + seq.serialize_element(&ScriptHash(*element))?; + } + seq.end() + } +} + +#[derive(FromRow, Serialize)] +pub struct Script { + pub workspace_id: String, + pub hash: ScriptHash, + pub path: String, + pub parent_hashes: Option, + pub summary: String, + pub description: String, + pub content: String, + pub created_by: String, + pub created_at: chrono::DateTime, + pub archived: bool, + pub schema: Option, + pub deleted: bool, + pub is_template: bool, + pub extra_perms: serde_json::Value, + pub lock: Option, + pub lock_error_logs: Option, +} + +#[derive(Serialize, Deserialize, sqlx::Type, Debug)] +#[sqlx(transparent)] +#[serde(transparent)] +pub struct Schema(pub serde_json::Value); + +impl Hash for Schema { + fn hash(&self, state: &mut H) { + if let Ok(s) = to_string_pretty(&self.0) { + s.hash(state); + } + } +} + +#[derive(Serialize, Deserialize, Hash)] +pub struct NewScript { + pub path: String, + pub parent_hash: Option, + pub summary: String, + pub description: String, + pub content: String, + pub schema: Option, + pub is_template: Option, + pub lock: Option>, +} + +#[derive(Deserialize)] +pub struct ListScriptQuery { + pub path_start: Option, + pub path_exact: Option, + pub created_by: Option, + pub first_parent_hash: Option, + pub last_parent_hash: Option, + pub parent_hash: Option, + pub show_archived: Option, + pub order_by: Option, + pub order_desc: Option, + pub is_template: Option, +} + +async fn list_scripts( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(pagination): Query, + Query(lq): Query, +) -> JsonResult> { + let (per_page, offset) = crate::utils::paginate(pagination); + + let mut sqlb = SqlBuilder::select_from("script as o") + .fields(&[ + "workspace_id", + "hash", + "path", + "array_remove(array[parent_hashes[1]], NULL) as parent_hashes", + "summary", + "description", + "'' as content", + "created_by", + "created_at", + "archived", + "schema", + "deleted", + "is_template", + "extra_perms", + "null as lock", + "CASE WHEN lock_error_logs IS NOT NULL THEN 'error' ELSE null END as lock_error_logs", + ]) + .order_by("created_at", lq.order_desc.unwrap_or(true)) + .and_where("workspace_id = ? OR workspace_id = 'starter'".bind(&w_id)) + .offset(offset) + .limit(per_page) + .clone(); + + if lq.show_archived.unwrap_or(false) { + sqlb.and_where_eq( + "created_at", + "(select max(created_at) from script where o.path = path + AND (workspace_id = $1 OR workspace_id = 'starter'))", + ); + } else { + sqlb.and_where_eq("archived", false); + } + if let Some(ps) = &lq.path_start { + sqlb.and_where_like_left("path", "?".bind(ps)); + } + if let Some(p) = &lq.path_exact { + sqlb.and_where_eq("path", "?".bind(p)); + } + if let Some(cb) = &lq.created_by { + sqlb.and_where_eq("created_by", "?".bind(cb)); + } + if let Some(ph) = &lq.first_parent_hash { + sqlb.and_where_eq("parent_hashes[1]", &ph.0); + } + if let Some(ph) = &lq.last_parent_hash { + sqlb.and_where_eq("parent_hashes[array_upper(parent_hashes, 1)]", &ph.0); + } + if let Some(ph) = &lq.parent_hash { + sqlb.and_where_eq("any(parent_hashes)", &ph.0); + } + if let Some(it) = &lq.is_template { + sqlb.and_where_eq("is_template", it); + } + + 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::<_, Script>(&sql).fetch_all(&mut tx).await?; + tx.commit().await?; + Ok(Json(rows)) +} + +fn hash_script(ns: &NewScript) -> i64 { + let mut dh = DefaultHasher::new(); + ns.hash(&mut dh); + dh.finish() as i64 +} +async fn create_script( + authed: Authed, + Tokened { token }: Tokened, + Extension(user_db): Extension, + Path(w_id): Path, + Json(ns): Json, +) -> Result<(StatusCode, String)> { + let hash = ScriptHash(hash_script(&ns)); + let mut tx = user_db.begin(&authed).await?; + + if sqlx::query_scalar!( + "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2", + hash.0, + &w_id + ) + .fetch_optional(&mut tx) + .await? + .is_some() + { + return Err(Error::BadRequest( + "A script with same hash (hence same path, description, summary, content) already \ + exists!" + .to_owned(), + )); + }; + + let clashing_script = sqlx::query_as::<_, Script>( + "SELECT * FROM script WHERE path = $1 AND archived = false AND workspace_id = $2", + ) + .bind(&ns.path) + .bind(&w_id) + .fetch_optional(&mut tx) + .await?; + + let parent_hashes_and_perms: Option<(Vec, 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(), + )); + }; + + 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 \ + lineage must be linear: no 2 scripts can have the same parent", + ScriptHash(clashing_hash) + ))); + }; + + 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 = v + .into_iter() + .take(MAX_HASH_HISTORY_LENGTH_STORED - 1) + .collect(); + v.insert(0, p_hash.0); + v + }; + let r: Result, 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 + } + }?; + + let p_hashes = parent_hashes_and_perms.as_ref().map(|v| &v.0[..]); + let extra_perms = parent_hashes_and_perms + .as_ref() + .map(|v| v.1.clone()) + .unwrap_or(json!({})); + + //::text::json is to ensure we use serde_json with preserve order + sqlx::query!( + "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, \ + created_by, schema, is_template, extra_perms, lock) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12)", + &w_id, + &hash.0, + ns.path, + p_hashes, + ns.summary, + ns.description, + &ns.content, + &authed.username, + ns.schema.and_then(|x| serde_json::to_string(&x.0).ok()), + ns.is_template.unwrap_or(false), + extra_perms, + ns.lock.as_ref().map(|x| x.join("\n")) + ) + .execute(&mut tx) + .await?; + + let mut tx = if ns.lock.is_none() { + let dependencies = parser::parse_imports(&ns.content)?; + let (_, tx) = jobs::push( + tx, + &w_id, + jobs::JobPayload::Dependencies { hash, dependencies }, + None, + &authed.username, + owner_to_token_owner(&authed.username, false), + None, + None, + None, + false, + ) + .await?; + tx + } else { + tx + }; + + if p_hashes.is_some() && !p_hashes.unwrap().is_empty() { + audit_log( + &mut tx, + &authed.username, + "scripts.update", + ActionKind::Update, + &w_id, + Some(&ns.path), + Some( + [ + ("hash", hash.to_string().as_str()), + ("token", &truncate_token(&token)), + ] + .into(), + ), + ) + .await?; + } else { + audit_log( + &mut tx, + &authed.username, + "scripts.create", + ActionKind::Create, + &w_id, + Some(&ns.path), + Some( + [ + ("workspace", w_id.as_str()), + ("hash", hash.to_string().as_str()), + ("token", &truncate_token(&token)), + ] + .into(), + ), + ) + .await?; + } + + tx.commit().await?; + + Ok((StatusCode::CREATED, format!("{}", hash))) +} + +async fn get_script_by_path( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult + +
+ +
+ + +
+
+ + diff --git a/frontend/src/routes/audit_logs.svelte b/frontend/src/routes/audit_logs.svelte new file mode 100644 index 0000000000..66ad2a9c94 --- /dev/null +++ b/frontend/src/routes/audit_logs.svelte @@ -0,0 +1,193 @@ + + + + + + +
+ +
+ + { + gotoPage((pageIndex ?? 1) + 1); + }} + on:previous={() => { + gotoPage((pageIndex ?? 1) - 1); + }} + currentPage={pageIndex} + > + + id + timestamp + + op kind + username + operation name + resource + parameters + + + {#if logs} + {#each logs as { id, timestamp, username, operation, action_kind, resource, parameters }} + + {id} + +
+ {displayDate(timestamp)} +
+ + + + + +
+ {username} +
+
{operation}
+ {resource} + + {#if parameters} +
+
{JSON.stringify(parameters, null)}
+
+ {/if} + + + {/each} + {/if} + +
+ + {#if logs?.length == 0} + + {/if} +
+ + diff --git a/frontend/src/routes/components/ArgInfo.svelte b/frontend/src/routes/components/ArgInfo.svelte new file mode 100644 index 0000000000..f5433b6331 --- /dev/null +++ b/frontend/src/routes/components/ArgInfo.svelte @@ -0,0 +1,53 @@ + + + + {@html github} + + + +
{resource.path}
+
+ +
+
+ +{#if value == ''} + {''}The arg was none and the default argument of the script is a function call, hence the actual + value used for this arg was the output of the script's function call for this arg +{:else if isString(value) && value.startsWith('$res:')} + {:else if asJson.length > 40} + {truncate(asJson, 40)}{asJson} +{:else} + {asJson} +{/if} diff --git a/frontend/src/routes/components/ArgInput.svelte b/frontend/src/routes/components/ArgInput.svelte new file mode 100644 index 0000000000..299c1cc0e2 --- /dev/null +++ b/frontend/src/routes/components/ArgInput.svelte @@ -0,0 +1,275 @@ + + +
+
+ {#if displayHeader} + + {/if} + {#if editableSchema} +
+ { + seeEditable = !seeEditable; + }} + >Customize argument + + {#if seeEditable} +
+