Compare commits
2 Commits
af5175b96a
...
62307e8c65
| Author | SHA1 | Date | |
|---|---|---|---|
| 62307e8c65 | |||
| 2ebb03b868 |
0
.codex_write_test
Normal file
0
.codex_write_test
Normal file
@@ -1,7 +1,27 @@
|
|||||||
name: Publish Images And Chart
|
name: Publish Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
target_arch:
|
||||||
|
description: Architecture to publish
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
default: all
|
||||||
|
target_image:
|
||||||
|
description: Image to publish
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- api
|
||||||
|
- executor
|
||||||
|
- notifier
|
||||||
|
- agent
|
||||||
|
- web
|
||||||
|
default: all
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -13,21 +33,26 @@ env:
|
|||||||
REGISTRY_HOST: ${{ vars.CLUSTER_GITEA_HOST }}
|
REGISTRY_HOST: ${{ vars.CLUSTER_GITEA_HOST }}
|
||||||
REGISTRY_NAMESPACE: ${{ vars.CONTAINER_REGISTRY_NAMESPACE }}
|
REGISTRY_NAMESPACE: ${{ vars.CONTAINER_REGISTRY_NAMESPACE }}
|
||||||
REGISTRY_PLAIN_HTTP: ${{ vars.CONTAINER_REGISTRY_INSECURE }}
|
REGISTRY_PLAIN_HTTP: ${{ vars.CONTAINER_REGISTRY_INSECURE }}
|
||||||
CHART_NAME: attune
|
ARTIFACT_REPOSITORY: attune-build-artifacts
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
CARGO_NET_RETRY: 10
|
||||||
|
RUSTUP_MAX_RETRIES: 10
|
||||||
|
RUST_MIN_STACK: 67108864
|
||||||
|
SQLX_OFFLINE: true
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
metadata:
|
metadata:
|
||||||
name: Resolve Publish Metadata
|
name: Resolve Publish Metadata
|
||||||
runs-on: ubuntu-latest
|
runs-on: build-amd64
|
||||||
outputs:
|
outputs:
|
||||||
registry: ${{ steps.meta.outputs.registry }}
|
registry: ${{ steps.meta.outputs.registry }}
|
||||||
namespace: ${{ steps.meta.outputs.namespace }}
|
namespace: ${{ steps.meta.outputs.namespace }}
|
||||||
registry_plain_http: ${{ steps.meta.outputs.registry_plain_http }}
|
registry_plain_http: ${{ steps.meta.outputs.registry_plain_http }}
|
||||||
image_tag: ${{ steps.meta.outputs.image_tag }}
|
image_tag: ${{ steps.meta.outputs.image_tag }}
|
||||||
image_tags: ${{ steps.meta.outputs.image_tags }}
|
image_tags: ${{ steps.meta.outputs.image_tags }}
|
||||||
chart_version: ${{ steps.meta.outputs.chart_version }}
|
artifact_ref_base: ${{ steps.meta.outputs.artifact_ref_base }}
|
||||||
app_version: ${{ steps.meta.outputs.app_version }}
|
|
||||||
release_channel: ${{ steps.meta.outputs.release_channel }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Resolve tags and registry paths
|
- name: Resolve tags and registry paths
|
||||||
id: meta
|
id: meta
|
||||||
@@ -78,97 +103,400 @@ jobs:
|
|||||||
if [ "$ref_type" = "tag" ] && printf '%s' "$ref_name" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-.].*)?$'; then
|
if [ "$ref_type" = "tag" ] && printf '%s' "$ref_name" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-.].*)?$'; then
|
||||||
version="${ref_name#v}"
|
version="${ref_name#v}"
|
||||||
image_tags="${version},latest,sha-${short_sha}"
|
image_tags="${version},latest,sha-${short_sha}"
|
||||||
chart_version="$version"
|
|
||||||
release_channel="release"
|
|
||||||
else
|
else
|
||||||
version="sha-${short_sha}"
|
version="sha-${short_sha}"
|
||||||
image_tags="edge,sha-${short_sha}"
|
image_tags="edge,sha-${short_sha}"
|
||||||
chart_version="0.0.0-dev.${{ github.run_number }}"
|
|
||||||
release_channel="edge"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
artifact_ref_base="${registry}/${namespace}/${ARTIFACT_REPOSITORY}"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "registry=$registry"
|
echo "registry=$registry"
|
||||||
echo "namespace=$namespace"
|
echo "namespace=$namespace"
|
||||||
echo "registry_plain_http=$registry_plain_http"
|
echo "registry_plain_http=$registry_plain_http"
|
||||||
echo "image_tag=$version"
|
echo "image_tag=$version"
|
||||||
echo "image_tags=$image_tags"
|
echo "image_tags=$image_tags"
|
||||||
echo "chart_version=$chart_version"
|
echo "artifact_ref_base=$artifact_ref_base"
|
||||||
echo "app_version=$version"
|
|
||||||
echo "release_channel=$release_channel"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
publish-images:
|
build-rust-bundles:
|
||||||
name: Publish ${{ matrix.image.name }}
|
name: Build Rust Bundles (${{ matrix.arch }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.runner_label }}
|
||||||
needs: metadata
|
needs: metadata
|
||||||
|
if: |
|
||||||
|
github.event_name != 'workflow_dispatch' ||
|
||||||
|
inputs.target_arch == 'all' ||
|
||||||
|
inputs.target_arch == matrix.arch
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
image:
|
include:
|
||||||
- name: api
|
- arch: amd64
|
||||||
repository: attune-api
|
runner_label: build-amd64
|
||||||
dockerfile: docker/Dockerfile.optimized
|
musl_target: x86_64-unknown-linux-musl
|
||||||
context: .
|
- arch: arm64
|
||||||
target: ""
|
runner_label: build-arm64
|
||||||
build_args: |
|
musl_target: aarch64-unknown-linux-musl
|
||||||
SERVICE=api
|
steps:
|
||||||
- name: executor
|
- name: Checkout
|
||||||
repository: attune-executor
|
uses: actions/checkout@v4
|
||||||
dockerfile: docker/Dockerfile.optimized
|
|
||||||
context: .
|
- name: Cache Rust toolchain
|
||||||
target: ""
|
uses: actions/cache@v4
|
||||||
build_args: |
|
with:
|
||||||
SERVICE=executor
|
path: |
|
||||||
- name: notifier
|
~/.rustup/toolchains
|
||||||
repository: attune-notifier
|
~/.rustup/update-hashes
|
||||||
dockerfile: docker/Dockerfile.optimized
|
key: rustup-publish-${{ runner.os }}-${{ matrix.arch }}-stable-v1
|
||||||
context: .
|
restore-keys: |
|
||||||
target: ""
|
rustup-${{ runner.os }}-${{ matrix.arch }}-stable-v1
|
||||||
build_args: |
|
rustup-${{ runner.os }}-stable-v1
|
||||||
SERVICE=notifier
|
rustup-
|
||||||
- name: sensor
|
|
||||||
repository: attune-sensor
|
- name: Setup Rust
|
||||||
dockerfile: docker/Dockerfile.sensor.optimized
|
uses: dtolnay/rust-toolchain@stable
|
||||||
context: .
|
with:
|
||||||
target: sensor-full
|
targets: ${{ matrix.musl_target }}
|
||||||
build_args: ""
|
|
||||||
- name: worker
|
- name: Cache Cargo registry + index
|
||||||
repository: attune-worker
|
uses: actions/cache@v4
|
||||||
dockerfile: docker/Dockerfile.worker.optimized
|
with:
|
||||||
context: .
|
path: |
|
||||||
target: worker-full
|
~/.cargo/registry/index
|
||||||
build_args: ""
|
~/.cargo/registry/cache
|
||||||
- name: web
|
~/.cargo/git/db
|
||||||
repository: attune-web
|
key: cargo-registry-publish-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
dockerfile: docker/Dockerfile.web
|
restore-keys: |
|
||||||
context: .
|
cargo-registry-publish-${{ matrix.arch }}-
|
||||||
target: ""
|
cargo-registry-
|
||||||
build_args: ""
|
|
||||||
- name: migrations
|
- name: Cache Cargo build artifacts
|
||||||
repository: attune-migrations
|
uses: actions/cache@v4
|
||||||
dockerfile: docker/Dockerfile.migrations
|
with:
|
||||||
context: .
|
path: target
|
||||||
target: ""
|
key: cargo-publish-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }}
|
||||||
build_args: ""
|
restore-keys: |
|
||||||
- name: init-user
|
cargo-publish-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}-
|
||||||
repository: attune-init-user
|
cargo-publish-${{ matrix.arch }}-
|
||||||
dockerfile: docker/Dockerfile.init-user
|
|
||||||
context: .
|
- name: Install native build dependencies
|
||||||
target: ""
|
shell: bash
|
||||||
build_args: ""
|
run: |
|
||||||
- name: init-packs
|
set -euo pipefail
|
||||||
repository: attune-init-packs
|
apt-get update
|
||||||
dockerfile: docker/Dockerfile.init-packs
|
apt-get install -y pkg-config libssl-dev musl-tools file
|
||||||
context: .
|
|
||||||
target: ""
|
- name: Build release binaries
|
||||||
build_args: ""
|
shell: bash
|
||||||
- name: agent
|
run: |
|
||||||
repository: attune-agent
|
set -euo pipefail
|
||||||
dockerfile: docker/Dockerfile.agent
|
cargo build --release \
|
||||||
context: .
|
--bin attune-api \
|
||||||
target: agent-init
|
--bin attune-executor \
|
||||||
build_args: ""
|
--bin attune-notifier
|
||||||
|
|
||||||
|
- name: Build static agent binaries
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cargo build --release \
|
||||||
|
--target "${{ matrix.musl_target }}" \
|
||||||
|
--bin attune-agent \
|
||||||
|
--bin attune-sensor-agent
|
||||||
|
|
||||||
|
- name: Assemble binary bundle
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
bundle_root="dist/bundle/${{ matrix.arch }}"
|
||||||
|
mkdir -p "$bundle_root/bin" "$bundle_root/agent"
|
||||||
|
|
||||||
|
cp target/release/attune-api "$bundle_root/bin/"
|
||||||
|
cp target/release/attune-executor "$bundle_root/bin/"
|
||||||
|
cp target/release/attune-notifier "$bundle_root/bin/"
|
||||||
|
cp target/${{ matrix.musl_target }}/release/attune-agent "$bundle_root/agent/"
|
||||||
|
cp target/${{ matrix.musl_target }}/release/attune-sensor-agent "$bundle_root/agent/"
|
||||||
|
|
||||||
|
cat > "$bundle_root/metadata.json" <<EOF
|
||||||
|
{
|
||||||
|
"git_sha": "${{ github.sha }}",
|
||||||
|
"ref": "${{ github.ref }}",
|
||||||
|
"arch": "${{ matrix.arch }}",
|
||||||
|
"image_tag": "${{ needs.metadata.outputs.image_tag }}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
tar -C dist/bundle/${{ matrix.arch }} -czf "dist/attune-binaries-${{ matrix.arch }}.tar.gz" .
|
||||||
|
|
||||||
|
- name: Setup ORAS
|
||||||
|
uses: oras-project/setup-oras@v1
|
||||||
|
|
||||||
|
- name: Log in to registry for artifacts
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
|
||||||
|
GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
registry_username="${REGISTRY_USERNAME:-${{ github.actor }}}"
|
||||||
|
registry_password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"
|
||||||
|
login_args=()
|
||||||
|
|
||||||
|
if [ -z "$registry_password" ]; then
|
||||||
|
echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
||||||
|
login_args+=(--plain-http)
|
||||||
|
fi
|
||||||
|
|
||||||
|
oras login "${{ needs.metadata.outputs.registry }}" \
|
||||||
|
"${login_args[@]}" \
|
||||||
|
--username "$registry_username" \
|
||||||
|
--password "$registry_password"
|
||||||
|
|
||||||
|
- name: Push binary bundle artifact
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
push_args=()
|
||||||
|
|
||||||
|
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
||||||
|
push_args+=(--plain-http)
|
||||||
|
fi
|
||||||
|
|
||||||
|
oras push \
|
||||||
|
"${push_args[@]}" \
|
||||||
|
"${{ needs.metadata.outputs.artifact_ref_base }}:rust-binaries-${{ needs.metadata.outputs.image_tag }}-${{ matrix.arch }}" \
|
||||||
|
--artifact-type application/vnd.attune.rust-binaries.v1 \
|
||||||
|
"dist/attune-binaries-${{ matrix.arch }}.tar.gz:application/vnd.attune.rust-binaries.layer.v1.tar+gzip"
|
||||||
|
|
||||||
|
publish-rust-images:
|
||||||
|
name: Publish ${{ matrix.image.name }} (${{ matrix.arch }})
|
||||||
|
runs-on: ${{ matrix.runner_label }}
|
||||||
|
needs:
|
||||||
|
- metadata
|
||||||
|
- build-rust-bundles
|
||||||
|
if: |
|
||||||
|
(github.event_name != 'workflow_dispatch' ||
|
||||||
|
inputs.target_arch == 'all' ||
|
||||||
|
inputs.target_arch == matrix.arch) &&
|
||||||
|
(github.event_name != 'workflow_dispatch' ||
|
||||||
|
inputs.target_image == 'all' ||
|
||||||
|
inputs.target_image == matrix.image.name)
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: amd64
|
||||||
|
runner_label: build-amd64
|
||||||
|
platform: linux/amd64
|
||||||
|
image:
|
||||||
|
name: api
|
||||||
|
repository: attune-api
|
||||||
|
source_path: bin/attune-api
|
||||||
|
dockerfile: docker/Dockerfile.runtime
|
||||||
|
- arch: amd64
|
||||||
|
runner_label: build-amd64
|
||||||
|
platform: linux/amd64
|
||||||
|
image:
|
||||||
|
name: executor
|
||||||
|
repository: attune-executor
|
||||||
|
source_path: bin/attune-executor
|
||||||
|
dockerfile: docker/Dockerfile.runtime
|
||||||
|
- arch: amd64
|
||||||
|
runner_label: build-amd64
|
||||||
|
platform: linux/amd64
|
||||||
|
image:
|
||||||
|
name: notifier
|
||||||
|
repository: attune-notifier
|
||||||
|
source_path: bin/attune-notifier
|
||||||
|
dockerfile: docker/Dockerfile.runtime
|
||||||
|
- arch: amd64
|
||||||
|
runner_label: build-amd64
|
||||||
|
platform: linux/amd64
|
||||||
|
image:
|
||||||
|
name: agent
|
||||||
|
repository: attune-agent
|
||||||
|
source_path: agent/attune-agent
|
||||||
|
dockerfile: docker/Dockerfile.agent-package
|
||||||
|
- arch: arm64
|
||||||
|
runner_label: build-arm64
|
||||||
|
platform: linux/arm64
|
||||||
|
image:
|
||||||
|
name: api
|
||||||
|
repository: attune-api
|
||||||
|
source_path: bin/attune-api
|
||||||
|
dockerfile: docker/Dockerfile.runtime
|
||||||
|
- arch: arm64
|
||||||
|
runner_label: build-arm64
|
||||||
|
platform: linux/arm64
|
||||||
|
image:
|
||||||
|
name: executor
|
||||||
|
repository: attune-executor
|
||||||
|
source_path: bin/attune-executor
|
||||||
|
dockerfile: docker/Dockerfile.runtime
|
||||||
|
- arch: arm64
|
||||||
|
runner_label: build-arm64
|
||||||
|
platform: linux/arm64
|
||||||
|
image:
|
||||||
|
name: notifier
|
||||||
|
repository: attune-notifier
|
||||||
|
source_path: bin/attune-notifier
|
||||||
|
dockerfile: docker/Dockerfile.runtime
|
||||||
|
- arch: arm64
|
||||||
|
runner_label: build-arm64
|
||||||
|
platform: linux/arm64
|
||||||
|
image:
|
||||||
|
name: agent
|
||||||
|
repository: attune-agent
|
||||||
|
source_path: agent/attune-agent
|
||||||
|
dockerfile: docker/Dockerfile.agent-package
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup ORAS
|
||||||
|
uses: oras-project/setup-oras@v1
|
||||||
|
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
if: needs.metadata.outputs.registry_plain_http != 'true'
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Setup Docker Buildx For Plain HTTP Registry
|
||||||
|
if: needs.metadata.outputs.registry_plain_http == 'true'
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
buildkitd-config-inline: |
|
||||||
|
[registry."${{ needs.metadata.outputs.registry }}"]
|
||||||
|
http = true
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
|
||||||
|
GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
registry_username="${REGISTRY_USERNAME:-${{ github.actor }}}"
|
||||||
|
registry_password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"
|
||||||
|
|
||||||
|
if [ -z "$registry_password" ]; then
|
||||||
|
echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.docker"
|
||||||
|
auth="$(printf '%s:%s' "$registry_username" "$registry_password" | base64 | tr -d '\n')"
|
||||||
|
|
||||||
|
cat > "$HOME/.docker/config.json" <<EOF
|
||||||
|
{
|
||||||
|
"auths": {
|
||||||
|
"${{ needs.metadata.outputs.registry }}": {
|
||||||
|
"auth": "${auth}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
oras_login_args=()
|
||||||
|
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
||||||
|
oras_login_args+=(--plain-http)
|
||||||
|
fi
|
||||||
|
|
||||||
|
oras login "${{ needs.metadata.outputs.registry }}" \
|
||||||
|
"${oras_login_args[@]}" \
|
||||||
|
--username "$registry_username" \
|
||||||
|
--password "$registry_password"
|
||||||
|
|
||||||
|
- name: Pull binary bundle
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
pull_args=()
|
||||||
|
|
||||||
|
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
||||||
|
pull_args+=(--plain-http)
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p dist/artifact
|
||||||
|
cd dist/artifact
|
||||||
|
|
||||||
|
oras pull \
|
||||||
|
"${pull_args[@]}" \
|
||||||
|
"${{ needs.metadata.outputs.artifact_ref_base }}:rust-binaries-${{ needs.metadata.outputs.image_tag }}-${{ matrix.arch }}"
|
||||||
|
|
||||||
|
tar -xzf "attune-binaries-${{ matrix.arch }}.tar.gz"
|
||||||
|
|
||||||
|
- name: Prepare packaging context
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
rm -rf dist/image
|
||||||
|
mkdir -p dist/image
|
||||||
|
|
||||||
|
case "${{ matrix.image.name }}" in
|
||||||
|
api|executor|notifier)
|
||||||
|
cp "dist/artifact/${{ matrix.image.source_path }}" dist/attune-service-binary
|
||||||
|
;;
|
||||||
|
agent)
|
||||||
|
cp dist/artifact/agent/attune-agent dist/attune-agent
|
||||||
|
cp dist/artifact/agent/attune-sensor-agent dist/attune-sensor-agent
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported image: ${{ matrix.image.name }}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Push architecture image
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
image_ref="${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/${{ matrix.image.repository }}:${{ needs.metadata.outputs.image_tag }}-${{ matrix.arch }}"
|
||||||
|
|
||||||
|
build_cmd=(
|
||||||
|
docker buildx build
|
||||||
|
.
|
||||||
|
--platform "${{ matrix.platform }}"
|
||||||
|
--file "${{ matrix.image.dockerfile }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
||||||
|
build_cmd+=(--output "type=image,\"name=${image_ref}\",push=true,registry.insecure=true")
|
||||||
|
else
|
||||||
|
build_cmd+=(--tag "$image_ref" --push)
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${build_cmd[@]}"
|
||||||
|
|
||||||
|
publish-web-images:
|
||||||
|
name: Publish web (${{ matrix.arch }})
|
||||||
|
runs-on: ${{ matrix.runner_label }}
|
||||||
|
needs: metadata
|
||||||
|
if: |
|
||||||
|
(github.event_name != 'workflow_dispatch' ||
|
||||||
|
inputs.target_arch == 'all' ||
|
||||||
|
inputs.target_arch == matrix.arch) &&
|
||||||
|
(github.event_name != 'workflow_dispatch' ||
|
||||||
|
inputs.target_image == 'all' ||
|
||||||
|
inputs.target_image == 'web')
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: amd64
|
||||||
|
runner_label: build-amd64
|
||||||
|
platform: linux/amd64
|
||||||
|
- arch: arm64
|
||||||
|
runner_label: build-arm64
|
||||||
|
platform: linux/arm64
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -196,7 +524,6 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
username="${REGISTRY_USERNAME:-${{ github.actor }}}"
|
username="${REGISTRY_USERNAME:-${{ github.actor }}}"
|
||||||
password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"
|
password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"
|
||||||
registry="${{ needs.metadata.outputs.registry }}"
|
|
||||||
|
|
||||||
if [ -z "$password" ]; then
|
if [ -z "$password" ]; then
|
||||||
echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
|
echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
|
||||||
@@ -209,81 +536,56 @@ jobs:
|
|||||||
cat > "$HOME/.docker/config.json" <<EOF
|
cat > "$HOME/.docker/config.json" <<EOF
|
||||||
{
|
{
|
||||||
"auths": {
|
"auths": {
|
||||||
"${registry}": {
|
"${{ needs.metadata.outputs.registry }}": {
|
||||||
"auth": "${auth}"
|
"auth": "${auth}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Prepare image tags
|
- name: Push architecture image
|
||||||
id: tags
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
image_ref_base="${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/${{ matrix.image.repository }}"
|
|
||||||
tag_lines=""
|
|
||||||
IFS=',' read -ra tags <<< "${{ needs.metadata.outputs.image_tags }}"
|
|
||||||
for tag in "${tags[@]}"; do
|
|
||||||
tag_lines="${tag_lines}${image_ref_base}:${tag}"$'\n'
|
|
||||||
done
|
|
||||||
printf 'tags<<EOF\n%sEOF\n' "$tag_lines" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Build and push image
|
image_ref="${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/attune-web:${{ needs.metadata.outputs.image_tag }}-${{ matrix.arch }}"
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
image_names_csv=""
|
|
||||||
build_cmd=(
|
build_cmd=(
|
||||||
docker buildx build
|
docker buildx build
|
||||||
"${{ matrix.image.context }}"
|
.
|
||||||
--file "${{ matrix.image.dockerfile }}"
|
--platform "${{ matrix.platform }}"
|
||||||
|
--file docker/Dockerfile.web
|
||||||
)
|
)
|
||||||
|
|
||||||
if [ -n "${{ matrix.image.target }}" ]; then
|
|
||||||
build_cmd+=(--target "${{ matrix.image.target }}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS= read -r tag; do
|
|
||||||
if [ -n "$tag" ]; then
|
|
||||||
if [ -n "$image_names_csv" ]; then
|
|
||||||
image_names_csv="${image_names_csv},${tag}"
|
|
||||||
else
|
|
||||||
image_names_csv="${tag}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${{ needs.metadata.outputs.registry_plain_http }}" != "true" ]; then
|
|
||||||
build_cmd+=(--tag "$tag")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "${{ steps.tags.outputs.tags }}"
|
|
||||||
|
|
||||||
while IFS= read -r build_arg; do
|
|
||||||
[ -n "$build_arg" ] && build_cmd+=(--build-arg "$build_arg")
|
|
||||||
done <<< "${{ matrix.image.build_args }}"
|
|
||||||
|
|
||||||
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
||||||
build_cmd+=(--output "type=image,\"name=${image_names_csv}\",push=true,registry.insecure=true")
|
build_cmd+=(--output "type=image,\"name=${image_ref}\",push=true,registry.insecure=true")
|
||||||
else
|
else
|
||||||
build_cmd+=(--push)
|
build_cmd+=(--tag "$image_ref" --push)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
"${build_cmd[@]}"
|
"${build_cmd[@]}"
|
||||||
|
|
||||||
publish-chart:
|
publish-manifests:
|
||||||
name: Publish Helm Chart
|
name: Publish manifest ${{ matrix.repository }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: build-amd64
|
||||||
needs:
|
needs:
|
||||||
- metadata
|
- metadata
|
||||||
- publish-images
|
- publish-rust-images
|
||||||
|
- publish-web-images
|
||||||
|
if: |
|
||||||
|
github.event_name != 'workflow_dispatch' ||
|
||||||
|
(inputs.target_arch == 'all' && inputs.target_image == 'all')
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
repository:
|
||||||
|
- attune-api
|
||||||
|
- attune-executor
|
||||||
|
- attune-notifier
|
||||||
|
- attune-agent
|
||||||
|
- attune-web
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Configure OCI registry auth
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Helm
|
|
||||||
uses: azure/setup-helm@v4
|
|
||||||
|
|
||||||
- name: Log in to Gitea OCI registry
|
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
|
REGISTRY_USERNAME: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
|
||||||
@@ -291,43 +593,48 @@ jobs:
|
|||||||
GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
registry_username="${REGISTRY_USERNAME:-${{ github.actor }}}"
|
username="${REGISTRY_USERNAME:-${{ github.actor }}}"
|
||||||
registry_password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"
|
password="${REGISTRY_PASSWORD:-${GITHUB_TOKEN_FALLBACK:-}}"
|
||||||
login_args=()
|
|
||||||
|
|
||||||
if [ -z "$registry_password" ]; then
|
if [ -z "$password" ]; then
|
||||||
echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
|
echo "Set CONTAINER_REGISTRY_PASSWORD or enable GITHUB_TOKEN package writes"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
mkdir -p "$HOME/.docker"
|
||||||
login_args+=(--plain-http)
|
auth="$(printf '%s:%s' "$username" "$password" | base64 | tr -d '\n')"
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$registry_password" | helm registry login "${{ needs.metadata.outputs.registry }}" \
|
cat > "$HOME/.docker/config.json" <<EOF
|
||||||
--username "$registry_username" \
|
{
|
||||||
"${login_args[@]}" \
|
"auths": {
|
||||||
--password-stdin
|
"${{ needs.metadata.outputs.registry }}": {
|
||||||
|
"auth": "${auth}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Lint chart
|
- name: Publish manifest tags
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
helm lint charts/attune
|
set -euo pipefail
|
||||||
|
|
||||||
- name: Package chart
|
image_base="${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/${{ matrix.repository }}"
|
||||||
run: |
|
|
||||||
mkdir -p dist
|
|
||||||
helm package charts/attune \
|
|
||||||
--destination dist \
|
|
||||||
--version "${{ needs.metadata.outputs.chart_version }}" \
|
|
||||||
--app-version "${{ needs.metadata.outputs.app_version }}"
|
|
||||||
|
|
||||||
- name: Push chart to OCI registry
|
|
||||||
run: |
|
|
||||||
push_args=()
|
push_args=()
|
||||||
|
|
||||||
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
if [ "${{ needs.metadata.outputs.registry_plain_http }}" = "true" ]; then
|
||||||
push_args+=(--plain-http)
|
push_args+=(--insecure)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
helm push "dist/${CHART_NAME}-${{ needs.metadata.outputs.chart_version }}.tgz" \
|
IFS=',' read -ra tags <<< "${{ needs.metadata.outputs.image_tags }}"
|
||||||
"oci://${{ needs.metadata.outputs.registry }}/${{ needs.metadata.outputs.namespace }}/helm" \
|
for tag in "${tags[@]}"; do
|
||||||
"${push_args[@]}"
|
manifest_ref="${image_base}:${tag}"
|
||||||
|
amd64_ref="${image_base}:${{ needs.metadata.outputs.image_tag }}-amd64"
|
||||||
|
arm64_ref="${image_base}:${{ needs.metadata.outputs.image_tag }}-arm64"
|
||||||
|
|
||||||
|
docker manifest rm "$manifest_ref" >/dev/null 2>&1 || true
|
||||||
|
docker manifest create "$manifest_ref" "$amd64_ref" "$arm64_ref"
|
||||||
|
docker manifest annotate "$manifest_ref" "$amd64_ref" --os linux --arch amd64
|
||||||
|
docker manifest annotate "$manifest_ref" "$arm64_ref" --os linux --arch arm64
|
||||||
|
docker manifest push "${push_args[@]}" "$manifest_ref"
|
||||||
|
done
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -78,4 +78,5 @@ docker-compose.override.yml
|
|||||||
*.pid
|
*.pid
|
||||||
|
|
||||||
packs.examples/
|
packs.examples/
|
||||||
|
packs.external/
|
||||||
codex/
|
codex/
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
use attune_common::{
|
use attune_common::{
|
||||||
config::LdapConfig,
|
config::LdapConfig,
|
||||||
repositories::{
|
repositories::{
|
||||||
identity::{CreateIdentityInput, IdentityRepository, UpdateIdentityInput},
|
identity::{
|
||||||
|
CreateIdentityInput, IdentityRepository, IdentityRoleAssignmentRepository,
|
||||||
|
UpdateIdentityInput,
|
||||||
|
},
|
||||||
Create, Update,
|
Create, Update,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -63,6 +66,11 @@ pub async fn authenticate(
|
|||||||
|
|
||||||
// Upsert identity in DB and issue JWT tokens
|
// Upsert identity in DB and issue JWT tokens
|
||||||
let identity = upsert_identity(state, &claims).await?;
|
let identity = upsert_identity(state, &claims).await?;
|
||||||
|
if identity.frozen {
|
||||||
|
return Err(ApiError::Forbidden(
|
||||||
|
"Identity is frozen and cannot authenticate".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
|
let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
|
||||||
let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;
|
let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;
|
||||||
|
|
||||||
@@ -351,10 +359,13 @@ async fn upsert_identity(
|
|||||||
display_name,
|
display_name,
|
||||||
password_hash: None,
|
password_hash: None,
|
||||||
attributes: Some(attributes),
|
attributes: Some(attributes),
|
||||||
|
frozen: None,
|
||||||
};
|
};
|
||||||
IdentityRepository::update(&state.db, identity.id, updated)
|
let identity = IdentityRepository::update(&state.db, identity.id, updated)
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
.map_err(ApiError::from)?;
|
||||||
|
sync_roles(&state.db, identity.id, "ldap", &claims.groups).await?;
|
||||||
|
Ok(identity)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Avoid login collisions
|
// Avoid login collisions
|
||||||
@@ -363,7 +374,7 @@ async fn upsert_identity(
|
|||||||
None => desired_login,
|
None => desired_login,
|
||||||
};
|
};
|
||||||
|
|
||||||
IdentityRepository::create(
|
let identity = IdentityRepository::create(
|
||||||
&state.db,
|
&state.db,
|
||||||
CreateIdentityInput {
|
CreateIdentityInput {
|
||||||
login,
|
login,
|
||||||
@@ -373,11 +384,24 @@ async fn upsert_identity(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
.map_err(ApiError::from)?;
|
||||||
|
sync_roles(&state.db, identity.id, "ldap", &claims.groups).await?;
|
||||||
|
Ok(identity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn sync_roles(
|
||||||
|
db: &sqlx::PgPool,
|
||||||
|
identity_id: i64,
|
||||||
|
source: &str,
|
||||||
|
roles: &[String],
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
IdentityRoleAssignmentRepository::replace_managed_roles(db, identity_id, source, roles)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
/// Derive the login name from LDAP claims.
|
/// Derive the login name from LDAP claims.
|
||||||
fn derive_login(claims: &LdapUserClaims) -> String {
|
fn derive_login(claims: &LdapUserClaims) -> String {
|
||||||
claims
|
claims
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
use attune_common::{
|
use attune_common::{
|
||||||
config::OidcConfig,
|
config::OidcConfig,
|
||||||
repositories::{
|
repositories::{
|
||||||
identity::{CreateIdentityInput, IdentityRepository, UpdateIdentityInput},
|
identity::{
|
||||||
|
CreateIdentityInput, IdentityRepository, IdentityRoleAssignmentRepository,
|
||||||
|
UpdateIdentityInput,
|
||||||
|
},
|
||||||
Create, Update,
|
Create, Update,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -282,6 +285,11 @@ pub async fn handle_callback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let identity = upsert_identity(state, &oidc_claims).await?;
|
let identity = upsert_identity(state, &oidc_claims).await?;
|
||||||
|
if identity.frozen {
|
||||||
|
return Err(ApiError::Forbidden(
|
||||||
|
"Identity is frozen and cannot authenticate".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
|
let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
|
||||||
let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;
|
let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;
|
||||||
|
|
||||||
@@ -511,10 +519,13 @@ async fn upsert_identity(
|
|||||||
display_name,
|
display_name,
|
||||||
password_hash: None,
|
password_hash: None,
|
||||||
attributes: Some(attributes.clone()),
|
attributes: Some(attributes.clone()),
|
||||||
|
frozen: None,
|
||||||
};
|
};
|
||||||
IdentityRepository::update(&state.db, identity.id, updated)
|
let identity = IdentityRepository::update(&state.db, identity.id, updated)
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
.map_err(ApiError::from)?;
|
||||||
|
sync_roles(&state.db, identity.id, "oidc", &oidc_claims.groups).await?;
|
||||||
|
Ok(identity)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let login = match IdentityRepository::find_by_login(&state.db, &desired_login).await? {
|
let login = match IdentityRepository::find_by_login(&state.db, &desired_login).await? {
|
||||||
@@ -522,7 +533,7 @@ async fn upsert_identity(
|
|||||||
None => desired_login,
|
None => desired_login,
|
||||||
};
|
};
|
||||||
|
|
||||||
IdentityRepository::create(
|
let identity = IdentityRepository::create(
|
||||||
&state.db,
|
&state.db,
|
||||||
CreateIdentityInput {
|
CreateIdentityInput {
|
||||||
login,
|
login,
|
||||||
@@ -532,11 +543,24 @@ async fn upsert_identity(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(Into::into)
|
.map_err(ApiError::from)?;
|
||||||
|
sync_roles(&state.db, identity.id, "oidc", &oidc_claims.groups).await?;
|
||||||
|
Ok(identity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn sync_roles(
|
||||||
|
db: &sqlx::PgPool,
|
||||||
|
identity_id: i64,
|
||||||
|
source: &str,
|
||||||
|
roles: &[String],
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
IdentityRoleAssignmentRepository::replace_managed_roles(db, identity_id, source, roles)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
fn derive_login(oidc_claims: &OidcIdentityClaims) -> String {
|
fn derive_login(oidc_claims: &OidcIdentityClaims) -> String {
|
||||||
oidc_claims
|
oidc_claims
|
||||||
.email
|
.email
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::{
|
|||||||
use attune_common::{
|
use attune_common::{
|
||||||
rbac::{Action, AuthorizationContext, Grant, Resource},
|
rbac::{Action, AuthorizationContext, Grant, Resource},
|
||||||
repositories::{
|
repositories::{
|
||||||
identity::{IdentityRepository, PermissionSetRepository},
|
identity::{IdentityRepository, IdentityRoleAssignmentRepository, PermissionSetRepository},
|
||||||
FindById,
|
FindById,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -95,8 +95,16 @@ impl AuthorizationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn load_effective_grants(&self, identity_id: i64) -> Result<Vec<Grant>, ApiError> {
|
async fn load_effective_grants(&self, identity_id: i64) -> Result<Vec<Grant>, ApiError> {
|
||||||
let permission_sets =
|
let mut permission_sets =
|
||||||
PermissionSetRepository::find_by_identity(&self.db, identity_id).await?;
|
PermissionSetRepository::find_by_identity(&self.db, identity_id).await?;
|
||||||
|
let roles =
|
||||||
|
IdentityRoleAssignmentRepository::find_role_names_by_identity(&self.db, identity_id)
|
||||||
|
.await?;
|
||||||
|
let role_permission_sets = PermissionSetRepository::find_by_roles(&self.db, &roles).await?;
|
||||||
|
permission_sets.extend(role_permission_sets);
|
||||||
|
|
||||||
|
let mut seen_permission_sets = std::collections::HashSet::new();
|
||||||
|
permission_sets.retain(|permission_set| seen_permission_sets.insert(permission_set.id));
|
||||||
|
|
||||||
let mut grants = Vec::new();
|
let mut grants = Vec::new();
|
||||||
for permission_set in permission_sets {
|
for permission_set in permission_sets {
|
||||||
@@ -126,10 +134,6 @@ fn resource_name(resource: Resource) -> &'static str {
|
|||||||
Resource::Inquiries => "inquiries",
|
Resource::Inquiries => "inquiries",
|
||||||
Resource::Keys => "keys",
|
Resource::Keys => "keys",
|
||||||
Resource::Artifacts => "artifacts",
|
Resource::Artifacts => "artifacts",
|
||||||
Resource::Workflows => "workflows",
|
|
||||||
Resource::Webhooks => "webhooks",
|
|
||||||
Resource::Analytics => "analytics",
|
|
||||||
Resource::History => "history",
|
|
||||||
Resource::Identities => "identities",
|
Resource::Identities => "identities",
|
||||||
Resource::Permissions => "permissions",
|
Resource::Permissions => "permissions",
|
||||||
}
|
}
|
||||||
@@ -145,5 +149,6 @@ fn action_name(action: Action) -> &'static str {
|
|||||||
Action::Cancel => "cancel",
|
Action::Cancel => "cancel",
|
||||||
Action::Respond => "respond",
|
Action::Respond => "respond",
|
||||||
Action::Manage => "manage",
|
Action::Manage => "manage",
|
||||||
|
Action::Decrypt => "decrypt",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ pub struct CreateActionRequest {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
|
|
||||||
/// Action description
|
/// Action description
|
||||||
#[validate(length(min = 1))]
|
|
||||||
#[schema(example = "Posts a message to a Slack channel")]
|
#[schema(example = "Posts a message to a Slack channel")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Entry point for action execution (e.g., path to script, function name)
|
/// Entry point for action execution (e.g., path to script, function name)
|
||||||
#[validate(length(min = 1, max = 1024))]
|
#[validate(length(min = 1, max = 1024))]
|
||||||
@@ -63,7 +62,6 @@ pub struct UpdateActionRequest {
|
|||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
|
|
||||||
/// Action description
|
/// Action description
|
||||||
#[validate(length(min = 1))]
|
|
||||||
#[schema(example = "Posts a message to a Slack channel with enhanced features")]
|
#[schema(example = "Posts a message to a Slack channel with enhanced features")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
@@ -121,7 +119,7 @@ pub struct ActionResponse {
|
|||||||
|
|
||||||
/// Action description
|
/// Action description
|
||||||
#[schema(example = "Posts a message to a Slack channel")]
|
#[schema(example = "Posts a message to a Slack channel")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Entry point
|
/// Entry point
|
||||||
#[schema(example = "/actions/slack/post_message.py")]
|
#[schema(example = "/actions/slack/post_message.py")]
|
||||||
@@ -183,7 +181,7 @@ pub struct ActionSummary {
|
|||||||
|
|
||||||
/// Action description
|
/// Action description
|
||||||
#[schema(example = "Posts a message to a Slack channel")]
|
#[schema(example = "Posts a message to a Slack channel")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Entry point
|
/// Entry point
|
||||||
#[schema(example = "/actions/slack/post_message.py")]
|
#[schema(example = "/actions/slack/post_message.py")]
|
||||||
@@ -321,7 +319,7 @@ mod tests {
|
|||||||
r#ref: "".to_string(), // Invalid: empty
|
r#ref: "".to_string(), // Invalid: empty
|
||||||
pack_ref: "test-pack".to_string(),
|
pack_ref: "test-pack".to_string(),
|
||||||
label: "Test Action".to_string(),
|
label: "Test Action".to_string(),
|
||||||
description: "Test description".to_string(),
|
description: Some("Test description".to_string()),
|
||||||
entrypoint: "/actions/test.py".to_string(),
|
entrypoint: "/actions/test.py".to_string(),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
@@ -338,7 +336,7 @@ mod tests {
|
|||||||
r#ref: "test.action".to_string(),
|
r#ref: "test.action".to_string(),
|
||||||
pack_ref: "test-pack".to_string(),
|
pack_ref: "test-pack".to_string(),
|
||||||
label: "Test Action".to_string(),
|
label: "Test Action".to_string(),
|
||||||
description: "Test description".to_string(),
|
description: Some("Test description".to_string()),
|
||||||
entrypoint: "/actions/test.py".to_string(),
|
entrypoint: "/actions/test.py".to_string(),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ pub use inquiry::{
|
|||||||
pub use key::{CreateKeyRequest, KeyQueryParams, KeyResponse, KeySummary, UpdateKeyRequest};
|
pub use key::{CreateKeyRequest, KeyQueryParams, KeyResponse, KeySummary, UpdateKeyRequest};
|
||||||
pub use pack::{CreatePackRequest, PackResponse, PackSummary, UpdatePackRequest};
|
pub use pack::{CreatePackRequest, PackResponse, PackSummary, UpdatePackRequest};
|
||||||
pub use permission::{
|
pub use permission::{
|
||||||
CreateIdentityRequest, CreatePermissionAssignmentRequest, IdentityResponse, IdentitySummary,
|
CreateIdentityRequest, CreateIdentityRoleAssignmentRequest, CreatePermissionAssignmentRequest,
|
||||||
PermissionAssignmentResponse, PermissionSetQueryParams, PermissionSetSummary,
|
CreatePermissionSetRoleAssignmentRequest, IdentityResponse, IdentityRoleAssignmentResponse,
|
||||||
UpdateIdentityRequest,
|
IdentitySummary, PermissionAssignmentResponse, PermissionSetQueryParams,
|
||||||
|
PermissionSetRoleAssignmentResponse, PermissionSetSummary, UpdateIdentityRequest,
|
||||||
};
|
};
|
||||||
pub use rule::{CreateRuleRequest, RuleResponse, RuleSummary, UpdateRuleRequest};
|
pub use rule::{CreateRuleRequest, RuleResponse, RuleSummary, UpdateRuleRequest};
|
||||||
pub use runtime::{CreateRuntimeRequest, RuntimeResponse, RuntimeSummary, UpdateRuntimeRequest};
|
pub use runtime::{CreateRuntimeRequest, RuntimeResponse, RuntimeSummary, UpdateRuntimeRequest};
|
||||||
|
|||||||
@@ -14,10 +14,32 @@ pub struct IdentitySummary {
|
|||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub login: String,
|
pub login: String,
|
||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
|
pub frozen: bool,
|
||||||
pub attributes: JsonValue,
|
pub attributes: JsonValue,
|
||||||
|
pub roles: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type IdentityResponse = IdentitySummary;
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
|
pub struct IdentityRoleAssignmentResponse {
|
||||||
|
pub id: i64,
|
||||||
|
pub identity_id: i64,
|
||||||
|
pub role: String,
|
||||||
|
pub source: String,
|
||||||
|
pub managed: bool,
|
||||||
|
pub created: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub updated: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
|
pub struct IdentityResponse {
|
||||||
|
pub id: i64,
|
||||||
|
pub login: String,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub frozen: bool,
|
||||||
|
pub attributes: JsonValue,
|
||||||
|
pub roles: Vec<IdentityRoleAssignmentResponse>,
|
||||||
|
pub direct_permissions: Vec<PermissionAssignmentResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
pub struct PermissionSetSummary {
|
pub struct PermissionSetSummary {
|
||||||
@@ -27,6 +49,7 @@ pub struct PermissionSetSummary {
|
|||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub grants: JsonValue,
|
pub grants: JsonValue,
|
||||||
|
pub roles: Vec<PermissionSetRoleAssignmentResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
@@ -38,6 +61,15 @@ pub struct PermissionAssignmentResponse {
|
|||||||
pub created: chrono::DateTime<chrono::Utc>,
|
pub created: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||||
|
pub struct PermissionSetRoleAssignmentResponse {
|
||||||
|
pub id: i64,
|
||||||
|
pub permission_set_id: i64,
|
||||||
|
pub permission_set_ref: Option<String>,
|
||||||
|
pub role: String,
|
||||||
|
pub created: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||||
pub struct CreatePermissionAssignmentRequest {
|
pub struct CreatePermissionAssignmentRequest {
|
||||||
pub identity_id: Option<i64>,
|
pub identity_id: Option<i64>,
|
||||||
@@ -45,6 +77,18 @@ pub struct CreatePermissionAssignmentRequest {
|
|||||||
pub permission_set_ref: String,
|
pub permission_set_ref: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||||
|
pub struct CreateIdentityRoleAssignmentRequest {
|
||||||
|
#[validate(length(min = 1, max = 255))]
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||||
|
pub struct CreatePermissionSetRoleAssignmentRequest {
|
||||||
|
#[validate(length(min = 1, max = 255))]
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
#[derive(Debug, Clone, Deserialize, Validate, ToSchema)]
|
||||||
pub struct CreateIdentityRequest {
|
pub struct CreateIdentityRequest {
|
||||||
#[validate(length(min = 3, max = 255))]
|
#[validate(length(min = 3, max = 255))]
|
||||||
@@ -62,4 +106,5 @@ pub struct UpdateIdentityRequest {
|
|||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub attributes: Option<JsonValue>,
|
pub attributes: Option<JsonValue>,
|
||||||
|
pub frozen: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ pub struct CreateRuleRequest {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
|
|
||||||
/// Rule description
|
/// Rule description
|
||||||
#[validate(length(min = 1))]
|
|
||||||
#[schema(example = "Send Slack notification when an error occurs")]
|
#[schema(example = "Send Slack notification when an error occurs")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Action reference to execute when rule matches
|
/// Action reference to execute when rule matches
|
||||||
#[validate(length(min = 1, max = 255))]
|
#[validate(length(min = 1, max = 255))]
|
||||||
@@ -69,7 +68,6 @@ pub struct UpdateRuleRequest {
|
|||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
|
|
||||||
/// Rule description
|
/// Rule description
|
||||||
#[validate(length(min = 1))]
|
|
||||||
#[schema(example = "Enhanced error notification with filtering")]
|
#[schema(example = "Enhanced error notification with filtering")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
@@ -115,7 +113,7 @@ pub struct RuleResponse {
|
|||||||
|
|
||||||
/// Rule description
|
/// Rule description
|
||||||
#[schema(example = "Send Slack notification when an error occurs")]
|
#[schema(example = "Send Slack notification when an error occurs")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Action ID (null if the referenced action has been deleted)
|
/// Action ID (null if the referenced action has been deleted)
|
||||||
#[schema(example = 1)]
|
#[schema(example = 1)]
|
||||||
@@ -183,7 +181,7 @@ pub struct RuleSummary {
|
|||||||
|
|
||||||
/// Rule description
|
/// Rule description
|
||||||
#[schema(example = "Send Slack notification when an error occurs")]
|
#[schema(example = "Send Slack notification when an error occurs")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Action reference
|
/// Action reference
|
||||||
#[schema(example = "slack.post_message")]
|
#[schema(example = "slack.post_message")]
|
||||||
@@ -297,7 +295,7 @@ mod tests {
|
|||||||
r#ref: "".to_string(), // Invalid: empty
|
r#ref: "".to_string(), // Invalid: empty
|
||||||
pack_ref: "test-pack".to_string(),
|
pack_ref: "test-pack".to_string(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test description".to_string(),
|
description: Some("Test description".to_string()),
|
||||||
action_ref: "test.action".to_string(),
|
action_ref: "test.action".to_string(),
|
||||||
trigger_ref: "test.trigger".to_string(),
|
trigger_ref: "test.trigger".to_string(),
|
||||||
conditions: default_empty_object(),
|
conditions: default_empty_object(),
|
||||||
@@ -315,7 +313,7 @@ mod tests {
|
|||||||
r#ref: "test.rule".to_string(),
|
r#ref: "test.rule".to_string(),
|
||||||
pack_ref: "test-pack".to_string(),
|
pack_ref: "test-pack".to_string(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test description".to_string(),
|
description: Some("Test description".to_string()),
|
||||||
action_ref: "test.action".to_string(),
|
action_ref: "test.action".to_string(),
|
||||||
trigger_ref: "test.trigger".to_string(),
|
trigger_ref: "test.trigger".to_string(),
|
||||||
conditions: serde_json::json!({
|
conditions: serde_json::json!({
|
||||||
|
|||||||
@@ -203,9 +203,8 @@ pub struct CreateSensorRequest {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
|
|
||||||
/// Sensor description
|
/// Sensor description
|
||||||
#[validate(length(min = 1))]
|
|
||||||
#[schema(example = "Monitors CPU usage and generates events")]
|
#[schema(example = "Monitors CPU usage and generates events")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Entry point for sensor execution (e.g., path to script, function name)
|
/// Entry point for sensor execution (e.g., path to script, function name)
|
||||||
#[validate(length(min = 1, max = 1024))]
|
#[validate(length(min = 1, max = 1024))]
|
||||||
@@ -247,7 +246,6 @@ pub struct UpdateSensorRequest {
|
|||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
|
|
||||||
/// Sensor description
|
/// Sensor description
|
||||||
#[validate(length(min = 1))]
|
|
||||||
#[schema(example = "Enhanced CPU monitoring with alerts")]
|
#[schema(example = "Enhanced CPU monitoring with alerts")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
@@ -297,7 +295,7 @@ pub struct SensorResponse {
|
|||||||
|
|
||||||
/// Sensor description
|
/// Sensor description
|
||||||
#[schema(example = "Monitors CPU usage and generates events")]
|
#[schema(example = "Monitors CPU usage and generates events")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Entry point
|
/// Entry point
|
||||||
#[schema(example = "/sensors/monitoring/cpu_monitor.py")]
|
#[schema(example = "/sensors/monitoring/cpu_monitor.py")]
|
||||||
@@ -357,7 +355,7 @@ pub struct SensorSummary {
|
|||||||
|
|
||||||
/// Sensor description
|
/// Sensor description
|
||||||
#[schema(example = "Monitors CPU usage and generates events")]
|
#[schema(example = "Monitors CPU usage and generates events")]
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Trigger reference
|
/// Trigger reference
|
||||||
#[schema(example = "monitoring.cpu_threshold")]
|
#[schema(example = "monitoring.cpu_threshold")]
|
||||||
@@ -499,7 +497,7 @@ mod tests {
|
|||||||
r#ref: "test.sensor".to_string(),
|
r#ref: "test.sensor".to_string(),
|
||||||
pack_ref: "test-pack".to_string(),
|
pack_ref: "test-pack".to_string(),
|
||||||
label: "Test Sensor".to_string(),
|
label: "Test Sensor".to_string(),
|
||||||
description: "Test description".to_string(),
|
description: Some("Test description".to_string()),
|
||||||
entrypoint: "/sensors/test.py".to_string(),
|
entrypoint: "/sensors/test.py".to_string(),
|
||||||
runtime_ref: "python3".to_string(),
|
runtime_ref: "python3".to_string(),
|
||||||
trigger_ref: "test.trigger".to_string(),
|
trigger_ref: "test.trigger".to_string(),
|
||||||
|
|||||||
@@ -27,8 +27,11 @@ use crate::dto::{
|
|||||||
UpdatePackRequest, WorkflowSyncResult,
|
UpdatePackRequest, WorkflowSyncResult,
|
||||||
},
|
},
|
||||||
permission::{
|
permission::{
|
||||||
CreateIdentityRequest, CreatePermissionAssignmentRequest, IdentityResponse,
|
CreateIdentityRequest, CreateIdentityRoleAssignmentRequest,
|
||||||
IdentitySummary, PermissionAssignmentResponse, PermissionSetSummary, UpdateIdentityRequest,
|
CreatePermissionAssignmentRequest, CreatePermissionSetRoleAssignmentRequest,
|
||||||
|
IdentityResponse, IdentityRoleAssignmentResponse, IdentitySummary,
|
||||||
|
PermissionAssignmentResponse, PermissionSetRoleAssignmentResponse, PermissionSetSummary,
|
||||||
|
UpdateIdentityRequest,
|
||||||
},
|
},
|
||||||
rule::{CreateRuleRequest, RuleResponse, RuleSummary, UpdateRuleRequest},
|
rule::{CreateRuleRequest, RuleResponse, RuleSummary, UpdateRuleRequest},
|
||||||
runtime::{CreateRuntimeRequest, RuntimeResponse, RuntimeSummary, UpdateRuntimeRequest},
|
runtime::{CreateRuntimeRequest, RuntimeResponse, RuntimeSummary, UpdateRuntimeRequest},
|
||||||
@@ -185,6 +188,12 @@ use crate::dto::{
|
|||||||
crate::routes::permissions::list_identity_permissions,
|
crate::routes::permissions::list_identity_permissions,
|
||||||
crate::routes::permissions::create_permission_assignment,
|
crate::routes::permissions::create_permission_assignment,
|
||||||
crate::routes::permissions::delete_permission_assignment,
|
crate::routes::permissions::delete_permission_assignment,
|
||||||
|
crate::routes::permissions::create_identity_role_assignment,
|
||||||
|
crate::routes::permissions::delete_identity_role_assignment,
|
||||||
|
crate::routes::permissions::create_permission_set_role_assignment,
|
||||||
|
crate::routes::permissions::delete_permission_set_role_assignment,
|
||||||
|
crate::routes::permissions::freeze_identity,
|
||||||
|
crate::routes::permissions::unfreeze_identity,
|
||||||
|
|
||||||
// Workflows
|
// Workflows
|
||||||
crate::routes::workflows::list_workflows,
|
crate::routes::workflows::list_workflows,
|
||||||
@@ -277,6 +286,10 @@ use crate::dto::{
|
|||||||
PermissionSetSummary,
|
PermissionSetSummary,
|
||||||
PermissionAssignmentResponse,
|
PermissionAssignmentResponse,
|
||||||
CreatePermissionAssignmentRequest,
|
CreatePermissionAssignmentRequest,
|
||||||
|
CreateIdentityRoleAssignmentRequest,
|
||||||
|
IdentityRoleAssignmentResponse,
|
||||||
|
CreatePermissionSetRoleAssignmentRequest,
|
||||||
|
PermissionSetRoleAssignmentResponse,
|
||||||
|
|
||||||
// Runtime DTOs
|
// Runtime DTOs
|
||||||
CreateRuntimeRequest,
|
CreateRuntimeRequest,
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ pub async fn update_action(
|
|||||||
// Create update input
|
// Create update input
|
||||||
let update_input = UpdateActionInput {
|
let update_input = UpdateActionInput {
|
||||||
label: request.label,
|
label: request.label,
|
||||||
description: request.description,
|
description: request.description.map(Patch::Set),
|
||||||
entrypoint: request.entrypoint,
|
entrypoint: request.entrypoint,
|
||||||
runtime: request.runtime,
|
runtime: request.runtime,
|
||||||
runtime_version_constraint: request.runtime_version_constraint.map(|patch| match patch {
|
runtime_version_constraint: request.runtime_version_constraint.map(|patch| match patch {
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ use attune_common::repositories::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::middleware::RequireAuth,
|
auth::{jwt::TokenType, middleware::AuthenticatedUser, middleware::RequireAuth},
|
||||||
|
authz::{AuthorizationCheck, AuthorizationService},
|
||||||
dto::{
|
dto::{
|
||||||
artifact::{
|
artifact::{
|
||||||
AllocateFileVersionByRefRequest, AppendProgressRequest, ArtifactExecutionPatch,
|
AllocateFileVersionByRefRequest, AppendProgressRequest, ArtifactExecutionPatch,
|
||||||
@@ -55,6 +56,7 @@ use crate::{
|
|||||||
middleware::{ApiError, ApiResult},
|
middleware::{ApiError, ApiResult},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
use attune_common::rbac::{Action, AuthorizationContext, Resource};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Artifact CRUD
|
// Artifact CRUD
|
||||||
@@ -72,7 +74,7 @@ use crate::{
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn list_artifacts(
|
pub async fn list_artifacts(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Query(query): Query<ArtifactQueryParams>,
|
Query(query): Query<ArtifactQueryParams>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
@@ -88,8 +90,16 @@ pub async fn list_artifacts(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = ArtifactRepository::search(&state.db, &filters).await?;
|
let result = ArtifactRepository::search(&state.db, &filters).await?;
|
||||||
|
let mut rows = result.rows;
|
||||||
|
|
||||||
let items: Vec<ArtifactSummary> = result.rows.into_iter().map(ArtifactSummary::from).collect();
|
if let Some((identity_id, grants)) = ensure_can_read_any_artifact(&state, &user).await? {
|
||||||
|
rows.retain(|artifact| {
|
||||||
|
let ctx = artifact_authorization_context(identity_id, artifact);
|
||||||
|
AuthorizationService::is_allowed(&grants, Resource::Artifacts, Action::Read, &ctx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: Vec<ArtifactSummary> = rows.into_iter().map(ArtifactSummary::from).collect();
|
||||||
|
|
||||||
let pagination = PaginationParams {
|
let pagination = PaginationParams {
|
||||||
page: query.page,
|
page: query.page,
|
||||||
@@ -113,7 +123,7 @@ pub async fn list_artifacts(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn get_artifact(
|
pub async fn get_artifact(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
@@ -121,6 +131,10 @@ pub async fn get_artifact(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Read, &artifact)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(ApiResponse::new(ArtifactResponse::from(artifact))),
|
Json(ApiResponse::new(ArtifactResponse::from(artifact))),
|
||||||
@@ -140,7 +154,7 @@ pub async fn get_artifact(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn get_artifact_by_ref(
|
pub async fn get_artifact_by_ref(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(artifact_ref): Path<String>,
|
Path(artifact_ref): Path<String>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
@@ -148,6 +162,10 @@ pub async fn get_artifact_by_ref(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact '{}' not found", artifact_ref)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact '{}' not found", artifact_ref)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Read, &artifact)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("Artifact '{}' not found", artifact_ref)))?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(ApiResponse::new(ArtifactResponse::from(artifact))),
|
Json(ApiResponse::new(ArtifactResponse::from(artifact))),
|
||||||
@@ -168,7 +186,7 @@ pub async fn get_artifact_by_ref(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn create_artifact(
|
pub async fn create_artifact(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(request): Json<CreateArtifactRequest>,
|
Json(request): Json<CreateArtifactRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
@@ -200,6 +218,16 @@ pub async fn create_artifact(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
authorize_artifact_create(
|
||||||
|
&state,
|
||||||
|
&user,
|
||||||
|
&request.r#ref,
|
||||||
|
request.scope,
|
||||||
|
&request.owner,
|
||||||
|
visibility,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let input = CreateArtifactInput {
|
let input = CreateArtifactInput {
|
||||||
r#ref: request.r#ref,
|
r#ref: request.r#ref,
|
||||||
scope: request.scope,
|
scope: request.scope,
|
||||||
@@ -240,16 +268,18 @@ pub async fn create_artifact(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn update_artifact(
|
pub async fn update_artifact(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
Json(request): Json<UpdateArtifactRequest>,
|
Json(request): Json<UpdateArtifactRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verify artifact exists
|
// Verify artifact exists
|
||||||
ArtifactRepository::find_by_id(&state.db, id)
|
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Update, &artifact).await?;
|
||||||
|
|
||||||
let input = UpdateArtifactInput {
|
let input = UpdateArtifactInput {
|
||||||
r#ref: None, // Ref is immutable after creation
|
r#ref: None, // Ref is immutable after creation
|
||||||
scope: request.scope,
|
scope: request.scope,
|
||||||
@@ -305,7 +335,7 @@ pub async fn update_artifact(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn delete_artifact(
|
pub async fn delete_artifact(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
@@ -313,6 +343,8 @@ pub async fn delete_artifact(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Delete, &artifact).await?;
|
||||||
|
|
||||||
// Before deleting DB rows, clean up any file-backed versions on disk
|
// Before deleting DB rows, clean up any file-backed versions on disk
|
||||||
let file_versions =
|
let file_versions =
|
||||||
ArtifactVersionRepository::find_file_versions_by_artifact(&state.db, id).await?;
|
ArtifactVersionRepository::find_file_versions_by_artifact(&state.db, id).await?;
|
||||||
@@ -355,11 +387,17 @@ pub async fn delete_artifact(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn list_artifacts_by_execution(
|
pub async fn list_artifacts_by_execution(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(execution_id): Path<i64>,
|
Path(execution_id): Path<i64>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
let artifacts = ArtifactRepository::find_by_execution(&state.db, execution_id).await?;
|
let mut artifacts = ArtifactRepository::find_by_execution(&state.db, execution_id).await?;
|
||||||
|
if let Some((identity_id, grants)) = ensure_can_read_any_artifact(&state, &user).await? {
|
||||||
|
artifacts.retain(|artifact| {
|
||||||
|
let ctx = artifact_authorization_context(identity_id, artifact);
|
||||||
|
AuthorizationService::is_allowed(&grants, Resource::Artifacts, Action::Read, &ctx)
|
||||||
|
});
|
||||||
|
}
|
||||||
let items: Vec<ArtifactSummary> = artifacts.into_iter().map(ArtifactSummary::from).collect();
|
let items: Vec<ArtifactSummary> = artifacts.into_iter().map(ArtifactSummary::from).collect();
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(ApiResponse::new(items))))
|
Ok((StatusCode::OK, Json(ApiResponse::new(items))))
|
||||||
@@ -387,7 +425,7 @@ pub async fn list_artifacts_by_execution(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn append_progress(
|
pub async fn append_progress(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
Json(request): Json<AppendProgressRequest>,
|
Json(request): Json<AppendProgressRequest>,
|
||||||
@@ -396,6 +434,8 @@ pub async fn append_progress(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Update, &artifact).await?;
|
||||||
|
|
||||||
if artifact.r#type != ArtifactType::Progress {
|
if artifact.r#type != ArtifactType::Progress {
|
||||||
return Err(ApiError::BadRequest(format!(
|
return Err(ApiError::BadRequest(format!(
|
||||||
"Artifact '{}' is type {:?}, not progress. Use version endpoints for file artifacts.",
|
"Artifact '{}' is type {:?}, not progress. Use version endpoints for file artifacts.",
|
||||||
@@ -430,16 +470,18 @@ pub async fn append_progress(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn set_artifact_data(
|
pub async fn set_artifact_data(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
Json(request): Json<SetDataRequest>,
|
Json(request): Json<SetDataRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verify exists
|
// Verify exists
|
||||||
ArtifactRepository::find_by_id(&state.db, id)
|
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Update, &artifact).await?;
|
||||||
|
|
||||||
let updated = ArtifactRepository::set_data(&state.db, id, &request.data).await?;
|
let updated = ArtifactRepository::set_data(&state.db, id, &request.data).await?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
@@ -468,15 +510,19 @@ pub async fn set_artifact_data(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn list_versions(
|
pub async fn list_versions(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verify artifact exists
|
// Verify artifact exists
|
||||||
ArtifactRepository::find_by_id(&state.db, id)
|
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Read, &artifact)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
let versions = ArtifactVersionRepository::list_by_artifact(&state.db, id).await?;
|
let versions = ArtifactVersionRepository::list_by_artifact(&state.db, id).await?;
|
||||||
let items: Vec<ArtifactVersionSummary> = versions
|
let items: Vec<ArtifactVersionSummary> = versions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -502,15 +548,19 @@ pub async fn list_versions(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn get_version(
|
pub async fn get_version(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path((id, version)): Path<(i64, i32)>,
|
Path((id, version)): Path<(i64, i32)>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// Verify artifact exists
|
// Verify artifact exists
|
||||||
ArtifactRepository::find_by_id(&state.db, id)
|
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Read, &artifact)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
let ver = ArtifactVersionRepository::find_by_version(&state.db, id, version)
|
let ver = ArtifactVersionRepository::find_by_version(&state.db, id, version)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
@@ -536,14 +586,18 @@ pub async fn get_version(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn get_latest_version(
|
pub async fn get_latest_version(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
ArtifactRepository::find_by_id(&state.db, id)
|
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Read, &artifact)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
let ver = ArtifactVersionRepository::find_latest(&state.db, id)
|
let ver = ArtifactVersionRepository::find_latest(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("No versions found for artifact {}", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("No versions found for artifact {}", id)))?;
|
||||||
@@ -568,15 +622,17 @@ pub async fn get_latest_version(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn create_version_json(
|
pub async fn create_version_json(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
Json(request): Json<CreateVersionJsonRequest>,
|
Json(request): Json<CreateVersionJsonRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
ArtifactRepository::find_by_id(&state.db, id)
|
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Update, &artifact).await?;
|
||||||
|
|
||||||
let input = CreateArtifactVersionInput {
|
let input = CreateArtifactVersionInput {
|
||||||
artifact: id,
|
artifact: id,
|
||||||
content_type: Some(
|
content_type: Some(
|
||||||
@@ -624,7 +680,7 @@ pub async fn create_version_json(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn create_version_file(
|
pub async fn create_version_file(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
Json(request): Json<CreateFileVersionRequest>,
|
Json(request): Json<CreateFileVersionRequest>,
|
||||||
@@ -633,6 +689,8 @@ pub async fn create_version_file(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Update, &artifact).await?;
|
||||||
|
|
||||||
// Validate this is a file-type artifact
|
// Validate this is a file-type artifact
|
||||||
if !is_file_backed_type(artifact.r#type) {
|
if !is_file_backed_type(artifact.r#type) {
|
||||||
return Err(ApiError::BadRequest(format!(
|
return Err(ApiError::BadRequest(format!(
|
||||||
@@ -726,15 +784,17 @@ pub async fn create_version_file(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn upload_version(
|
pub async fn upload_version(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
mut multipart: Multipart,
|
mut multipart: Multipart,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
ArtifactRepository::find_by_id(&state.db, id)
|
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Update, &artifact).await?;
|
||||||
|
|
||||||
let mut file_data: Option<Vec<u8>> = None;
|
let mut file_data: Option<Vec<u8>> = None;
|
||||||
let mut content_type: Option<String> = None;
|
let mut content_type: Option<String> = None;
|
||||||
let mut meta: Option<serde_json::Value> = None;
|
let mut meta: Option<serde_json::Value> = None;
|
||||||
@@ -854,7 +914,7 @@ pub async fn upload_version(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn download_version(
|
pub async fn download_version(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path((id, version)): Path<(i64, i32)>,
|
Path((id, version)): Path<(i64, i32)>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
@@ -862,6 +922,10 @@ pub async fn download_version(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Read, &artifact)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
// First try without content (cheaper query) to check for file_path
|
// First try without content (cheaper query) to check for file_path
|
||||||
let ver = ArtifactVersionRepository::find_by_version(&state.db, id, version)
|
let ver = ArtifactVersionRepository::find_by_version(&state.db, id, version)
|
||||||
.await?
|
.await?
|
||||||
@@ -904,7 +968,7 @@ pub async fn download_version(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn download_latest(
|
pub async fn download_latest(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(id): Path<i64>,
|
Path(id): Path<i64>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
@@ -912,6 +976,10 @@ pub async fn download_latest(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Read, &artifact)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
// First try without content (cheaper query) to check for file_path
|
// First try without content (cheaper query) to check for file_path
|
||||||
let ver = ArtifactVersionRepository::find_latest(&state.db, id)
|
let ver = ArtifactVersionRepository::find_latest(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
@@ -955,7 +1023,7 @@ pub async fn download_latest(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn delete_version(
|
pub async fn delete_version(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path((id, version)): Path<(i64, i32)>,
|
Path((id, version)): Path<(i64, i32)>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
@@ -964,6 +1032,8 @@ pub async fn delete_version(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Delete, &artifact).await?;
|
||||||
|
|
||||||
// Find the version by artifact + version number
|
// Find the version by artifact + version number
|
||||||
let ver = ArtifactVersionRepository::find_by_version(&state.db, id, version)
|
let ver = ArtifactVersionRepository::find_by_version(&state.db, id, version)
|
||||||
.await?
|
.await?
|
||||||
@@ -1042,7 +1112,7 @@ pub async fn delete_version(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn upload_version_by_ref(
|
pub async fn upload_version_by_ref(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(artifact_ref): Path<String>,
|
Path(artifact_ref): Path<String>,
|
||||||
mut multipart: Multipart,
|
mut multipart: Multipart,
|
||||||
@@ -1157,6 +1227,8 @@ pub async fn upload_version_by_ref(
|
|||||||
// Upsert: find existing artifact or create a new one
|
// Upsert: find existing artifact or create a new one
|
||||||
let artifact = match ArtifactRepository::find_by_ref(&state.db, &artifact_ref).await? {
|
let artifact = match ArtifactRepository::find_by_ref(&state.db, &artifact_ref).await? {
|
||||||
Some(existing) => {
|
Some(existing) => {
|
||||||
|
authorize_artifact_action(&state, &user, Action::Update, &existing).await?;
|
||||||
|
|
||||||
// Update execution link if a new execution ID was provided
|
// Update execution link if a new execution ID was provided
|
||||||
if execution_id.is_some() && execution_id != existing.execution {
|
if execution_id.is_some() && execution_id != existing.execution {
|
||||||
let update_input = UpdateArtifactInput {
|
let update_input = UpdateArtifactInput {
|
||||||
@@ -1211,6 +1283,16 @@ pub async fn upload_version_by_ref(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
authorize_artifact_create(
|
||||||
|
&state,
|
||||||
|
&user,
|
||||||
|
&artifact_ref,
|
||||||
|
a_scope,
|
||||||
|
owner.as_deref().unwrap_or_default(),
|
||||||
|
a_visibility,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Parse retention
|
// Parse retention
|
||||||
let a_retention_policy: RetentionPolicyType = match &retention_policy {
|
let a_retention_policy: RetentionPolicyType = match &retention_policy {
|
||||||
Some(rp) if !rp.is_empty() => {
|
Some(rp) if !rp.is_empty() => {
|
||||||
@@ -1297,7 +1379,7 @@ pub async fn upload_version_by_ref(
|
|||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn allocate_file_version_by_ref(
|
pub async fn allocate_file_version_by_ref(
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Path(artifact_ref): Path<String>,
|
Path(artifact_ref): Path<String>,
|
||||||
Json(request): Json<AllocateFileVersionByRefRequest>,
|
Json(request): Json<AllocateFileVersionByRefRequest>,
|
||||||
@@ -1305,6 +1387,8 @@ pub async fn allocate_file_version_by_ref(
|
|||||||
// Upsert: find existing artifact or create a new one
|
// Upsert: find existing artifact or create a new one
|
||||||
let artifact = match ArtifactRepository::find_by_ref(&state.db, &artifact_ref).await? {
|
let artifact = match ArtifactRepository::find_by_ref(&state.db, &artifact_ref).await? {
|
||||||
Some(existing) => {
|
Some(existing) => {
|
||||||
|
authorize_artifact_action(&state, &user, Action::Update, &existing).await?;
|
||||||
|
|
||||||
// Update execution link if a new execution ID was provided
|
// Update execution link if a new execution ID was provided
|
||||||
if request.execution.is_some() && request.execution != existing.execution {
|
if request.execution.is_some() && request.execution != existing.execution {
|
||||||
let update_input = UpdateArtifactInput {
|
let update_input = UpdateArtifactInput {
|
||||||
@@ -1347,6 +1431,16 @@ pub async fn allocate_file_version_by_ref(
|
|||||||
.unwrap_or(RetentionPolicyType::Versions);
|
.unwrap_or(RetentionPolicyType::Versions);
|
||||||
let a_retention_limit = request.retention_limit.unwrap_or(10);
|
let a_retention_limit = request.retention_limit.unwrap_or(10);
|
||||||
|
|
||||||
|
authorize_artifact_create(
|
||||||
|
&state,
|
||||||
|
&user,
|
||||||
|
&artifact_ref,
|
||||||
|
a_scope,
|
||||||
|
request.owner.as_deref().unwrap_or_default(),
|
||||||
|
a_visibility,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let create_input = CreateArtifactInput {
|
let create_input = CreateArtifactInput {
|
||||||
r#ref: artifact_ref.clone(),
|
r#ref: artifact_ref.clone(),
|
||||||
scope: a_scope,
|
scope: a_scope,
|
||||||
@@ -1437,6 +1531,105 @@ pub async fn allocate_file_version_by_ref(
|
|||||||
// Helpers
|
// Helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
async fn authorize_artifact_action(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
user: &AuthenticatedUser,
|
||||||
|
action: Action,
|
||||||
|
artifact: &attune_common::models::artifact::Artifact,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
if user.claims.token_type != TokenType::Access {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let identity_id = user
|
||||||
|
.identity_id()
|
||||||
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
||||||
|
let authz = AuthorizationService::new(state.db.clone());
|
||||||
|
authz
|
||||||
|
.authorize(
|
||||||
|
user,
|
||||||
|
AuthorizationCheck {
|
||||||
|
resource: Resource::Artifacts,
|
||||||
|
action,
|
||||||
|
context: artifact_authorization_context(identity_id, artifact),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authorize_artifact_create(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
user: &AuthenticatedUser,
|
||||||
|
artifact_ref: &str,
|
||||||
|
scope: OwnerType,
|
||||||
|
owner: &str,
|
||||||
|
visibility: ArtifactVisibility,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
if user.claims.token_type != TokenType::Access {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let identity_id = user
|
||||||
|
.identity_id()
|
||||||
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
||||||
|
let authz = AuthorizationService::new(state.db.clone());
|
||||||
|
let mut ctx = AuthorizationContext::new(identity_id);
|
||||||
|
ctx.target_ref = Some(artifact_ref.to_string());
|
||||||
|
ctx.owner_type = Some(scope);
|
||||||
|
ctx.owner_ref = Some(owner.to_string());
|
||||||
|
ctx.visibility = Some(visibility);
|
||||||
|
|
||||||
|
authz
|
||||||
|
.authorize(
|
||||||
|
user,
|
||||||
|
AuthorizationCheck {
|
||||||
|
resource: Resource::Artifacts,
|
||||||
|
action: Action::Create,
|
||||||
|
context: ctx,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_can_read_any_artifact(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
user: &AuthenticatedUser,
|
||||||
|
) -> Result<Option<(i64, Vec<attune_common::rbac::Grant>)>, ApiError> {
|
||||||
|
if user.claims.token_type != TokenType::Access {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let identity_id = user
|
||||||
|
.identity_id()
|
||||||
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
||||||
|
let authz = AuthorizationService::new(state.db.clone());
|
||||||
|
let grants = authz.effective_grants(user).await?;
|
||||||
|
|
||||||
|
let can_read_any_artifact = grants
|
||||||
|
.iter()
|
||||||
|
.any(|g| g.resource == Resource::Artifacts && g.actions.contains(&Action::Read));
|
||||||
|
if !can_read_any_artifact {
|
||||||
|
return Err(ApiError::Forbidden(
|
||||||
|
"Insufficient permissions: artifacts:read".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some((identity_id, grants)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn artifact_authorization_context(
|
||||||
|
identity_id: i64,
|
||||||
|
artifact: &attune_common::models::artifact::Artifact,
|
||||||
|
) -> AuthorizationContext {
|
||||||
|
let mut ctx = AuthorizationContext::new(identity_id);
|
||||||
|
ctx.target_id = Some(artifact.id);
|
||||||
|
ctx.target_ref = Some(artifact.r#ref.clone());
|
||||||
|
ctx.owner_type = Some(artifact.scope);
|
||||||
|
ctx.owner_ref = Some(artifact.owner.clone());
|
||||||
|
ctx.visibility = Some(artifact.visibility);
|
||||||
|
ctx
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true for artifact types that should use file-backed storage on disk.
|
/// Returns true for artifact types that should use file-backed storage on disk.
|
||||||
fn is_file_backed_type(artifact_type: ArtifactType) -> bool {
|
fn is_file_backed_type(artifact_type: ArtifactType) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
@@ -1775,14 +1968,19 @@ pub async fn stream_artifact(
|
|||||||
let token = params.token.as_ref().ok_or(ApiError::Unauthorized(
|
let token = params.token.as_ref().ok_or(ApiError::Unauthorized(
|
||||||
"Missing authentication token".to_string(),
|
"Missing authentication token".to_string(),
|
||||||
))?;
|
))?;
|
||||||
validate_token(token, &state.jwt_config)
|
let claims = validate_token(token, &state.jwt_config)
|
||||||
.map_err(|_| ApiError::Unauthorized("Invalid authentication token".to_string()))?;
|
.map_err(|_| ApiError::Unauthorized("Invalid authentication token".to_string()))?;
|
||||||
|
let user = AuthenticatedUser { claims };
|
||||||
|
|
||||||
// --- resolve artifact + latest version ---------------------------------
|
// --- resolve artifact + latest version ---------------------------------
|
||||||
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
let artifact = ArtifactRepository::find_by_id(&state.db, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
|
authorize_artifact_action(&state, &user, Action::Read, &artifact)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ApiError::NotFound(format!("Artifact with ID {} not found", id)))?;
|
||||||
|
|
||||||
if !is_file_backed_type(artifact.r#type) {
|
if !is_file_backed_type(artifact.r#type) {
|
||||||
return Err(ApiError::BadRequest(format!(
|
return Err(ApiError::BadRequest(format!(
|
||||||
"Artifact '{}' is type {:?} which is not file-backed. \
|
"Artifact '{}' is type {:?} which is not file-backed. \
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ pub async fn login(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::Unauthorized("Invalid login or password".to_string()))?;
|
.ok_or_else(|| ApiError::Unauthorized("Invalid login or password".to_string()))?;
|
||||||
|
|
||||||
|
if identity.frozen {
|
||||||
|
return Err(ApiError::Forbidden(
|
||||||
|
"Identity is frozen and cannot authenticate".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Check if identity has a password set
|
// Check if identity has a password set
|
||||||
let password_hash = identity
|
let password_hash = identity
|
||||||
.password_hash
|
.password_hash
|
||||||
@@ -324,6 +330,12 @@ pub async fn refresh_token(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::Unauthorized("Identity not found".to_string()))?;
|
.ok_or_else(|| ApiError::Unauthorized("Identity not found".to_string()))?;
|
||||||
|
|
||||||
|
if identity.frozen {
|
||||||
|
return Err(ApiError::Forbidden(
|
||||||
|
"Identity is frozen and cannot authenticate".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Generate new tokens
|
// Generate new tokens
|
||||||
let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
|
let access_token = generate_access_token(identity.id, &identity.login, &state.jwt_config)?;
|
||||||
let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;
|
let refresh_token = generate_refresh_token(identity.id, &identity.login, &state.jwt_config)?;
|
||||||
@@ -380,6 +392,12 @@ pub async fn get_current_user(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound("Identity not found".to_string()))?;
|
.ok_or_else(|| ApiError::NotFound("Identity not found".to_string()))?;
|
||||||
|
|
||||||
|
if identity.frozen {
|
||||||
|
return Err(ApiError::Forbidden(
|
||||||
|
"Identity is frozen and cannot authenticate".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let response = CurrentUserResponse {
|
let response = CurrentUserResponse {
|
||||||
id: identity.id,
|
id: identity.id,
|
||||||
login: identity.login,
|
login: identity.login,
|
||||||
@@ -551,6 +569,7 @@ pub async fn change_password(
|
|||||||
display_name: None,
|
display_name: None,
|
||||||
password_hash: Some(new_password_hash),
|
password_hash: Some(new_password_hash),
|
||||||
attributes: None,
|
attributes: None,
|
||||||
|
frozen: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
IdentityRepository::update(&state.db, identity_id, update_input).await?;
|
IdentityRepository::update(&state.db, identity_id, update_input).await?;
|
||||||
|
|||||||
@@ -82,6 +82,17 @@ pub async fn create_event(
|
|||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(payload): Json<CreateEventRequest>,
|
Json(payload): Json<CreateEventRequest>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
|
// Only sensor and execution tokens may create events directly.
|
||||||
|
// User sessions must go through the webhook receiver instead.
|
||||||
|
use crate::auth::jwt::TokenType;
|
||||||
|
if user.0.claims.token_type == TokenType::Access {
|
||||||
|
return Err(ApiError::Forbidden(
|
||||||
|
"Events may only be created by sensor services. To fire an event as a user, \
|
||||||
|
enable webhooks on the trigger and POST to its webhook URL."
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Validate request
|
// Validate request
|
||||||
payload
|
payload
|
||||||
.validate()
|
.validate()
|
||||||
@@ -128,7 +139,6 @@ pub async fn create_event(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Determine source (sensor) from authenticated user if it's a sensor token
|
// Determine source (sensor) from authenticated user if it's a sensor token
|
||||||
use crate::auth::jwt::TokenType;
|
|
||||||
let (source_id, source_ref) = match user.0.claims.token_type {
|
let (source_id, source_ref) = match user.0.claims.token_type {
|
||||||
TokenType::Sensor => {
|
TokenType::Sensor => {
|
||||||
// Extract sensor reference from login
|
// Extract sensor reference from login
|
||||||
|
|||||||
@@ -93,19 +93,6 @@ pub async fn create_execution(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut execution_ctx = AuthorizationContext::new(identity_id);
|
|
||||||
execution_ctx.pack_ref = Some(action.pack_ref.clone());
|
|
||||||
authz
|
|
||||||
.authorize(
|
|
||||||
&user,
|
|
||||||
AuthorizationCheck {
|
|
||||||
resource: Resource::Executions,
|
|
||||||
action: Action::Create,
|
|
||||||
context: execution_ctx,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create execution input
|
// Create execution input
|
||||||
|
|||||||
@@ -120,12 +120,16 @@ pub async fn get_key(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Key '{}' not found", key_ref)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Key '{}' not found", key_ref)))?;
|
||||||
|
|
||||||
if user.0.claims.token_type == TokenType::Access {
|
// For encrypted keys, track whether this caller is permitted to see the value.
|
||||||
|
// Non-Access tokens (sensor, execution) always get full access.
|
||||||
|
let can_decrypt = if user.0.claims.token_type == TokenType::Access {
|
||||||
let identity_id = user
|
let identity_id = user
|
||||||
.0
|
.0
|
||||||
.identity_id()
|
.identity_id()
|
||||||
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
||||||
let authz = AuthorizationService::new(state.db.clone());
|
let authz = AuthorizationService::new(state.db.clone());
|
||||||
|
|
||||||
|
// Basic read check — hide behind 404 to prevent enumeration.
|
||||||
authz
|
authz
|
||||||
.authorize(
|
.authorize(
|
||||||
&user.0,
|
&user.0,
|
||||||
@@ -136,28 +140,55 @@ pub async fn get_key(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
// Hide unauthorized records behind 404 to reduce enumeration leakage.
|
|
||||||
.map_err(|_| ApiError::NotFound(format!("Key '{}' not found", key_ref)))?;
|
.map_err(|_| ApiError::NotFound(format!("Key '{}' not found", key_ref)))?;
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt value if encrypted
|
// For encrypted keys, separately check Keys::Decrypt.
|
||||||
|
// Failing this is not an error — we just return the value as null.
|
||||||
|
if key.encrypted {
|
||||||
|
authz
|
||||||
|
.authorize(
|
||||||
|
&user.0,
|
||||||
|
AuthorizationCheck {
|
||||||
|
resource: Resource::Keys,
|
||||||
|
action: Action::Decrypt,
|
||||||
|
context: key_authorization_context(identity_id, &key),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decrypt value if encrypted and caller has permission.
|
||||||
|
// If they lack Keys::Decrypt, return null rather than the ciphertext.
|
||||||
if key.encrypted {
|
if key.encrypted {
|
||||||
let encryption_key = state
|
if can_decrypt {
|
||||||
.config
|
let encryption_key =
|
||||||
.security
|
state
|
||||||
.encryption_key
|
.config
|
||||||
.as_ref()
|
.security
|
||||||
.ok_or_else(|| {
|
.encryption_key
|
||||||
ApiError::InternalServerError("Encryption key not configured on server".to_string())
|
.as_ref()
|
||||||
})?;
|
.ok_or_else(|| {
|
||||||
|
ApiError::InternalServerError(
|
||||||
|
"Encryption key not configured on server".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let decrypted_value = attune_common::crypto::decrypt_json(&key.value, encryption_key)
|
let decrypted_value = attune_common::crypto::decrypt_json(&key.value, encryption_key)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("Failed to decrypt key '{}': {}", key_ref, e);
|
tracing::error!("Failed to decrypt key '{}': {}", key_ref, e);
|
||||||
ApiError::InternalServerError(format!("Failed to decrypt key: {}", e))
|
ApiError::InternalServerError(format!("Failed to decrypt key: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
key.value = decrypted_value;
|
key.value = decrypted_value;
|
||||||
|
} else {
|
||||||
|
key.value = serde_json::Value::Null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = ApiResponse::new(KeyResponse::from(key));
|
let response = ApiResponse::new(KeyResponse::from(key));
|
||||||
@@ -195,6 +226,7 @@ pub async fn create_key(
|
|||||||
let mut ctx = AuthorizationContext::new(identity_id);
|
let mut ctx = AuthorizationContext::new(identity_id);
|
||||||
ctx.owner_identity_id = request.owner_identity;
|
ctx.owner_identity_id = request.owner_identity;
|
||||||
ctx.owner_type = Some(request.owner_type);
|
ctx.owner_type = Some(request.owner_type);
|
||||||
|
ctx.owner_ref = requested_key_owner_ref(&request);
|
||||||
ctx.encrypted = Some(request.encrypted);
|
ctx.encrypted = Some(request.encrypted);
|
||||||
ctx.target_ref = Some(request.r#ref.clone());
|
ctx.target_ref = Some(request.r#ref.clone());
|
||||||
|
|
||||||
@@ -541,6 +573,38 @@ fn key_authorization_context(identity_id: i64, key: &Key) -> AuthorizationContex
|
|||||||
ctx.target_ref = Some(key.r#ref.clone());
|
ctx.target_ref = Some(key.r#ref.clone());
|
||||||
ctx.owner_identity_id = key.owner_identity;
|
ctx.owner_identity_id = key.owner_identity;
|
||||||
ctx.owner_type = Some(key.owner_type);
|
ctx.owner_type = Some(key.owner_type);
|
||||||
|
ctx.owner_ref = key_owner_ref(
|
||||||
|
key.owner_type,
|
||||||
|
key.owner.as_deref(),
|
||||||
|
key.owner_pack_ref.as_deref(),
|
||||||
|
key.owner_action_ref.as_deref(),
|
||||||
|
key.owner_sensor_ref.as_deref(),
|
||||||
|
);
|
||||||
ctx.encrypted = Some(key.encrypted);
|
ctx.encrypted = Some(key.encrypted);
|
||||||
ctx
|
ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn requested_key_owner_ref(request: &CreateKeyRequest) -> Option<String> {
|
||||||
|
key_owner_ref(
|
||||||
|
request.owner_type,
|
||||||
|
request.owner.as_deref(),
|
||||||
|
request.owner_pack_ref.as_deref(),
|
||||||
|
request.owner_action_ref.as_deref(),
|
||||||
|
request.owner_sensor_ref.as_deref(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_owner_ref(
|
||||||
|
owner_type: OwnerType,
|
||||||
|
owner: Option<&str>,
|
||||||
|
owner_pack_ref: Option<&str>,
|
||||||
|
owner_action_ref: Option<&str>,
|
||||||
|
owner_sensor_ref: Option<&str>,
|
||||||
|
) -> Option<String> {
|
||||||
|
match owner_type {
|
||||||
|
OwnerType::Pack => owner_pack_ref.map(str::to_string),
|
||||||
|
OwnerType::Action => owner_action_ref.map(str::to_string),
|
||||||
|
OwnerType::Sensor => owner_sensor_ref.map(str::to_string),
|
||||||
|
_ => owner.map(str::to_string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ use std::sync::Arc;
|
|||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use attune_common::{
|
use attune_common::{
|
||||||
models::identity::{Identity, PermissionSet},
|
models::identity::{Identity, IdentityRoleAssignment},
|
||||||
rbac::{Action, AuthorizationContext, Resource},
|
rbac::{Action, AuthorizationContext, Resource},
|
||||||
repositories::{
|
repositories::{
|
||||||
identity::{
|
identity::{
|
||||||
CreateIdentityInput, CreatePermissionAssignmentInput, IdentityRepository,
|
CreateIdentityInput, CreateIdentityRoleAssignmentInput,
|
||||||
PermissionAssignmentRepository, PermissionSetRepository, UpdateIdentityInput,
|
CreatePermissionAssignmentInput, CreatePermissionSetRoleAssignmentInput,
|
||||||
|
IdentityRepository, IdentityRoleAssignmentRepository, PermissionAssignmentRepository,
|
||||||
|
PermissionSetRepository, PermissionSetRoleAssignmentRepository, UpdateIdentityInput,
|
||||||
},
|
},
|
||||||
Create, Delete, FindById, FindByRef, List, Update,
|
Create, Delete, FindById, FindByRef, List, Update,
|
||||||
},
|
},
|
||||||
@@ -26,9 +28,12 @@ use crate::{
|
|||||||
authz::{AuthorizationCheck, AuthorizationService},
|
authz::{AuthorizationCheck, AuthorizationService},
|
||||||
dto::{
|
dto::{
|
||||||
common::{PaginatedResponse, PaginationParams},
|
common::{PaginatedResponse, PaginationParams},
|
||||||
ApiResponse, CreateIdentityRequest, CreatePermissionAssignmentRequest, IdentityResponse,
|
ApiResponse, CreateIdentityRequest, CreateIdentityRoleAssignmentRequest,
|
||||||
IdentitySummary, PermissionAssignmentResponse, PermissionSetQueryParams,
|
CreatePermissionAssignmentRequest, CreatePermissionSetRoleAssignmentRequest,
|
||||||
PermissionSetSummary, SuccessResponse, UpdateIdentityRequest,
|
IdentityResponse, IdentityRoleAssignmentResponse, IdentitySummary,
|
||||||
|
PermissionAssignmentResponse, PermissionSetQueryParams,
|
||||||
|
PermissionSetRoleAssignmentResponse, PermissionSetSummary, SuccessResponse,
|
||||||
|
UpdateIdentityRequest,
|
||||||
},
|
},
|
||||||
middleware::{ApiError, ApiResult},
|
middleware::{ApiError, ApiResult},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@@ -58,16 +63,22 @@ pub async fn list_identities(
|
|||||||
let page_items = if start >= identities.len() {
|
let page_items = if start >= identities.len() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
identities[start..end]
|
identities[start..end].to_vec()
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(IdentitySummary::from)
|
|
||||||
.collect()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut summaries = Vec::with_capacity(page_items.len());
|
||||||
|
for identity in page_items {
|
||||||
|
let role_assignments =
|
||||||
|
IdentityRoleAssignmentRepository::find_by_identity(&state.db, identity.id).await?;
|
||||||
|
let roles = role_assignments.into_iter().map(|ra| ra.role).collect();
|
||||||
|
let mut summary = IdentitySummary::from(identity);
|
||||||
|
summary.roles = roles;
|
||||||
|
summaries.push(summary);
|
||||||
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(PaginatedResponse::new(page_items, &query, total)),
|
Json(PaginatedResponse::new(summaries, &query, total)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +105,42 @@ pub async fn get_identity(
|
|||||||
let identity = IdentityRepository::find_by_id(&state.db, identity_id)
|
let identity = IdentityRepository::find_by_id(&state.db, identity_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Identity '{}' not found", identity_id)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Identity '{}' not found", identity_id)))?;
|
||||||
|
let roles = IdentityRoleAssignmentRepository::find_by_identity(&state.db, identity_id).await?;
|
||||||
|
let assignments =
|
||||||
|
PermissionAssignmentRepository::find_by_identity(&state.db, identity_id).await?;
|
||||||
|
let permission_sets = PermissionSetRepository::find_by_identity(&state.db, identity_id).await?;
|
||||||
|
let permission_set_refs = permission_sets
|
||||||
|
.into_iter()
|
||||||
|
.map(|ps| (ps.id, ps.r#ref))
|
||||||
|
.collect::<std::collections::HashMap<_, _>>();
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(ApiResponse::new(IdentityResponse::from(identity))),
|
Json(ApiResponse::new(IdentityResponse {
|
||||||
|
id: identity.id,
|
||||||
|
login: identity.login,
|
||||||
|
display_name: identity.display_name,
|
||||||
|
frozen: identity.frozen,
|
||||||
|
attributes: identity.attributes,
|
||||||
|
roles: roles
|
||||||
|
.into_iter()
|
||||||
|
.map(IdentityRoleAssignmentResponse::from)
|
||||||
|
.collect(),
|
||||||
|
direct_permissions: assignments
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|assignment| {
|
||||||
|
permission_set_refs.get(&assignment.permset).cloned().map(
|
||||||
|
|permission_set_ref| PermissionAssignmentResponse {
|
||||||
|
id: assignment.id,
|
||||||
|
identity_id: assignment.identity,
|
||||||
|
permission_set_id: assignment.permset,
|
||||||
|
permission_set_ref,
|
||||||
|
created: assignment.created,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +223,7 @@ pub async fn update_identity(
|
|||||||
display_name: request.display_name,
|
display_name: request.display_name,
|
||||||
password_hash,
|
password_hash,
|
||||||
attributes: request.attributes,
|
attributes: request.attributes,
|
||||||
|
frozen: request.frozen,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -257,10 +301,33 @@ pub async fn list_permission_sets(
|
|||||||
permission_sets.retain(|ps| ps.pack_ref.as_deref() == Some(pack_ref.as_str()));
|
permission_sets.retain(|ps| ps.pack_ref.as_deref() == Some(pack_ref.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: Vec<PermissionSetSummary> = permission_sets
|
let mut response = Vec::with_capacity(permission_sets.len());
|
||||||
.into_iter()
|
for permission_set in permission_sets {
|
||||||
.map(PermissionSetSummary::from)
|
let permission_set_ref = permission_set.r#ref.clone();
|
||||||
.collect();
|
let roles = PermissionSetRoleAssignmentRepository::find_by_permission_set(
|
||||||
|
&state.db,
|
||||||
|
permission_set.id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
response.push(PermissionSetSummary {
|
||||||
|
id: permission_set.id,
|
||||||
|
r#ref: permission_set.r#ref,
|
||||||
|
pack_ref: permission_set.pack_ref,
|
||||||
|
label: permission_set.label,
|
||||||
|
description: permission_set.description,
|
||||||
|
grants: permission_set.grants,
|
||||||
|
roles: roles
|
||||||
|
.into_iter()
|
||||||
|
.map(|assignment| PermissionSetRoleAssignmentResponse {
|
||||||
|
id: assignment.id,
|
||||||
|
permission_set_id: assignment.permset,
|
||||||
|
permission_set_ref: Some(permission_set_ref.clone()),
|
||||||
|
role: assignment.role,
|
||||||
|
created: assignment.created,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(response)))
|
Ok((StatusCode::OK, Json(response)))
|
||||||
}
|
}
|
||||||
@@ -412,6 +479,229 @@ pub async fn delete_permission_assignment(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/identities/{id}/roles",
|
||||||
|
tag = "permissions",
|
||||||
|
params(
|
||||||
|
("id" = i64, Path, description = "Identity ID")
|
||||||
|
),
|
||||||
|
request_body = CreateIdentityRoleAssignmentRequest,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Identity role assignment created", body = inline(ApiResponse<IdentityRoleAssignmentResponse>)),
|
||||||
|
(status = 404, description = "Identity not found")
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn create_identity_role_assignment(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
RequireAuth(user): RequireAuth,
|
||||||
|
Path(identity_id): Path<i64>,
|
||||||
|
Json(request): Json<CreateIdentityRoleAssignmentRequest>,
|
||||||
|
) -> ApiResult<impl IntoResponse> {
|
||||||
|
authorize_permissions(&state, &user, Resource::Permissions, Action::Manage).await?;
|
||||||
|
request.validate()?;
|
||||||
|
|
||||||
|
IdentityRepository::find_by_id(&state.db, identity_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| ApiError::NotFound(format!("Identity '{}' not found", identity_id)))?;
|
||||||
|
|
||||||
|
let assignment = IdentityRoleAssignmentRepository::create(
|
||||||
|
&state.db,
|
||||||
|
CreateIdentityRoleAssignmentInput {
|
||||||
|
identity: identity_id,
|
||||||
|
role: request.role,
|
||||||
|
source: "manual".to_string(),
|
||||||
|
managed: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(ApiResponse::new(IdentityRoleAssignmentResponse::from(
|
||||||
|
assignment,
|
||||||
|
))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/identities/roles/{id}",
|
||||||
|
tag = "permissions",
|
||||||
|
params(
|
||||||
|
("id" = i64, Path, description = "Identity role assignment ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Identity role assignment deleted", body = inline(ApiResponse<SuccessResponse>)),
|
||||||
|
(status = 404, description = "Identity role assignment not found")
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn delete_identity_role_assignment(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
RequireAuth(user): RequireAuth,
|
||||||
|
Path(assignment_id): Path<i64>,
|
||||||
|
) -> ApiResult<impl IntoResponse> {
|
||||||
|
authorize_permissions(&state, &user, Resource::Permissions, Action::Manage).await?;
|
||||||
|
|
||||||
|
let assignment = IdentityRoleAssignmentRepository::find_by_id(&state.db, assignment_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::NotFound(format!(
|
||||||
|
"Identity role assignment '{}' not found",
|
||||||
|
assignment_id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if assignment.managed {
|
||||||
|
return Err(ApiError::BadRequest(
|
||||||
|
"Managed role assignments must be updated through the identity provider sync"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentityRoleAssignmentRepository::delete(&state.db, assignment_id).await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(ApiResponse::new(SuccessResponse::new(
|
||||||
|
"Identity role assignment deleted successfully",
|
||||||
|
))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/permissions/sets/{id}/roles",
|
||||||
|
tag = "permissions",
|
||||||
|
params(
|
||||||
|
("id" = i64, Path, description = "Permission set ID")
|
||||||
|
),
|
||||||
|
request_body = CreatePermissionSetRoleAssignmentRequest,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Permission set role assignment created", body = inline(ApiResponse<PermissionSetRoleAssignmentResponse>)),
|
||||||
|
(status = 404, description = "Permission set not found")
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn create_permission_set_role_assignment(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
RequireAuth(user): RequireAuth,
|
||||||
|
Path(permission_set_id): Path<i64>,
|
||||||
|
Json(request): Json<CreatePermissionSetRoleAssignmentRequest>,
|
||||||
|
) -> ApiResult<impl IntoResponse> {
|
||||||
|
authorize_permissions(&state, &user, Resource::Permissions, Action::Manage).await?;
|
||||||
|
request.validate()?;
|
||||||
|
|
||||||
|
let permission_set = PermissionSetRepository::find_by_id(&state.db, permission_set_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::NotFound(format!("Permission set '{}' not found", permission_set_id))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let assignment = PermissionSetRoleAssignmentRepository::create(
|
||||||
|
&state.db,
|
||||||
|
CreatePermissionSetRoleAssignmentInput {
|
||||||
|
permset: permission_set_id,
|
||||||
|
role: request.role,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
Json(ApiResponse::new(PermissionSetRoleAssignmentResponse {
|
||||||
|
id: assignment.id,
|
||||||
|
permission_set_id: assignment.permset,
|
||||||
|
permission_set_ref: Some(permission_set.r#ref),
|
||||||
|
role: assignment.role,
|
||||||
|
created: assignment.created,
|
||||||
|
})),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/v1/permissions/sets/roles/{id}",
|
||||||
|
tag = "permissions",
|
||||||
|
params(
|
||||||
|
("id" = i64, Path, description = "Permission set role assignment ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Permission set role assignment deleted", body = inline(ApiResponse<SuccessResponse>)),
|
||||||
|
(status = 404, description = "Permission set role assignment not found")
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn delete_permission_set_role_assignment(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
RequireAuth(user): RequireAuth,
|
||||||
|
Path(assignment_id): Path<i64>,
|
||||||
|
) -> ApiResult<impl IntoResponse> {
|
||||||
|
authorize_permissions(&state, &user, Resource::Permissions, Action::Manage).await?;
|
||||||
|
|
||||||
|
PermissionSetRoleAssignmentRepository::find_by_id(&state.db, assignment_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::NotFound(format!(
|
||||||
|
"Permission set role assignment '{}' not found",
|
||||||
|
assignment_id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
PermissionSetRoleAssignmentRepository::delete(&state.db, assignment_id).await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(ApiResponse::new(SuccessResponse::new(
|
||||||
|
"Permission set role assignment deleted successfully",
|
||||||
|
))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/identities/{id}/freeze",
|
||||||
|
tag = "permissions",
|
||||||
|
params(
|
||||||
|
("id" = i64, Path, description = "Identity ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Identity frozen", body = inline(ApiResponse<SuccessResponse>)),
|
||||||
|
(status = 404, description = "Identity not found")
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn freeze_identity(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
RequireAuth(user): RequireAuth,
|
||||||
|
Path(identity_id): Path<i64>,
|
||||||
|
) -> ApiResult<impl IntoResponse> {
|
||||||
|
set_identity_frozen(&state, &user, identity_id, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/v1/identities/{id}/unfreeze",
|
||||||
|
tag = "permissions",
|
||||||
|
params(
|
||||||
|
("id" = i64, Path, description = "Identity ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Identity unfrozen", body = inline(ApiResponse<SuccessResponse>)),
|
||||||
|
(status = 404, description = "Identity not found")
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn unfreeze_identity(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
RequireAuth(user): RequireAuth,
|
||||||
|
Path(identity_id): Path<i64>,
|
||||||
|
) -> ApiResult<impl IntoResponse> {
|
||||||
|
set_identity_frozen(&state, &user, identity_id, false).await
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes() -> Router<Arc<AppState>> {
|
pub fn routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/identities", get(list_identities).post(create_identity))
|
.route("/identities", get(list_identities).post(create_identity))
|
||||||
@@ -421,11 +711,29 @@ pub fn routes() -> Router<Arc<AppState>> {
|
|||||||
.put(update_identity)
|
.put(update_identity)
|
||||||
.delete(delete_identity),
|
.delete(delete_identity),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/identities/{id}/roles",
|
||||||
|
post(create_identity_role_assignment),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/identities/{id}/permissions",
|
"/identities/{id}/permissions",
|
||||||
get(list_identity_permissions),
|
get(list_identity_permissions),
|
||||||
)
|
)
|
||||||
|
.route("/identities/{id}/freeze", post(freeze_identity))
|
||||||
|
.route("/identities/{id}/unfreeze", post(unfreeze_identity))
|
||||||
|
.route(
|
||||||
|
"/identities/roles/{id}",
|
||||||
|
delete(delete_identity_role_assignment),
|
||||||
|
)
|
||||||
.route("/permissions/sets", get(list_permission_sets))
|
.route("/permissions/sets", get(list_permission_sets))
|
||||||
|
.route(
|
||||||
|
"/permissions/sets/{id}/roles",
|
||||||
|
post(create_permission_set_role_assignment),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/permissions/sets/roles/{id}",
|
||||||
|
delete(delete_permission_set_role_assignment),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/permissions/assignments",
|
"/permissions/assignments",
|
||||||
post(create_permission_assignment),
|
post(create_permission_assignment),
|
||||||
@@ -488,20 +796,82 @@ impl From<Identity> for IdentitySummary {
|
|||||||
id: value.id,
|
id: value.id,
|
||||||
login: value.login,
|
login: value.login,
|
||||||
display_name: value.display_name,
|
display_name: value.display_name,
|
||||||
|
frozen: value.frozen,
|
||||||
attributes: value.attributes,
|
attributes: value.attributes,
|
||||||
|
roles: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PermissionSet> for PermissionSetSummary {
|
impl From<IdentityRoleAssignment> for IdentityRoleAssignmentResponse {
|
||||||
fn from(value: PermissionSet) -> Self {
|
fn from(value: IdentityRoleAssignment) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
r#ref: value.r#ref,
|
identity_id: value.identity,
|
||||||
pack_ref: value.pack_ref,
|
role: value.role,
|
||||||
label: value.label,
|
source: value.source,
|
||||||
description: value.description,
|
managed: value.managed,
|
||||||
grants: value.grants,
|
created: value.created,
|
||||||
|
updated: value.updated,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Identity> for IdentityResponse {
|
||||||
|
fn from(value: Identity) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
login: value.login,
|
||||||
|
display_name: value.display_name,
|
||||||
|
frozen: value.frozen,
|
||||||
|
attributes: value.attributes,
|
||||||
|
roles: Vec::new(),
|
||||||
|
direct_permissions: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_identity_frozen(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
user: &crate::auth::middleware::AuthenticatedUser,
|
||||||
|
identity_id: i64,
|
||||||
|
frozen: bool,
|
||||||
|
) -> ApiResult<impl IntoResponse> {
|
||||||
|
authorize_permissions(state, user, Resource::Identities, Action::Update).await?;
|
||||||
|
|
||||||
|
let caller_identity_id = user
|
||||||
|
.identity_id()
|
||||||
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
||||||
|
if caller_identity_id == identity_id && frozen {
|
||||||
|
return Err(ApiError::BadRequest(
|
||||||
|
"Refusing to freeze the currently authenticated identity".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentityRepository::find_by_id(&state.db, identity_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| ApiError::NotFound(format!("Identity '{}' not found", identity_id)))?;
|
||||||
|
|
||||||
|
IdentityRepository::update(
|
||||||
|
&state.db,
|
||||||
|
identity_id,
|
||||||
|
UpdateIdentityInput {
|
||||||
|
display_name: None,
|
||||||
|
password_hash: None,
|
||||||
|
attributes: None,
|
||||||
|
frozen: Some(frozen),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let message = if frozen {
|
||||||
|
"Identity frozen successfully"
|
||||||
|
} else {
|
||||||
|
"Identity unfrozen successfully"
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(ApiResponse::new(SuccessResponse::new(message))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use attune_common::repositories::{
|
|||||||
pack::PackRepository,
|
pack::PackRepository,
|
||||||
rule::{CreateRuleInput, RuleRepository, RuleSearchFilters, UpdateRuleInput},
|
rule::{CreateRuleInput, RuleRepository, RuleSearchFilters, UpdateRuleInput},
|
||||||
trigger::TriggerRepository,
|
trigger::TriggerRepository,
|
||||||
Create, Delete, FindByRef, Update,
|
Create, Delete, FindByRef, Patch, Update,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -474,7 +474,7 @@ pub async fn update_rule(
|
|||||||
// Create update input
|
// Create update input
|
||||||
let update_input = UpdateRuleInput {
|
let update_input = UpdateRuleInput {
|
||||||
label: request.label,
|
label: request.label,
|
||||||
description: request.description,
|
description: request.description.map(Patch::Set),
|
||||||
conditions: request.conditions,
|
conditions: request.conditions,
|
||||||
action_params: request.action_params,
|
action_params: request.action_params,
|
||||||
trigger_params: request.trigger_params,
|
trigger_params: request.trigger_params,
|
||||||
|
|||||||
@@ -724,7 +724,7 @@ pub async fn update_sensor(
|
|||||||
// Create update input
|
// Create update input
|
||||||
let update_input = UpdateSensorInput {
|
let update_input = UpdateSensorInput {
|
||||||
label: request.label,
|
label: request.label,
|
||||||
description: request.description,
|
description: request.description.map(Patch::Set),
|
||||||
entrypoint: request.entrypoint,
|
entrypoint: request.entrypoint,
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_ref: None,
|
runtime_ref: None,
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ use attune_common::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use attune_common::rbac::{Action, AuthorizationContext, Resource};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::middleware::RequireAuth,
|
auth::middleware::RequireAuth,
|
||||||
|
authz::{AuthorizationCheck, AuthorizationService},
|
||||||
dto::{
|
dto::{
|
||||||
trigger::TriggerResponse,
|
trigger::TriggerResponse,
|
||||||
webhook::{WebhookReceiverRequest, WebhookReceiverResponse},
|
webhook::{WebhookReceiverRequest, WebhookReceiverResponse},
|
||||||
@@ -170,7 +173,7 @@ fn get_webhook_config_array(
|
|||||||
)]
|
)]
|
||||||
pub async fn enable_webhook(
|
pub async fn enable_webhook(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
Path(trigger_ref): Path<String>,
|
Path(trigger_ref): Path<String>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// First, find the trigger by ref to get its ID
|
// First, find the trigger by ref to get its ID
|
||||||
@@ -179,6 +182,26 @@ pub async fn enable_webhook(
|
|||||||
.map_err(|e| ApiError::InternalServerError(e.to_string()))?
|
.map_err(|e| ApiError::InternalServerError(e.to_string()))?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
||||||
|
|
||||||
|
if user.claims.token_type == crate::auth::jwt::TokenType::Access {
|
||||||
|
let identity_id = user
|
||||||
|
.identity_id()
|
||||||
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
||||||
|
let authz = AuthorizationService::new(state.db.clone());
|
||||||
|
let mut ctx = AuthorizationContext::new(identity_id);
|
||||||
|
ctx.target_ref = Some(trigger.r#ref.clone());
|
||||||
|
ctx.pack_ref = trigger.pack_ref.clone();
|
||||||
|
authz
|
||||||
|
.authorize(
|
||||||
|
&user,
|
||||||
|
AuthorizationCheck {
|
||||||
|
resource: Resource::Triggers,
|
||||||
|
action: Action::Update,
|
||||||
|
context: ctx,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Enable webhooks for this trigger
|
// Enable webhooks for this trigger
|
||||||
let _webhook_info = TriggerRepository::enable_webhook(&state.db, trigger.id)
|
let _webhook_info = TriggerRepository::enable_webhook(&state.db, trigger.id)
|
||||||
.await
|
.await
|
||||||
@@ -213,7 +236,7 @@ pub async fn enable_webhook(
|
|||||||
)]
|
)]
|
||||||
pub async fn disable_webhook(
|
pub async fn disable_webhook(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
Path(trigger_ref): Path<String>,
|
Path(trigger_ref): Path<String>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// First, find the trigger by ref to get its ID
|
// First, find the trigger by ref to get its ID
|
||||||
@@ -222,6 +245,26 @@ pub async fn disable_webhook(
|
|||||||
.map_err(|e| ApiError::InternalServerError(e.to_string()))?
|
.map_err(|e| ApiError::InternalServerError(e.to_string()))?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
||||||
|
|
||||||
|
if user.claims.token_type == crate::auth::jwt::TokenType::Access {
|
||||||
|
let identity_id = user
|
||||||
|
.identity_id()
|
||||||
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
||||||
|
let authz = AuthorizationService::new(state.db.clone());
|
||||||
|
let mut ctx = AuthorizationContext::new(identity_id);
|
||||||
|
ctx.target_ref = Some(trigger.r#ref.clone());
|
||||||
|
ctx.pack_ref = trigger.pack_ref.clone();
|
||||||
|
authz
|
||||||
|
.authorize(
|
||||||
|
&user,
|
||||||
|
AuthorizationCheck {
|
||||||
|
resource: Resource::Triggers,
|
||||||
|
action: Action::Update,
|
||||||
|
context: ctx,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Disable webhooks for this trigger
|
// Disable webhooks for this trigger
|
||||||
TriggerRepository::disable_webhook(&state.db, trigger.id)
|
TriggerRepository::disable_webhook(&state.db, trigger.id)
|
||||||
.await
|
.await
|
||||||
@@ -257,7 +300,7 @@ pub async fn disable_webhook(
|
|||||||
)]
|
)]
|
||||||
pub async fn regenerate_webhook_key(
|
pub async fn regenerate_webhook_key(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
RequireAuth(_user): RequireAuth,
|
RequireAuth(user): RequireAuth,
|
||||||
Path(trigger_ref): Path<String>,
|
Path(trigger_ref): Path<String>,
|
||||||
) -> ApiResult<impl IntoResponse> {
|
) -> ApiResult<impl IntoResponse> {
|
||||||
// First, find the trigger by ref to get its ID
|
// First, find the trigger by ref to get its ID
|
||||||
@@ -266,6 +309,26 @@ pub async fn regenerate_webhook_key(
|
|||||||
.map_err(|e| ApiError::InternalServerError(e.to_string()))?
|
.map_err(|e| ApiError::InternalServerError(e.to_string()))?
|
||||||
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
||||||
|
|
||||||
|
if user.claims.token_type == crate::auth::jwt::TokenType::Access {
|
||||||
|
let identity_id = user
|
||||||
|
.identity_id()
|
||||||
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
||||||
|
let authz = AuthorizationService::new(state.db.clone());
|
||||||
|
let mut ctx = AuthorizationContext::new(identity_id);
|
||||||
|
ctx.target_ref = Some(trigger.r#ref.clone());
|
||||||
|
ctx.pack_ref = trigger.pack_ref.clone();
|
||||||
|
authz
|
||||||
|
.authorize(
|
||||||
|
&user,
|
||||||
|
AuthorizationCheck {
|
||||||
|
resource: Resource::Triggers,
|
||||||
|
action: Action::Update,
|
||||||
|
context: ctx,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if webhooks are enabled
|
// Check if webhooks are enabled
|
||||||
if !trigger.webhook_enabled {
|
if !trigger.webhook_enabled {
|
||||||
return Err(ApiError::BadRequest(
|
return Err(ApiError::BadRequest(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use attune_common::repositories::{
|
|||||||
CreateWorkflowDefinitionInput, UpdateWorkflowDefinitionInput, WorkflowDefinitionRepository,
|
CreateWorkflowDefinitionInput, UpdateWorkflowDefinitionInput, WorkflowDefinitionRepository,
|
||||||
WorkflowSearchFilters,
|
WorkflowSearchFilters,
|
||||||
},
|
},
|
||||||
Create, Delete, FindByRef, Update,
|
Create, Delete, FindByRef, Patch, Update,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -217,7 +217,7 @@ pub async fn create_workflow(
|
|||||||
pack.id,
|
pack.id,
|
||||||
&pack.r#ref,
|
&pack.r#ref,
|
||||||
&request.label,
|
&request.label,
|
||||||
&request.description.clone().unwrap_or_default(),
|
request.description.as_deref(),
|
||||||
"workflow",
|
"workflow",
|
||||||
request.param_schema.as_ref(),
|
request.param_schema.as_ref(),
|
||||||
request.out_schema.as_ref(),
|
request.out_schema.as_ref(),
|
||||||
@@ -416,7 +416,7 @@ pub async fn save_workflow_file(
|
|||||||
pack.id,
|
pack.id,
|
||||||
&pack.r#ref,
|
&pack.r#ref,
|
||||||
&request.label,
|
&request.label,
|
||||||
&request.description.clone().unwrap_or_default(),
|
request.description.as_deref(),
|
||||||
&entrypoint,
|
&entrypoint,
|
||||||
request.param_schema.as_ref(),
|
request.param_schema.as_ref(),
|
||||||
request.out_schema.as_ref(),
|
request.out_schema.as_ref(),
|
||||||
@@ -499,7 +499,7 @@ pub async fn update_workflow_file(
|
|||||||
pack.id,
|
pack.id,
|
||||||
&pack.r#ref,
|
&pack.r#ref,
|
||||||
&request.label,
|
&request.label,
|
||||||
&request.description.unwrap_or_default(),
|
request.description.as_deref(),
|
||||||
&entrypoint,
|
&entrypoint,
|
||||||
request.param_schema.as_ref(),
|
request.param_schema.as_ref(),
|
||||||
request.out_schema.as_ref(),
|
request.out_schema.as_ref(),
|
||||||
@@ -702,7 +702,7 @@ async fn create_companion_action(
|
|||||||
pack_id: i64,
|
pack_id: i64,
|
||||||
pack_ref: &str,
|
pack_ref: &str,
|
||||||
label: &str,
|
label: &str,
|
||||||
description: &str,
|
description: Option<&str>,
|
||||||
entrypoint: &str,
|
entrypoint: &str,
|
||||||
param_schema: Option<&serde_json::Value>,
|
param_schema: Option<&serde_json::Value>,
|
||||||
out_schema: Option<&serde_json::Value>,
|
out_schema: Option<&serde_json::Value>,
|
||||||
@@ -713,7 +713,7 @@ async fn create_companion_action(
|
|||||||
pack: pack_id,
|
pack: pack_id,
|
||||||
pack_ref: pack_ref.to_string(),
|
pack_ref: pack_ref.to_string(),
|
||||||
label: label.to_string(),
|
label: label.to_string(),
|
||||||
description: description.to_string(),
|
description: description.map(|s| s.to_string()),
|
||||||
entrypoint: entrypoint.to_string(),
|
entrypoint: entrypoint.to_string(),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
@@ -787,7 +787,7 @@ async fn update_companion_action(
|
|||||||
if let Some(action) = existing_action {
|
if let Some(action) = existing_action {
|
||||||
let update_input = UpdateActionInput {
|
let update_input = UpdateActionInput {
|
||||||
label: label.map(|s| s.to_string()),
|
label: label.map(|s| s.to_string()),
|
||||||
description: description.map(|s| s.to_string()),
|
description: description.map(|s| Patch::Set(s.to_string())),
|
||||||
entrypoint: None,
|
entrypoint: None,
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
@@ -838,7 +838,7 @@ async fn ensure_companion_action(
|
|||||||
pack_id: i64,
|
pack_id: i64,
|
||||||
pack_ref: &str,
|
pack_ref: &str,
|
||||||
label: &str,
|
label: &str,
|
||||||
description: &str,
|
description: Option<&str>,
|
||||||
entrypoint: &str,
|
entrypoint: &str,
|
||||||
param_schema: Option<&serde_json::Value>,
|
param_schema: Option<&serde_json::Value>,
|
||||||
out_schema: Option<&serde_json::Value>,
|
out_schema: Option<&serde_json::Value>,
|
||||||
@@ -853,7 +853,10 @@ async fn ensure_companion_action(
|
|||||||
// Update existing companion action
|
// Update existing companion action
|
||||||
let update_input = UpdateActionInput {
|
let update_input = UpdateActionInput {
|
||||||
label: Some(label.to_string()),
|
label: Some(label.to_string()),
|
||||||
description: Some(description.to_string()),
|
description: Some(match description {
|
||||||
|
Some(description) => Patch::Set(description.to_string()),
|
||||||
|
None => Patch::Clear,
|
||||||
|
}),
|
||||||
entrypoint: Some(entrypoint.to_string()),
|
entrypoint: Some(entrypoint.to_string()),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ mod tests {
|
|||||||
pack: 1,
|
pack: 1,
|
||||||
pack_ref: "test".to_string(),
|
pack_ref: "test".to_string(),
|
||||||
label: "Test Action".to_string(),
|
label: "Test Action".to_string(),
|
||||||
description: "Test action".to_string(),
|
description: Some("Test action".to_string()),
|
||||||
entrypoint: "test.sh".to_string(),
|
entrypoint: "test.sh".to_string(),
|
||||||
runtime: Some(1),
|
runtime: Some(1),
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ impl TestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create and authenticate a test user
|
/// Create and authenticate a test user
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn with_auth(mut self) -> Result<Self> {
|
pub async fn with_auth(mut self) -> Result<Self> {
|
||||||
// Generate unique username to avoid conflicts in parallel tests
|
// Generate unique username to avoid conflicts in parallel tests
|
||||||
let unique_id = uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string();
|
let unique_id = uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string();
|
||||||
@@ -394,6 +395,7 @@ impl TestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get authenticated token
|
/// Get authenticated token
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn token(&self) -> Option<&str> {
|
pub fn token(&self) -> Option<&str> {
|
||||||
self.token.as_deref()
|
self.token.as_deref()
|
||||||
}
|
}
|
||||||
@@ -495,7 +497,7 @@ pub async fn create_test_action(pool: &PgPool, pack_id: i64, ref_name: &str) ->
|
|||||||
pack: pack_id,
|
pack: pack_id,
|
||||||
pack_ref: format!("pack_{}", pack_id),
|
pack_ref: format!("pack_{}", pack_id),
|
||||||
label: format!("Test Action {}", ref_name),
|
label: format!("Test Action {}", ref_name),
|
||||||
description: format!("Test action for {}", ref_name),
|
description: Some(format!("Test action for {}", ref_name)),
|
||||||
entrypoint: "main.py".to_string(),
|
entrypoint: "main.py".to_string(),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
276
crates/api/tests/rbac_scoped_resources_api_tests.rs
Normal file
276
crates/api/tests/rbac_scoped_resources_api_tests.rs
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
use axum::http::StatusCode;
|
||||||
|
use helpers::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use attune_common::{
|
||||||
|
models::enums::{ArtifactType, ArtifactVisibility, OwnerType, RetentionPolicyType},
|
||||||
|
repositories::{
|
||||||
|
artifact::{ArtifactRepository, CreateArtifactInput},
|
||||||
|
identity::{
|
||||||
|
CreatePermissionAssignmentInput, CreatePermissionSetInput, IdentityRepository,
|
||||||
|
PermissionAssignmentRepository, PermissionSetRepository,
|
||||||
|
},
|
||||||
|
key::{CreateKeyInput, KeyRepository},
|
||||||
|
Create,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
async fn register_scoped_user(
|
||||||
|
ctx: &TestContext,
|
||||||
|
login: &str,
|
||||||
|
grants: serde_json::Value,
|
||||||
|
) -> Result<String> {
|
||||||
|
let response = ctx
|
||||||
|
.post(
|
||||||
|
"/auth/register",
|
||||||
|
json!({
|
||||||
|
"login": login,
|
||||||
|
"password": "TestPassword123!",
|
||||||
|
"display_name": format!("Scoped User {}", login),
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::CREATED);
|
||||||
|
let body: serde_json::Value = response.json().await?;
|
||||||
|
let token = body["data"]["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.expect("missing access token")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let identity = IdentityRepository::find_by_login(&ctx.pool, login)
|
||||||
|
.await?
|
||||||
|
.expect("registered identity should exist");
|
||||||
|
|
||||||
|
let permset = PermissionSetRepository::create(
|
||||||
|
&ctx.pool,
|
||||||
|
CreatePermissionSetInput {
|
||||||
|
r#ref: format!("test.scoped_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
pack: None,
|
||||||
|
pack_ref: None,
|
||||||
|
label: Some("Scoped Test Permission Set".to_string()),
|
||||||
|
description: Some("Scoped test grants".to_string()),
|
||||||
|
grants,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
PermissionAssignmentRepository::create(
|
||||||
|
&ctx.pool,
|
||||||
|
CreatePermissionAssignmentInput {
|
||||||
|
identity: identity.id,
|
||||||
|
permset: permset.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "integration test — requires database"]
|
||||||
|
async fn test_pack_scoped_key_permissions_enforce_owner_refs() {
|
||||||
|
let ctx = TestContext::new()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create test context");
|
||||||
|
|
||||||
|
let token = register_scoped_user(
|
||||||
|
&ctx,
|
||||||
|
&format!("scoped_keys_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
json!([
|
||||||
|
{
|
||||||
|
"resource": "keys",
|
||||||
|
"actions": ["read"],
|
||||||
|
"constraints": {
|
||||||
|
"owner_types": ["pack"],
|
||||||
|
"owner_refs": ["python_example"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to register scoped user");
|
||||||
|
|
||||||
|
KeyRepository::create(
|
||||||
|
&ctx.pool,
|
||||||
|
CreateKeyInput {
|
||||||
|
r#ref: format!("python_example_key_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
owner_type: OwnerType::Pack,
|
||||||
|
owner: Some("python_example".to_string()),
|
||||||
|
owner_identity: None,
|
||||||
|
owner_pack: None,
|
||||||
|
owner_pack_ref: Some("python_example".to_string()),
|
||||||
|
owner_action: None,
|
||||||
|
owner_action_ref: None,
|
||||||
|
owner_sensor: None,
|
||||||
|
owner_sensor_ref: None,
|
||||||
|
name: "Python Example Key".to_string(),
|
||||||
|
encrypted: false,
|
||||||
|
encryption_key_hash: None,
|
||||||
|
value: json!("allowed"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create scoped key");
|
||||||
|
|
||||||
|
let blocked_key = KeyRepository::create(
|
||||||
|
&ctx.pool,
|
||||||
|
CreateKeyInput {
|
||||||
|
r#ref: format!("other_pack_key_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
owner_type: OwnerType::Pack,
|
||||||
|
owner: Some("other_pack".to_string()),
|
||||||
|
owner_identity: None,
|
||||||
|
owner_pack: None,
|
||||||
|
owner_pack_ref: Some("other_pack".to_string()),
|
||||||
|
owner_action: None,
|
||||||
|
owner_action_ref: None,
|
||||||
|
owner_sensor: None,
|
||||||
|
owner_sensor_ref: None,
|
||||||
|
name: "Other Pack Key".to_string(),
|
||||||
|
encrypted: false,
|
||||||
|
encryption_key_hash: None,
|
||||||
|
value: json!("blocked"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create blocked key");
|
||||||
|
|
||||||
|
let allowed_list = ctx
|
||||||
|
.get("/api/v1/keys", Some(&token))
|
||||||
|
.await
|
||||||
|
.expect("Failed to list keys");
|
||||||
|
assert_eq!(allowed_list.status(), StatusCode::OK);
|
||||||
|
let allowed_body: serde_json::Value = allowed_list.json().await.expect("Invalid key list");
|
||||||
|
assert_eq!(
|
||||||
|
allowed_body["data"]
|
||||||
|
.as_array()
|
||||||
|
.expect("expected list")
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert_eq!(allowed_body["data"][0]["owner"], "python_example");
|
||||||
|
|
||||||
|
let blocked_get = ctx
|
||||||
|
.get(&format!("/api/v1/keys/{}", blocked_key.r#ref), Some(&token))
|
||||||
|
.await
|
||||||
|
.expect("Failed to fetch blocked key");
|
||||||
|
assert_eq!(blocked_get.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "integration test — requires database"]
|
||||||
|
async fn test_pack_scoped_artifact_permissions_enforce_owner_refs() {
|
||||||
|
let ctx = TestContext::new()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create test context");
|
||||||
|
|
||||||
|
let token = register_scoped_user(
|
||||||
|
&ctx,
|
||||||
|
&format!("scoped_artifacts_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
json!([
|
||||||
|
{
|
||||||
|
"resource": "artifacts",
|
||||||
|
"actions": ["read", "create"],
|
||||||
|
"constraints": {
|
||||||
|
"owner_types": ["pack"],
|
||||||
|
"owner_refs": ["python_example"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to register scoped user");
|
||||||
|
|
||||||
|
let allowed_artifact = ArtifactRepository::create(
|
||||||
|
&ctx.pool,
|
||||||
|
CreateArtifactInput {
|
||||||
|
r#ref: format!("python_example.allowed_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
scope: OwnerType::Pack,
|
||||||
|
owner: "python_example".to_string(),
|
||||||
|
r#type: ArtifactType::FileText,
|
||||||
|
visibility: ArtifactVisibility::Private,
|
||||||
|
retention_policy: RetentionPolicyType::Versions,
|
||||||
|
retention_limit: 5,
|
||||||
|
name: Some("Allowed Artifact".to_string()),
|
||||||
|
description: None,
|
||||||
|
content_type: Some("text/plain".to_string()),
|
||||||
|
execution: None,
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create allowed artifact");
|
||||||
|
|
||||||
|
let blocked_artifact = ArtifactRepository::create(
|
||||||
|
&ctx.pool,
|
||||||
|
CreateArtifactInput {
|
||||||
|
r#ref: format!("other_pack.blocked_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
scope: OwnerType::Pack,
|
||||||
|
owner: "other_pack".to_string(),
|
||||||
|
r#type: ArtifactType::FileText,
|
||||||
|
visibility: ArtifactVisibility::Private,
|
||||||
|
retention_policy: RetentionPolicyType::Versions,
|
||||||
|
retention_limit: 5,
|
||||||
|
name: Some("Blocked Artifact".to_string()),
|
||||||
|
description: None,
|
||||||
|
content_type: Some("text/plain".to_string()),
|
||||||
|
execution: None,
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create blocked artifact");
|
||||||
|
|
||||||
|
let allowed_get = ctx
|
||||||
|
.get(
|
||||||
|
&format!("/api/v1/artifacts/{}", allowed_artifact.id),
|
||||||
|
Some(&token),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to fetch allowed artifact");
|
||||||
|
assert_eq!(allowed_get.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let blocked_get = ctx
|
||||||
|
.get(
|
||||||
|
&format!("/api/v1/artifacts/{}", blocked_artifact.id),
|
||||||
|
Some(&token),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to fetch blocked artifact");
|
||||||
|
assert_eq!(blocked_get.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let create_allowed = ctx
|
||||||
|
.post(
|
||||||
|
"/api/v1/artifacts",
|
||||||
|
json!({
|
||||||
|
"ref": format!("python_example.created_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
"scope": "pack",
|
||||||
|
"owner": "python_example",
|
||||||
|
"type": "file_text",
|
||||||
|
"name": "Created Artifact"
|
||||||
|
}),
|
||||||
|
Some(&token),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create allowed artifact");
|
||||||
|
assert_eq!(create_allowed.status(), StatusCode::CREATED);
|
||||||
|
|
||||||
|
let create_blocked = ctx
|
||||||
|
.post(
|
||||||
|
"/api/v1/artifacts",
|
||||||
|
json!({
|
||||||
|
"ref": format!("other_pack.created_{}", uuid::Uuid::new_v4().simple()),
|
||||||
|
"scope": "pack",
|
||||||
|
"owner": "other_pack",
|
||||||
|
"type": "file_text",
|
||||||
|
"name": "Blocked Artifact"
|
||||||
|
}),
|
||||||
|
Some(&token),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create blocked artifact");
|
||||||
|
assert_eq!(create_blocked.status(), StatusCode::FORBIDDEN);
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ async fn setup_test_pack_and_action(pool: &PgPool) -> Result<(Pack, Action)> {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Action".to_string(),
|
label: "Test Action".to_string(),
|
||||||
description: "Test action for SSE tests".to_string(),
|
description: Some("Test action for SSE tests".to_string()),
|
||||||
entrypoint: "test.sh".to_string(),
|
entrypoint: "test.sh".to_string(),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ struct Action {
|
|||||||
action_ref: String,
|
action_ref: String,
|
||||||
pack_ref: String,
|
pack_ref: String,
|
||||||
label: String,
|
label: String,
|
||||||
description: String,
|
description: Option<String>,
|
||||||
entrypoint: String,
|
entrypoint: String,
|
||||||
runtime: Option<i64>,
|
runtime: Option<i64>,
|
||||||
created: String,
|
created: String,
|
||||||
@@ -105,7 +105,7 @@ struct ActionDetail {
|
|||||||
pack: i64,
|
pack: i64,
|
||||||
pack_ref: String,
|
pack_ref: String,
|
||||||
label: String,
|
label: String,
|
||||||
description: String,
|
description: Option<String>,
|
||||||
entrypoint: String,
|
entrypoint: String,
|
||||||
runtime: Option<i64>,
|
runtime: Option<i64>,
|
||||||
param_schema: Option<serde_json::Value>,
|
param_schema: Option<serde_json::Value>,
|
||||||
@@ -253,7 +253,7 @@ async fn handle_list(
|
|||||||
.runtime
|
.runtime
|
||||||
.map(|r| r.to_string())
|
.map(|r| r.to_string())
|
||||||
.unwrap_or_else(|| "none".to_string()),
|
.unwrap_or_else(|| "none".to_string()),
|
||||||
output::truncate(&action.description, 40),
|
output::truncate(&action.description.unwrap_or_default(), 40),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +288,10 @@ async fn handle_show(
|
|||||||
("Reference", action.action_ref.clone()),
|
("Reference", action.action_ref.clone()),
|
||||||
("Pack", action.pack_ref.clone()),
|
("Pack", action.pack_ref.clone()),
|
||||||
("Label", action.label.clone()),
|
("Label", action.label.clone()),
|
||||||
("Description", action.description.clone()),
|
(
|
||||||
|
"Description",
|
||||||
|
action.description.unwrap_or_else(|| "None".to_string()),
|
||||||
|
),
|
||||||
("Entry Point", action.entrypoint.clone()),
|
("Entry Point", action.entrypoint.clone()),
|
||||||
(
|
(
|
||||||
"Runtime",
|
"Runtime",
|
||||||
@@ -356,7 +359,10 @@ async fn handle_update(
|
|||||||
("Ref", action.action_ref.clone()),
|
("Ref", action.action_ref.clone()),
|
||||||
("Pack", action.pack_ref.clone()),
|
("Pack", action.pack_ref.clone()),
|
||||||
("Label", action.label.clone()),
|
("Label", action.label.clone()),
|
||||||
("Description", action.description.clone()),
|
(
|
||||||
|
"Description",
|
||||||
|
action.description.unwrap_or_else(|| "None".to_string()),
|
||||||
|
),
|
||||||
("Entrypoint", action.entrypoint.clone()),
|
("Entrypoint", action.entrypoint.clone()),
|
||||||
(
|
(
|
||||||
"Runtime",
|
"Runtime",
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ struct Rule {
|
|||||||
pack: Option<i64>,
|
pack: Option<i64>,
|
||||||
pack_ref: String,
|
pack_ref: String,
|
||||||
label: String,
|
label: String,
|
||||||
description: String,
|
description: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
trigger: Option<i64>,
|
trigger: Option<i64>,
|
||||||
trigger_ref: String,
|
trigger_ref: String,
|
||||||
@@ -133,7 +133,7 @@ struct RuleDetail {
|
|||||||
pack: Option<i64>,
|
pack: Option<i64>,
|
||||||
pack_ref: String,
|
pack_ref: String,
|
||||||
label: String,
|
label: String,
|
||||||
description: String,
|
description: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
trigger: Option<i64>,
|
trigger: Option<i64>,
|
||||||
trigger_ref: String,
|
trigger_ref: String,
|
||||||
@@ -321,7 +321,10 @@ async fn handle_show(
|
|||||||
("Ref", rule.rule_ref.clone()),
|
("Ref", rule.rule_ref.clone()),
|
||||||
("Pack", rule.pack_ref.clone()),
|
("Pack", rule.pack_ref.clone()),
|
||||||
("Label", rule.label.clone()),
|
("Label", rule.label.clone()),
|
||||||
("Description", rule.description.clone()),
|
(
|
||||||
|
"Description",
|
||||||
|
rule.description.unwrap_or_else(|| "None".to_string()),
|
||||||
|
),
|
||||||
("Trigger", rule.trigger_ref.clone()),
|
("Trigger", rule.trigger_ref.clone()),
|
||||||
("Action", rule.action_ref.clone()),
|
("Action", rule.action_ref.clone()),
|
||||||
("Enabled", output::format_bool(rule.enabled)),
|
("Enabled", output::format_bool(rule.enabled)),
|
||||||
@@ -440,7 +443,10 @@ async fn handle_update(
|
|||||||
("Ref", rule.rule_ref.clone()),
|
("Ref", rule.rule_ref.clone()),
|
||||||
("Pack", rule.pack_ref.clone()),
|
("Pack", rule.pack_ref.clone()),
|
||||||
("Label", rule.label.clone()),
|
("Label", rule.label.clone()),
|
||||||
("Description", rule.description.clone()),
|
(
|
||||||
|
"Description",
|
||||||
|
rule.description.unwrap_or_else(|| "None".to_string()),
|
||||||
|
),
|
||||||
("Trigger", rule.trigger_ref.clone()),
|
("Trigger", rule.trigger_ref.clone()),
|
||||||
("Action", rule.action_ref.clone()),
|
("Action", rule.action_ref.clone()),
|
||||||
("Enabled", output::format_bool(rule.enabled)),
|
("Enabled", output::format_bool(rule.enabled)),
|
||||||
|
|||||||
@@ -444,13 +444,55 @@ pub mod runtime {
|
|||||||
|
|
||||||
/// Optional environment variables to set during action execution.
|
/// Optional environment variables to set during action execution.
|
||||||
///
|
///
|
||||||
/// Values support the same template variables as other fields:
|
/// Entries support the same template variables as other fields:
|
||||||
/// `{pack_dir}`, `{env_dir}`, `{interpreter}`, `{manifest_path}`.
|
/// `{pack_dir}`, `{env_dir}`, `{interpreter}`, `{manifest_path}`.
|
||||||
///
|
///
|
||||||
/// Example: `{"NODE_PATH": "{env_dir}/node_modules"}` ensures Node.js
|
/// The shorthand string form replaces the variable entirely:
|
||||||
/// can find packages installed in the isolated runtime environment.
|
/// `{"NODE_PATH": "{env_dir}/node_modules"}`
|
||||||
|
///
|
||||||
|
/// The object form supports declarative merge semantics:
|
||||||
|
/// `{"PYTHONPATH": {"value": "{pack_dir}/lib", "operation": "prepend"}}`
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub env_vars: HashMap<String, String>,
|
pub env_vars: HashMap<String, RuntimeEnvVarConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Declarative configuration for a single runtime environment variable.
|
||||||
|
///
|
||||||
|
/// The string form is shorthand for `{ "value": "...", "operation": "set" }`.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum RuntimeEnvVarConfig {
|
||||||
|
Value(String),
|
||||||
|
Spec(RuntimeEnvVarSpec),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full configuration for a runtime environment variable.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct RuntimeEnvVarSpec {
|
||||||
|
/// Template value to resolve for this variable.
|
||||||
|
pub value: String,
|
||||||
|
|
||||||
|
/// How the resolved value should be merged with any existing value.
|
||||||
|
#[serde(default)]
|
||||||
|
pub operation: RuntimeEnvVarOperation,
|
||||||
|
|
||||||
|
/// Separator used for prepend/append operations.
|
||||||
|
#[serde(default = "default_env_var_separator")]
|
||||||
|
pub separator: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge behavior for runtime-provided environment variables.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RuntimeEnvVarOperation {
|
||||||
|
#[default]
|
||||||
|
Set,
|
||||||
|
Prepend,
|
||||||
|
Append,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_env_var_separator() -> String {
|
||||||
|
":".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls how inline code is materialized before execution.
|
/// Controls how inline code is materialized before execution.
|
||||||
@@ -768,6 +810,43 @@ pub mod runtime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RuntimeEnvVarConfig {
|
||||||
|
/// Resolve this environment variable against the current template
|
||||||
|
/// variables and any existing value already present in the process env.
|
||||||
|
pub fn resolve(
|
||||||
|
&self,
|
||||||
|
vars: &HashMap<&str, String>,
|
||||||
|
existing_value: Option<&str>,
|
||||||
|
) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Value(value) => RuntimeExecutionConfig::resolve_template(value, vars),
|
||||||
|
Self::Spec(spec) => {
|
||||||
|
let resolved = RuntimeExecutionConfig::resolve_template(&spec.value, vars);
|
||||||
|
match spec.operation {
|
||||||
|
RuntimeEnvVarOperation::Set => resolved,
|
||||||
|
RuntimeEnvVarOperation::Prepend => {
|
||||||
|
join_env_var_values(&resolved, existing_value, &spec.separator)
|
||||||
|
}
|
||||||
|
RuntimeEnvVarOperation::Append => join_env_var_values(
|
||||||
|
existing_value.unwrap_or_default(),
|
||||||
|
Some(&resolved),
|
||||||
|
&spec.separator,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_env_var_values(left: &str, right: Option<&str>, separator: &str) -> String {
|
||||||
|
match (left.is_empty(), right.unwrap_or_default().is_empty()) {
|
||||||
|
(true, true) => String::new(),
|
||||||
|
(false, true) => left.to_string(),
|
||||||
|
(true, false) => right.unwrap_or_default().to_string(),
|
||||||
|
(false, false) => format!("{}{}{}", left, separator, right.unwrap_or_default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
pub struct Runtime {
|
pub struct Runtime {
|
||||||
pub id: Id,
|
pub id: Id,
|
||||||
@@ -887,7 +966,7 @@ pub mod trigger {
|
|||||||
pub pack: Option<Id>,
|
pub pack: Option<Id>,
|
||||||
pub pack_ref: Option<String>,
|
pub pack_ref: Option<String>,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub runtime: Id,
|
pub runtime: Id,
|
||||||
pub runtime_ref: String,
|
pub runtime_ref: String,
|
||||||
@@ -915,7 +994,7 @@ pub mod action {
|
|||||||
pub pack: Id,
|
pub pack: Id,
|
||||||
pub pack_ref: String,
|
pub pack_ref: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub runtime: Option<Id>,
|
pub runtime: Option<Id>,
|
||||||
/// Optional semver version constraint for the runtime
|
/// Optional semver version constraint for the runtime
|
||||||
@@ -965,7 +1044,7 @@ pub mod rule {
|
|||||||
pub pack: Id,
|
pub pack: Id,
|
||||||
pub pack_ref: String,
|
pub pack_ref: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
pub action: Option<Id>,
|
pub action: Option<Id>,
|
||||||
pub action_ref: String,
|
pub action_ref: String,
|
||||||
pub trigger: Option<Id>,
|
pub trigger: Option<Id>,
|
||||||
@@ -1221,6 +1300,7 @@ pub mod identity {
|
|||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub password_hash: Option<String>,
|
pub password_hash: Option<String>,
|
||||||
pub attributes: JsonDict,
|
pub attributes: JsonDict,
|
||||||
|
pub frozen: bool,
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
pub updated: DateTime<Utc>,
|
pub updated: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
@@ -1245,6 +1325,25 @@ pub mod identity {
|
|||||||
pub permset: Id,
|
pub permset: Id,
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct IdentityRoleAssignment {
|
||||||
|
pub id: Id,
|
||||||
|
pub identity: Id,
|
||||||
|
pub role: String,
|
||||||
|
pub source: String,
|
||||||
|
pub managed: bool,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub updated: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct PermissionSetRoleAssignment {
|
||||||
|
pub id: Id,
|
||||||
|
pub permset: Id,
|
||||||
|
pub role: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Key/Value storage
|
/// Key/Value storage
|
||||||
@@ -1620,3 +1719,68 @@ pub mod entity_history {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::runtime::{
|
||||||
|
RuntimeEnvVarConfig, RuntimeEnvVarOperation, RuntimeEnvVarSpec, RuntimeExecutionConfig,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_execution_config_env_vars_accept_string_and_object_forms() {
|
||||||
|
let config: RuntimeExecutionConfig = serde_json::from_value(json!({
|
||||||
|
"env_vars": {
|
||||||
|
"NODE_PATH": "{env_dir}/node_modules",
|
||||||
|
"PYTHONPATH": {
|
||||||
|
"value": "{pack_dir}/lib",
|
||||||
|
"operation": "prepend",
|
||||||
|
"separator": ":"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.expect("runtime execution config should deserialize");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
config.env_vars.get("NODE_PATH"),
|
||||||
|
Some(RuntimeEnvVarConfig::Value(value)) if value == "{env_dir}/node_modules"
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
config.env_vars.get("PYTHONPATH"),
|
||||||
|
Some(RuntimeEnvVarConfig::Spec(RuntimeEnvVarSpec {
|
||||||
|
value,
|
||||||
|
operation: RuntimeEnvVarOperation::Prepend,
|
||||||
|
separator,
|
||||||
|
})) if value == "{pack_dir}/lib" && separator == ":"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_env_var_config_resolves_prepend_and_append_against_existing_values() {
|
||||||
|
let mut vars = HashMap::new();
|
||||||
|
vars.insert("pack_dir", "/packs/example".to_string());
|
||||||
|
vars.insert("env_dir", "/runtime_envs/example/python".to_string());
|
||||||
|
|
||||||
|
let prepend = RuntimeEnvVarConfig::Spec(RuntimeEnvVarSpec {
|
||||||
|
value: "{pack_dir}/lib".to_string(),
|
||||||
|
operation: RuntimeEnvVarOperation::Prepend,
|
||||||
|
separator: ":".to_string(),
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
prepend.resolve(&vars, Some("/already/set")),
|
||||||
|
"/packs/example/lib:/already/set"
|
||||||
|
);
|
||||||
|
|
||||||
|
let append = RuntimeEnvVarConfig::Spec(RuntimeEnvVarSpec {
|
||||||
|
value: "{env_dir}/node_modules".to_string(),
|
||||||
|
operation: RuntimeEnvVarOperation::Append,
|
||||||
|
separator: ":".to_string(),
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
append.resolve(&vars, Some("/base/modules")),
|
||||||
|
"/base/modules:/runtime_envs/example/python/node_modules"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -725,8 +725,7 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
let description = data
|
let description = data
|
||||||
.get("description")
|
.get("description")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.map(|s| s.to_string());
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let enabled = data
|
let enabled = data
|
||||||
.get("enabled")
|
.get("enabled")
|
||||||
@@ -745,7 +744,10 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
if let Some(existing) = TriggerRepository::find_by_ref(self.pool, &trigger_ref).await? {
|
if let Some(existing) = TriggerRepository::find_by_ref(self.pool, &trigger_ref).await? {
|
||||||
let update_input = UpdateTriggerInput {
|
let update_input = UpdateTriggerInput {
|
||||||
label: Some(label),
|
label: Some(label),
|
||||||
description: Some(Patch::Set(description)),
|
description: Some(match description {
|
||||||
|
Some(description) => Patch::Set(description),
|
||||||
|
None => Patch::Clear,
|
||||||
|
}),
|
||||||
enabled: Some(enabled),
|
enabled: Some(enabled),
|
||||||
param_schema: Some(match param_schema {
|
param_schema: Some(match param_schema {
|
||||||
Some(value) => Patch::Set(value),
|
Some(value) => Patch::Set(value),
|
||||||
@@ -778,7 +780,7 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
pack: Some(self.pack_id),
|
pack: Some(self.pack_id),
|
||||||
pack_ref: Some(self.pack_ref.clone()),
|
pack_ref: Some(self.pack_ref.clone()),
|
||||||
label,
|
label,
|
||||||
description: Some(description),
|
description,
|
||||||
enabled,
|
enabled,
|
||||||
param_schema,
|
param_schema,
|
||||||
out_schema,
|
out_schema,
|
||||||
@@ -858,8 +860,7 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
let description = data
|
let description = data
|
||||||
.get("description")
|
.get("description")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.map(|s| s.to_string());
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// ── Workflow file handling ──────────────────────────────────
|
// ── Workflow file handling ──────────────────────────────────
|
||||||
// If the action declares `workflow_file`, load the referenced
|
// If the action declares `workflow_file`, load the referenced
|
||||||
@@ -876,7 +877,7 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
wf_path,
|
wf_path,
|
||||||
&action_ref,
|
&action_ref,
|
||||||
&label,
|
&label,
|
||||||
&description,
|
description.as_deref().unwrap_or(""),
|
||||||
&data,
|
&data,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -956,7 +957,10 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
if let Some(existing) = ActionRepository::find_by_ref(self.pool, &action_ref).await? {
|
if let Some(existing) = ActionRepository::find_by_ref(self.pool, &action_ref).await? {
|
||||||
let update_input = UpdateActionInput {
|
let update_input = UpdateActionInput {
|
||||||
label: Some(label),
|
label: Some(label),
|
||||||
description: Some(description),
|
description: Some(match description {
|
||||||
|
Some(description) => Patch::Set(description),
|
||||||
|
None => Patch::Clear,
|
||||||
|
}),
|
||||||
entrypoint: Some(entrypoint),
|
entrypoint: Some(entrypoint),
|
||||||
runtime: runtime_id,
|
runtime: runtime_id,
|
||||||
runtime_version_constraint: Some(match runtime_version_constraint {
|
runtime_version_constraint: Some(match runtime_version_constraint {
|
||||||
@@ -1310,8 +1314,7 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
let description = data
|
let description = data
|
||||||
.get("description")
|
.get("description")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.map(|s| s.to_string());
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let enabled = data
|
let enabled = data
|
||||||
.get("enabled")
|
.get("enabled")
|
||||||
@@ -1347,7 +1350,10 @@ impl<'a> PackComponentLoader<'a> {
|
|||||||
if let Some(existing) = SensorRepository::find_by_ref(self.pool, &sensor_ref).await? {
|
if let Some(existing) = SensorRepository::find_by_ref(self.pool, &sensor_ref).await? {
|
||||||
let update_input = UpdateSensorInput {
|
let update_input = UpdateSensorInput {
|
||||||
label: Some(label),
|
label: Some(label),
|
||||||
description: Some(description),
|
description: Some(match description {
|
||||||
|
Some(description) => Patch::Set(description),
|
||||||
|
None => Patch::Clear,
|
||||||
|
}),
|
||||||
entrypoint: Some(entrypoint),
|
entrypoint: Some(entrypoint),
|
||||||
runtime: Some(sensor_runtime_id),
|
runtime: Some(sensor_runtime_id),
|
||||||
runtime_ref: Some(sensor_runtime_ref.clone()),
|
runtime_ref: Some(sensor_runtime_ref.clone()),
|
||||||
|
|||||||
@@ -21,10 +21,6 @@ pub enum Resource {
|
|||||||
Inquiries,
|
Inquiries,
|
||||||
Keys,
|
Keys,
|
||||||
Artifacts,
|
Artifacts,
|
||||||
Workflows,
|
|
||||||
Webhooks,
|
|
||||||
Analytics,
|
|
||||||
History,
|
|
||||||
Identities,
|
Identities,
|
||||||
Permissions,
|
Permissions,
|
||||||
}
|
}
|
||||||
@@ -40,6 +36,7 @@ pub enum Action {
|
|||||||
Cancel,
|
Cancel,
|
||||||
Respond,
|
Respond,
|
||||||
Manage,
|
Manage,
|
||||||
|
Decrypt,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@@ -69,6 +66,8 @@ pub struct GrantConstraints {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub owner_types: Option<Vec<OwnerType>>,
|
pub owner_types: Option<Vec<OwnerType>>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub owner_refs: Option<Vec<String>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub visibility: Option<Vec<ArtifactVisibility>>,
|
pub visibility: Option<Vec<ArtifactVisibility>>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub execution_scope: Option<ExecutionScopeConstraint>,
|
pub execution_scope: Option<ExecutionScopeConstraint>,
|
||||||
@@ -99,6 +98,7 @@ pub struct AuthorizationContext {
|
|||||||
pub pack_ref: Option<String>,
|
pub pack_ref: Option<String>,
|
||||||
pub owner_identity_id: Option<Id>,
|
pub owner_identity_id: Option<Id>,
|
||||||
pub owner_type: Option<OwnerType>,
|
pub owner_type: Option<OwnerType>,
|
||||||
|
pub owner_ref: Option<String>,
|
||||||
pub visibility: Option<ArtifactVisibility>,
|
pub visibility: Option<ArtifactVisibility>,
|
||||||
pub encrypted: Option<bool>,
|
pub encrypted: Option<bool>,
|
||||||
pub execution_owner_identity_id: Option<Id>,
|
pub execution_owner_identity_id: Option<Id>,
|
||||||
@@ -115,6 +115,7 @@ impl AuthorizationContext {
|
|||||||
pack_ref: None,
|
pack_ref: None,
|
||||||
owner_identity_id: None,
|
owner_identity_id: None,
|
||||||
owner_type: None,
|
owner_type: None,
|
||||||
|
owner_ref: None,
|
||||||
visibility: None,
|
visibility: None,
|
||||||
encrypted: None,
|
encrypted: None,
|
||||||
execution_owner_identity_id: None,
|
execution_owner_identity_id: None,
|
||||||
@@ -162,6 +163,15 @@ impl Grant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(owner_refs) = &constraints.owner_refs {
|
||||||
|
let Some(owner_ref) = &ctx.owner_ref else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !owner_refs.contains(owner_ref) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(visibility) = &constraints.visibility {
|
if let Some(visibility) = &constraints.visibility {
|
||||||
let Some(target_visibility) = ctx.visibility else {
|
let Some(target_visibility) = ctx.visibility else {
|
||||||
return false;
|
return false;
|
||||||
@@ -289,4 +299,28 @@ mod tests {
|
|||||||
.insert("team".to_string(), json!("infra"));
|
.insert("team".to_string(), json!("infra"));
|
||||||
assert!(!grant.allows(Resource::Packs, Action::Read, &ctx));
|
assert!(!grant.allows(Resource::Packs, Action::Read, &ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn owner_ref_constraint_requires_exact_value_match() {
|
||||||
|
let grant = Grant {
|
||||||
|
resource: Resource::Artifacts,
|
||||||
|
actions: vec![Action::Read],
|
||||||
|
constraints: Some(GrantConstraints {
|
||||||
|
owner_types: Some(vec![OwnerType::Pack]),
|
||||||
|
owner_refs: Some(vec!["python_example".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctx = AuthorizationContext::new(1);
|
||||||
|
ctx.owner_type = Some(OwnerType::Pack);
|
||||||
|
ctx.owner_ref = Some("python_example".to_string());
|
||||||
|
assert!(grant.allows(Resource::Artifacts, Action::Read, &ctx));
|
||||||
|
|
||||||
|
ctx.owner_ref = Some("other_pack".to_string());
|
||||||
|
assert!(!grant.allows(Resource::Artifacts, Action::Read, &ctx));
|
||||||
|
|
||||||
|
ctx.owner_ref = None;
|
||||||
|
assert!(!grant.allows(Resource::Artifacts, Action::Read, &ctx));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pub struct CreateActionInput {
|
|||||||
pub pack: Id,
|
pub pack: Id,
|
||||||
pub pack_ref: String,
|
pub pack_ref: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub runtime: Option<Id>,
|
pub runtime: Option<Id>,
|
||||||
pub runtime_version_constraint: Option<String>,
|
pub runtime_version_constraint: Option<String>,
|
||||||
@@ -64,7 +64,7 @@ pub struct CreateActionInput {
|
|||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct UpdateActionInput {
|
pub struct UpdateActionInput {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<Patch<String>>,
|
||||||
pub entrypoint: Option<String>,
|
pub entrypoint: Option<String>,
|
||||||
pub runtime: Option<Id>,
|
pub runtime: Option<Id>,
|
||||||
pub runtime_version_constraint: Option<Patch<String>>,
|
pub runtime_version_constraint: Option<Patch<String>>,
|
||||||
@@ -210,7 +210,10 @@ impl Update for ActionRepository {
|
|||||||
query.push(", ");
|
query.push(", ");
|
||||||
}
|
}
|
||||||
query.push("description = ");
|
query.push("description = ");
|
||||||
query.push_bind(description);
|
match description {
|
||||||
|
Patch::Set(value) => query.push_bind(value),
|
||||||
|
Patch::Clear => query.push_bind(Option::<String>::None),
|
||||||
|
};
|
||||||
has_updates = true;
|
has_updates = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -577,6 +577,14 @@ pub struct CreateArtifactVersionInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ArtifactVersionRepository {
|
impl ArtifactVersionRepository {
|
||||||
|
fn select_columns_with_alias(alias: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{alias}.id, {alias}.artifact, {alias}.version, {alias}.content_type, \
|
||||||
|
{alias}.size_bytes, NULL::bytea AS content, {alias}.content_json, \
|
||||||
|
{alias}.file_path, {alias}.meta, {alias}.created_by, {alias}.created"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Find a version by ID (without binary content for performance)
|
/// Find a version by ID (without binary content for performance)
|
||||||
pub async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<ArtifactVersion>>
|
pub async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<ArtifactVersion>>
|
||||||
where
|
where
|
||||||
@@ -812,14 +820,11 @@ impl ArtifactVersionRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"SELECT av.{} \
|
"SELECT {} \
|
||||||
FROM artifact_version av \
|
FROM artifact_version av \
|
||||||
JOIN artifact a ON av.artifact = a.id \
|
JOIN artifact a ON av.artifact = a.id \
|
||||||
WHERE a.execution = $1 AND av.file_path IS NOT NULL",
|
WHERE a.execution = $1 AND av.file_path IS NOT NULL",
|
||||||
artifact_version::SELECT_COLUMNS
|
Self::select_columns_with_alias("av")
|
||||||
.split(", ")
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", av.")
|
|
||||||
);
|
);
|
||||||
sqlx::query_as::<_, ArtifactVersion>(&query)
|
sqlx::query_as::<_, ArtifactVersion>(&query)
|
||||||
.bind(execution_id)
|
.bind(execution_id)
|
||||||
@@ -847,3 +852,18 @@ impl ArtifactVersionRepository {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::ArtifactVersionRepository;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aliased_select_columns_keep_null_content_expression_unqualified() {
|
||||||
|
let columns = ArtifactVersionRepository::select_columns_with_alias("av");
|
||||||
|
|
||||||
|
assert!(columns.contains("av.id"));
|
||||||
|
assert!(columns.contains("av.file_path"));
|
||||||
|
assert!(columns.contains("NULL::bytea AS content"));
|
||||||
|
assert!(!columns.contains("av.NULL::bytea AS content"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub struct UpdateIdentityInput {
|
|||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub password_hash: Option<String>,
|
pub password_hash: Option<String>,
|
||||||
pub attributes: Option<JsonDict>,
|
pub attributes: Option<JsonDict>,
|
||||||
|
pub frozen: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -37,7 +38,7 @@ impl FindById for IdentityRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, Identity>(
|
sqlx::query_as::<_, Identity>(
|
||||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity WHERE id = $1"
|
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated FROM identity WHERE id = $1"
|
||||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +50,7 @@ impl List for IdentityRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, Identity>(
|
sqlx::query_as::<_, Identity>(
|
||||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity ORDER BY login ASC"
|
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated FROM identity ORDER BY login ASC"
|
||||||
).fetch_all(executor).await.map_err(Into::into)
|
).fetch_all(executor).await.map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ impl Create for IdentityRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, Identity>(
|
sqlx::query_as::<_, Identity>(
|
||||||
"INSERT INTO identity (login, display_name, password_hash, attributes) VALUES ($1, $2, $3, $4) RETURNING id, login, display_name, password_hash, attributes, created, updated"
|
"INSERT INTO identity (login, display_name, password_hash, attributes) VALUES ($1, $2, $3, $4) RETURNING id, login, display_name, password_hash, attributes, frozen, created, updated"
|
||||||
)
|
)
|
||||||
.bind(&input.login)
|
.bind(&input.login)
|
||||||
.bind(&input.display_name)
|
.bind(&input.display_name)
|
||||||
@@ -111,6 +112,13 @@ impl Update for IdentityRepository {
|
|||||||
query.push("attributes = ").push_bind(attributes);
|
query.push("attributes = ").push_bind(attributes);
|
||||||
has_updates = true;
|
has_updates = true;
|
||||||
}
|
}
|
||||||
|
if let Some(frozen) = input.frozen {
|
||||||
|
if has_updates {
|
||||||
|
query.push(", ");
|
||||||
|
}
|
||||||
|
query.push("frozen = ").push_bind(frozen);
|
||||||
|
has_updates = true;
|
||||||
|
}
|
||||||
|
|
||||||
if !has_updates {
|
if !has_updates {
|
||||||
// No updates requested, fetch and return existing entity
|
// No updates requested, fetch and return existing entity
|
||||||
@@ -119,7 +127,7 @@ impl Update for IdentityRepository {
|
|||||||
|
|
||||||
query.push(", updated = NOW() WHERE id = ").push_bind(id);
|
query.push(", updated = NOW() WHERE id = ").push_bind(id);
|
||||||
query.push(
|
query.push(
|
||||||
" RETURNING id, login, display_name, password_hash, attributes, created, updated",
|
" RETURNING id, login, display_name, password_hash, attributes, frozen, created, updated",
|
||||||
);
|
);
|
||||||
|
|
||||||
query
|
query
|
||||||
@@ -156,7 +164,7 @@ impl IdentityRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, Identity>(
|
sqlx::query_as::<_, Identity>(
|
||||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity WHERE login = $1"
|
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated FROM identity WHERE login = $1"
|
||||||
).bind(login).fetch_optional(executor).await.map_err(Into::into)
|
).bind(login).fetch_optional(executor).await.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +177,7 @@ impl IdentityRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, Identity>(
|
sqlx::query_as::<_, Identity>(
|
||||||
"SELECT id, login, display_name, password_hash, attributes, created, updated
|
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated
|
||||||
FROM identity
|
FROM identity
|
||||||
WHERE attributes->'oidc'->>'issuer' = $1
|
WHERE attributes->'oidc'->>'issuer' = $1
|
||||||
AND attributes->'oidc'->>'sub' = $2",
|
AND attributes->'oidc'->>'sub' = $2",
|
||||||
@@ -190,7 +198,7 @@ impl IdentityRepository {
|
|||||||
E: Executor<'e, Database = Postgres> + 'e,
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
{
|
{
|
||||||
sqlx::query_as::<_, Identity>(
|
sqlx::query_as::<_, Identity>(
|
||||||
"SELECT id, login, display_name, password_hash, attributes, created, updated
|
"SELECT id, login, display_name, password_hash, attributes, frozen, created, updated
|
||||||
FROM identity
|
FROM identity
|
||||||
WHERE attributes->'ldap'->>'server_url' = $1
|
WHERE attributes->'ldap'->>'server_url' = $1
|
||||||
AND attributes->'ldap'->>'dn' = $2",
|
AND attributes->'ldap'->>'dn' = $2",
|
||||||
@@ -363,6 +371,27 @@ impl PermissionSetRepository {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_roles<'e, E>(executor: E, roles: &[String]) -> Result<Vec<PermissionSet>>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
if roles.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query_as::<_, PermissionSet>(
|
||||||
|
"SELECT DISTINCT ps.id, ps.ref, ps.pack, ps.pack_ref, ps.label, ps.description, ps.grants, ps.created, ps.updated
|
||||||
|
FROM permission_set ps
|
||||||
|
INNER JOIN permission_set_role_assignment psra ON psra.permset = ps.id
|
||||||
|
WHERE psra.role = ANY($1)
|
||||||
|
ORDER BY ps.ref ASC",
|
||||||
|
)
|
||||||
|
.bind(roles)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete permission sets belonging to a pack whose refs are NOT in the given set.
|
/// Delete permission sets belonging to a pack whose refs are NOT in the given set.
|
||||||
///
|
///
|
||||||
/// Used during pack reinstallation to clean up permission sets that were
|
/// Used during pack reinstallation to clean up permission sets that were
|
||||||
@@ -481,3 +510,231 @@ impl PermissionAssignmentRepository {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct IdentityRoleAssignmentRepository;
|
||||||
|
|
||||||
|
impl Repository for IdentityRoleAssignmentRepository {
|
||||||
|
type Entity = IdentityRoleAssignment;
|
||||||
|
fn table_name() -> &'static str {
|
||||||
|
"identity_role_assignment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CreateIdentityRoleAssignmentInput {
|
||||||
|
pub identity: Id,
|
||||||
|
pub role: String,
|
||||||
|
pub source: String,
|
||||||
|
pub managed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl FindById for IdentityRoleAssignmentRepository {
|
||||||
|
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
sqlx::query_as::<_, IdentityRoleAssignment>(
|
||||||
|
"SELECT id, identity, role, source, managed, created, updated FROM identity_role_assignment WHERE id = $1"
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(executor)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Create for IdentityRoleAssignmentRepository {
|
||||||
|
type CreateInput = CreateIdentityRoleAssignmentInput;
|
||||||
|
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
sqlx::query_as::<_, IdentityRoleAssignment>(
|
||||||
|
"INSERT INTO identity_role_assignment (identity, role, source, managed)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, identity, role, source, managed, created, updated",
|
||||||
|
)
|
||||||
|
.bind(input.identity)
|
||||||
|
.bind(&input.role)
|
||||||
|
.bind(&input.source)
|
||||||
|
.bind(input.managed)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Delete for IdentityRoleAssignmentRepository {
|
||||||
|
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
let result = sqlx::query("DELETE FROM identity_role_assignment WHERE id = $1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(executor)
|
||||||
|
.await?;
|
||||||
|
Ok(result.rows_affected() > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdentityRoleAssignmentRepository {
|
||||||
|
pub async fn find_by_identity<'e, E>(
|
||||||
|
executor: E,
|
||||||
|
identity_id: Id,
|
||||||
|
) -> Result<Vec<IdentityRoleAssignment>>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
sqlx::query_as::<_, IdentityRoleAssignment>(
|
||||||
|
"SELECT id, identity, role, source, managed, created, updated
|
||||||
|
FROM identity_role_assignment
|
||||||
|
WHERE identity = $1
|
||||||
|
ORDER BY role ASC",
|
||||||
|
)
|
||||||
|
.bind(identity_id)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_role_names_by_identity<'e, E>(
|
||||||
|
executor: E,
|
||||||
|
identity_id: Id,
|
||||||
|
) -> Result<Vec<String>>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
sqlx::query_scalar::<_, String>(
|
||||||
|
"SELECT role FROM identity_role_assignment WHERE identity = $1 ORDER BY role ASC",
|
||||||
|
)
|
||||||
|
.bind(identity_id)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn replace_managed_roles<'e, E>(
|
||||||
|
executor: E,
|
||||||
|
identity_id: Id,
|
||||||
|
source: &str,
|
||||||
|
roles: &[String],
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + Copy + 'e,
|
||||||
|
{
|
||||||
|
sqlx::query(
|
||||||
|
"DELETE FROM identity_role_assignment WHERE identity = $1 AND source = $2 AND managed = true",
|
||||||
|
)
|
||||||
|
.bind(identity_id)
|
||||||
|
.bind(source)
|
||||||
|
.execute(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for role in roles {
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO identity_role_assignment (identity, role, source, managed)
|
||||||
|
VALUES ($1, $2, $3, true)
|
||||||
|
ON CONFLICT (identity, role) DO UPDATE
|
||||||
|
SET source = EXCLUDED.source,
|
||||||
|
managed = EXCLUDED.managed,
|
||||||
|
updated = NOW()",
|
||||||
|
)
|
||||||
|
.bind(identity_id)
|
||||||
|
.bind(role)
|
||||||
|
.bind(source)
|
||||||
|
.execute(executor)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PermissionSetRoleAssignmentRepository;
|
||||||
|
|
||||||
|
impl Repository for PermissionSetRoleAssignmentRepository {
|
||||||
|
type Entity = PermissionSetRoleAssignment;
|
||||||
|
fn table_name() -> &'static str {
|
||||||
|
"permission_set_role_assignment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CreatePermissionSetRoleAssignmentInput {
|
||||||
|
pub permset: Id,
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl FindById for PermissionSetRoleAssignmentRepository {
|
||||||
|
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
sqlx::query_as::<_, PermissionSetRoleAssignment>(
|
||||||
|
"SELECT id, permset, role, created FROM permission_set_role_assignment WHERE id = $1",
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(executor)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Create for PermissionSetRoleAssignmentRepository {
|
||||||
|
type CreateInput = CreatePermissionSetRoleAssignmentInput;
|
||||||
|
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
sqlx::query_as::<_, PermissionSetRoleAssignment>(
|
||||||
|
"INSERT INTO permission_set_role_assignment (permset, role)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, permset, role, created",
|
||||||
|
)
|
||||||
|
.bind(input.permset)
|
||||||
|
.bind(&input.role)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Delete for PermissionSetRoleAssignmentRepository {
|
||||||
|
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
let result = sqlx::query("DELETE FROM permission_set_role_assignment WHERE id = $1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(executor)
|
||||||
|
.await?;
|
||||||
|
Ok(result.rows_affected() > 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PermissionSetRoleAssignmentRepository {
|
||||||
|
pub async fn find_by_permission_set<'e, E>(
|
||||||
|
executor: E,
|
||||||
|
permset_id: Id,
|
||||||
|
) -> Result<Vec<PermissionSetRoleAssignment>>
|
||||||
|
where
|
||||||
|
E: Executor<'e, Database = Postgres> + 'e,
|
||||||
|
{
|
||||||
|
sqlx::query_as::<_, PermissionSetRoleAssignment>(
|
||||||
|
"SELECT id, permset, role, created
|
||||||
|
FROM permission_set_role_assignment
|
||||||
|
WHERE permset = $1
|
||||||
|
ORDER BY role ASC",
|
||||||
|
)
|
||||||
|
.bind(permset_id)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::models::{rule::*, Id};
|
|||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||||
|
|
||||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
use super::{Create, Delete, FindById, FindByRef, List, Patch, Repository, Update};
|
||||||
|
|
||||||
/// Filters for [`RuleRepository::list_search`].
|
/// Filters for [`RuleRepository::list_search`].
|
||||||
///
|
///
|
||||||
@@ -41,7 +41,7 @@ pub struct RestoreRuleInput {
|
|||||||
pub pack: Id,
|
pub pack: Id,
|
||||||
pub pack_ref: String,
|
pub pack_ref: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
pub action: Option<Id>,
|
pub action: Option<Id>,
|
||||||
pub action_ref: String,
|
pub action_ref: String,
|
||||||
pub trigger: Option<Id>,
|
pub trigger: Option<Id>,
|
||||||
@@ -70,7 +70,7 @@ pub struct CreateRuleInput {
|
|||||||
pub pack: Id,
|
pub pack: Id,
|
||||||
pub pack_ref: String,
|
pub pack_ref: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
pub action: Id,
|
pub action: Id,
|
||||||
pub action_ref: String,
|
pub action_ref: String,
|
||||||
pub trigger: Id,
|
pub trigger: Id,
|
||||||
@@ -86,7 +86,7 @@ pub struct CreateRuleInput {
|
|||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct UpdateRuleInput {
|
pub struct UpdateRuleInput {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<Patch<String>>,
|
||||||
pub conditions: Option<serde_json::Value>,
|
pub conditions: Option<serde_json::Value>,
|
||||||
pub action_params: Option<serde_json::Value>,
|
pub action_params: Option<serde_json::Value>,
|
||||||
pub trigger_params: Option<serde_json::Value>,
|
pub trigger_params: Option<serde_json::Value>,
|
||||||
@@ -228,7 +228,10 @@ impl Update for RuleRepository {
|
|||||||
query.push(", ");
|
query.push(", ");
|
||||||
}
|
}
|
||||||
query.push("description = ");
|
query.push("description = ");
|
||||||
query.push_bind(description);
|
match description {
|
||||||
|
Patch::Set(value) => query.push_bind(value),
|
||||||
|
Patch::Clear => query.push_bind(Option::<String>::None),
|
||||||
|
};
|
||||||
has_updates = true;
|
has_updates = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -665,7 +665,7 @@ pub struct CreateSensorInput {
|
|||||||
pub pack: Option<Id>,
|
pub pack: Option<Id>,
|
||||||
pub pack_ref: Option<String>,
|
pub pack_ref: Option<String>,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub runtime: Id,
|
pub runtime: Id,
|
||||||
pub runtime_ref: String,
|
pub runtime_ref: String,
|
||||||
@@ -681,7 +681,7 @@ pub struct CreateSensorInput {
|
|||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct UpdateSensorInput {
|
pub struct UpdateSensorInput {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<Patch<String>>,
|
||||||
pub entrypoint: Option<String>,
|
pub entrypoint: Option<String>,
|
||||||
pub runtime: Option<Id>,
|
pub runtime: Option<Id>,
|
||||||
pub runtime_ref: Option<String>,
|
pub runtime_ref: Option<String>,
|
||||||
@@ -830,7 +830,10 @@ impl Update for SensorRepository {
|
|||||||
query.push(", ");
|
query.push(", ");
|
||||||
}
|
}
|
||||||
query.push("description = ");
|
query.push("description = ");
|
||||||
query.push_bind(description);
|
match description {
|
||||||
|
Patch::Set(value) => query.push_bind(value),
|
||||||
|
Patch::Clear => query.push_bind(Option::<String>::None),
|
||||||
|
};
|
||||||
has_updates = true;
|
has_updates = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::repositories::action::{ActionRepository, CreateActionInput, UpdateActionInput};
|
use crate::repositories::action::{ActionRepository, CreateActionInput, UpdateActionInput};
|
||||||
use crate::repositories::workflow::{CreateWorkflowDefinitionInput, UpdateWorkflowDefinitionInput};
|
use crate::repositories::workflow::{CreateWorkflowDefinitionInput, UpdateWorkflowDefinitionInput};
|
||||||
|
use crate::repositories::Patch;
|
||||||
use crate::repositories::{
|
use crate::repositories::{
|
||||||
Create, Delete, FindByRef, PackRepository, Update, WorkflowDefinitionRepository,
|
Create, Delete, FindByRef, PackRepository, Update, WorkflowDefinitionRepository,
|
||||||
};
|
};
|
||||||
@@ -270,7 +271,7 @@ impl WorkflowRegistrar {
|
|||||||
pack: pack_id,
|
pack: pack_id,
|
||||||
pack_ref: pack_ref.to_string(),
|
pack_ref: pack_ref.to_string(),
|
||||||
label: effective_label.to_string(),
|
label: effective_label.to_string(),
|
||||||
description: workflow.description.clone().unwrap_or_default(),
|
description: workflow.description.clone(),
|
||||||
entrypoint,
|
entrypoint,
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
@@ -317,7 +318,10 @@ impl WorkflowRegistrar {
|
|||||||
// Update the existing companion action to stay in sync
|
// Update the existing companion action to stay in sync
|
||||||
let update_input = UpdateActionInput {
|
let update_input = UpdateActionInput {
|
||||||
label: Some(effective_label.to_string()),
|
label: Some(effective_label.to_string()),
|
||||||
description: workflow.description.clone(),
|
description: Some(match workflow.description.clone() {
|
||||||
|
Some(description) => Patch::Set(description),
|
||||||
|
None => Patch::Clear,
|
||||||
|
}),
|
||||||
entrypoint: Some(format!("workflows/{}.workflow.yaml", workflow_name)),
|
entrypoint: Some(format!("workflows/{}.workflow.yaml", workflow_name)),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ async fn test_create_action_with_optional_fields() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(action.label, "Full Test Action");
|
assert_eq!(action.label, "Full Test Action");
|
||||||
assert_eq!(action.description, "Action with all optional fields");
|
assert_eq!(
|
||||||
|
action.description,
|
||||||
|
Some("Action with all optional fields".to_string())
|
||||||
|
);
|
||||||
assert_eq!(action.entrypoint, "custom.py");
|
assert_eq!(action.entrypoint, "custom.py");
|
||||||
assert!(action.param_schema.is_some());
|
assert!(action.param_schema.is_some());
|
||||||
assert!(action.out_schema.is_some());
|
assert!(action.out_schema.is_some());
|
||||||
@@ -204,7 +207,9 @@ async fn test_update_action() {
|
|||||||
|
|
||||||
let update = UpdateActionInput {
|
let update = UpdateActionInput {
|
||||||
label: Some("Updated Label".to_string()),
|
label: Some("Updated Label".to_string()),
|
||||||
description: Some("Updated description".to_string()),
|
description: Some(attune_common::repositories::Patch::Set(
|
||||||
|
"Updated description".to_string(),
|
||||||
|
)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,7 +219,7 @@ async fn test_update_action() {
|
|||||||
|
|
||||||
assert_eq!(updated.id, action.id);
|
assert_eq!(updated.id, action.id);
|
||||||
assert_eq!(updated.label, "Updated Label");
|
assert_eq!(updated.label, "Updated Label");
|
||||||
assert_eq!(updated.description, "Updated description");
|
assert_eq!(updated.description, Some("Updated description".to_string()));
|
||||||
assert_eq!(updated.entrypoint, action.entrypoint); // Unchanged
|
assert_eq!(updated.entrypoint, action.entrypoint); // Unchanged
|
||||||
assert!(updated.updated > original_updated);
|
assert!(updated.updated > original_updated);
|
||||||
}
|
}
|
||||||
@@ -338,7 +343,7 @@ async fn test_action_foreign_key_constraint() {
|
|||||||
pack: 99999,
|
pack: 99999,
|
||||||
pack_ref: "nonexistent.pack".to_string(),
|
pack_ref: "nonexistent.pack".to_string(),
|
||||||
label: "Test Action".to_string(),
|
label: "Test Action".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
entrypoint: "main.py".to_string(),
|
entrypoint: "main.py".to_string(),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async fn test_create_enforcement_minimal() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -121,7 +121,7 @@ async fn test_create_enforcement_with_event() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -189,7 +189,7 @@ async fn test_create_enforcement_with_conditions() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -255,7 +255,7 @@ async fn test_create_enforcement_with_any_condition() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -397,7 +397,7 @@ async fn test_find_enforcement_by_id() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -471,7 +471,7 @@ async fn test_get_enforcement_by_id() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -552,7 +552,7 @@ async fn test_list_enforcements() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -624,7 +624,7 @@ async fn test_update_enforcement_status() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -690,7 +690,7 @@ async fn test_update_enforcement_status_transitions() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -769,7 +769,7 @@ async fn test_update_enforcement_payload() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -832,7 +832,7 @@ async fn test_update_enforcement_both_fields() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -896,7 +896,7 @@ async fn test_update_enforcement_no_changes() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -981,7 +981,7 @@ async fn test_delete_enforcement() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1056,7 +1056,7 @@ async fn test_find_enforcements_by_rule() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Rule 1".to_string(),
|
label: "Rule 1".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1078,7 +1078,7 @@ async fn test_find_enforcements_by_rule() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Rule 2".to_string(),
|
label: "Rule 2".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1149,7 +1149,7 @@ async fn test_find_enforcements_by_status() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1239,7 +1239,7 @@ async fn test_find_enforcements_by_event() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1324,7 +1324,7 @@ async fn test_delete_rule_sets_enforcement_rule_to_null() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1390,7 +1390,7 @@ async fn test_enforcement_resolved_at_lifecycle() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ async fn test_delete_event_enforcement_retains_event_id() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ impl ActionFixture {
|
|||||||
pack_ref: self.pack_ref,
|
pack_ref: self.pack_ref,
|
||||||
r#ref: self.r#ref,
|
r#ref: self.r#ref,
|
||||||
label: self.label,
|
label: self.label,
|
||||||
description: self.description,
|
description: Some(self.description),
|
||||||
entrypoint: self.entrypoint,
|
entrypoint: self.entrypoint,
|
||||||
runtime: self.runtime,
|
runtime: self.runtime,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
@@ -1088,7 +1088,7 @@ impl SensorFixture {
|
|||||||
pack: self.pack_id,
|
pack: self.pack_id,
|
||||||
pack_ref: self.pack_ref,
|
pack_ref: self.pack_ref,
|
||||||
label: self.label,
|
label: self.label,
|
||||||
description: self.description,
|
description: Some(self.description),
|
||||||
entrypoint: self.entrypoint,
|
entrypoint: self.entrypoint,
|
||||||
runtime: self.runtime_id,
|
runtime: self.runtime_id,
|
||||||
runtime_ref: self.runtime_ref,
|
runtime_ref: self.runtime_ref,
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ async fn test_update_identity() {
|
|||||||
display_name: Some("Updated Name".to_string()),
|
display_name: Some("Updated Name".to_string()),
|
||||||
password_hash: None,
|
password_hash: None,
|
||||||
attributes: Some(json!({"key": "updated", "new_key": "new_value"})),
|
attributes: Some(json!({"key": "updated", "new_key": "new_value"})),
|
||||||
|
frozen: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
||||||
@@ -252,6 +253,7 @@ async fn test_update_identity_partial() {
|
|||||||
display_name: Some("Only Display Name Changed".to_string()),
|
display_name: Some("Only Display Name Changed".to_string()),
|
||||||
password_hash: None,
|
password_hash: None,
|
||||||
attributes: None,
|
attributes: None,
|
||||||
|
frozen: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
||||||
@@ -274,6 +276,7 @@ async fn test_update_identity_not_found() {
|
|||||||
display_name: Some("Updated Name".to_string()),
|
display_name: Some("Updated Name".to_string()),
|
||||||
password_hash: None,
|
password_hash: None,
|
||||||
attributes: None,
|
attributes: None,
|
||||||
|
frozen: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = IdentityRepository::update(&pool, 999999, update_input).await;
|
let result = IdentityRepository::update(&pool, 999999, update_input).await;
|
||||||
@@ -380,6 +383,7 @@ async fn test_identity_updated_changes_on_update() {
|
|||||||
display_name: Some("Updated".to_string()),
|
display_name: Some("Updated".to_string()),
|
||||||
password_hash: None,
|
password_hash: None,
|
||||||
attributes: None,
|
attributes: None,
|
||||||
|
frozen: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
let updated = IdentityRepository::update(&pool, identity.id, update_input)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ mod helpers;
|
|||||||
use attune_common::{
|
use attune_common::{
|
||||||
repositories::{
|
repositories::{
|
||||||
rule::{CreateRuleInput, RuleRepository, UpdateRuleInput},
|
rule::{CreateRuleInput, RuleRepository, UpdateRuleInput},
|
||||||
Create, Delete, FindById, FindByRef, List, Update,
|
Create, Delete, FindById, FindByRef, List, Patch, Update,
|
||||||
},
|
},
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
@@ -48,7 +48,7 @@ async fn test_create_rule() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "A test rule".to_string(),
|
description: Some("A test rule".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -66,7 +66,7 @@ async fn test_create_rule() {
|
|||||||
assert_eq!(rule.pack, pack.id);
|
assert_eq!(rule.pack, pack.id);
|
||||||
assert_eq!(rule.pack_ref, pack.r#ref);
|
assert_eq!(rule.pack_ref, pack.r#ref);
|
||||||
assert_eq!(rule.label, "Test Rule");
|
assert_eq!(rule.label, "Test Rule");
|
||||||
assert_eq!(rule.description, "A test rule");
|
assert_eq!(rule.description, Some("A test rule".to_string()));
|
||||||
assert_eq!(rule.action, Some(action.id));
|
assert_eq!(rule.action, Some(action.id));
|
||||||
assert_eq!(rule.action_ref, action.r#ref);
|
assert_eq!(rule.action_ref, action.r#ref);
|
||||||
assert_eq!(rule.trigger, Some(trigger.id));
|
assert_eq!(rule.trigger, Some(trigger.id));
|
||||||
@@ -105,7 +105,7 @@ async fn test_create_rule_disabled() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Disabled Rule".to_string(),
|
label: "Disabled Rule".to_string(),
|
||||||
description: "A disabled rule".to_string(),
|
description: Some("A disabled rule".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -155,7 +155,7 @@ async fn test_create_rule_with_complex_conditions() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Complex Rule".to_string(),
|
label: "Complex Rule".to_string(),
|
||||||
description: "Rule with complex conditions".to_string(),
|
description: Some("Rule with complex conditions".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -200,7 +200,7 @@ async fn test_create_rule_duplicate_ref() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "First Rule".to_string(),
|
label: "First Rule".to_string(),
|
||||||
description: "First".to_string(),
|
description: Some("First".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -220,7 +220,7 @@ async fn test_create_rule_duplicate_ref() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Second Rule".to_string(),
|
label: "Second Rule".to_string(),
|
||||||
description: "Second".to_string(),
|
description: Some("Second".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -274,7 +274,7 @@ async fn test_create_rule_invalid_ref_format_uppercase() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Upper Rule".to_string(),
|
label: "Upper Rule".to_string(),
|
||||||
description: "Invalid uppercase ref".to_string(),
|
description: Some("Invalid uppercase ref".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -316,7 +316,7 @@ async fn test_create_rule_invalid_ref_format_no_dot() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "No Dot Rule".to_string(),
|
label: "No Dot Rule".to_string(),
|
||||||
description: "Invalid ref without dot".to_string(),
|
description: Some("Invalid ref without dot".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -362,7 +362,7 @@ async fn test_find_rule_by_id() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Find Rule".to_string(),
|
label: "Find Rule".to_string(),
|
||||||
description: "Rule to find".to_string(),
|
description: Some("Rule to find".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -422,7 +422,7 @@ async fn test_find_rule_by_ref() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Find By Ref Rule".to_string(),
|
label: "Find By Ref Rule".to_string(),
|
||||||
description: "Find by ref".to_string(),
|
description: Some("Find by ref".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -484,7 +484,7 @@ async fn test_list_rules() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: format!("List Rule {}", i),
|
label: format!("List Rule {}", i),
|
||||||
description: format!("Rule {}", i),
|
description: Some(format!("Rule {}", i)),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -538,7 +538,7 @@ async fn test_list_rules_ordered_by_ref() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: name.to_string(),
|
label: name.to_string(),
|
||||||
description: name.to_string(),
|
description: Some(name.to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -594,7 +594,7 @@ async fn test_update_rule_label() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Original Label".to_string(),
|
label: "Original Label".to_string(),
|
||||||
description: "Original".to_string(),
|
description: Some("Original".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -618,7 +618,7 @@ async fn test_update_rule_label() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(updated.label, "Updated Label");
|
assert_eq!(updated.label, "Updated Label");
|
||||||
assert_eq!(updated.description, "Original"); // unchanged
|
assert_eq!(updated.description, Some("Original".to_string())); // unchanged
|
||||||
assert!(updated.updated > created.updated);
|
assert!(updated.updated > created.updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +647,7 @@ async fn test_update_rule_description() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test".to_string(),
|
label: "Test".to_string(),
|
||||||
description: "Old description".to_string(),
|
description: Some("Old description".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -662,7 +662,7 @@ async fn test_update_rule_description() {
|
|||||||
let created = RuleRepository::create(&pool, input).await.unwrap();
|
let created = RuleRepository::create(&pool, input).await.unwrap();
|
||||||
|
|
||||||
let update = UpdateRuleInput {
|
let update = UpdateRuleInput {
|
||||||
description: Some("New description".to_string()),
|
description: Some(Patch::Set("New description".to_string())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -670,7 +670,7 @@ async fn test_update_rule_description() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(updated.description, "New description");
|
assert_eq!(updated.description, Some("New description".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -698,7 +698,7 @@ async fn test_update_rule_conditions() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test".to_string(),
|
label: "Test".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -750,7 +750,7 @@ async fn test_update_rule_enabled() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test".to_string(),
|
label: "Test".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -803,7 +803,7 @@ async fn test_update_rule_multiple_fields() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Old".to_string(),
|
label: "Old".to_string(),
|
||||||
description: "Old".to_string(),
|
description: Some("Old".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -819,7 +819,7 @@ async fn test_update_rule_multiple_fields() {
|
|||||||
|
|
||||||
let update = UpdateRuleInput {
|
let update = UpdateRuleInput {
|
||||||
label: Some("New Label".to_string()),
|
label: Some("New Label".to_string()),
|
||||||
description: Some("New Description".to_string()),
|
description: Some(Patch::Set("New Description".to_string())),
|
||||||
conditions: Some(json!({"updated": true})),
|
conditions: Some(json!({"updated": true})),
|
||||||
action_params: None,
|
action_params: None,
|
||||||
trigger_params: None,
|
trigger_params: None,
|
||||||
@@ -831,7 +831,7 @@ async fn test_update_rule_multiple_fields() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(updated.label, "New Label");
|
assert_eq!(updated.label, "New Label");
|
||||||
assert_eq!(updated.description, "New Description");
|
assert_eq!(updated.description, Some("New Description".to_string()));
|
||||||
assert_eq!(updated.conditions, json!({"updated": true}));
|
assert_eq!(updated.conditions, json!({"updated": true}));
|
||||||
assert!(!updated.enabled);
|
assert!(!updated.enabled);
|
||||||
}
|
}
|
||||||
@@ -861,7 +861,7 @@ async fn test_update_rule_no_changes() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Test".to_string(),
|
label: "Test".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -914,7 +914,7 @@ async fn test_delete_rule() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "To Delete".to_string(),
|
label: "To Delete".to_string(),
|
||||||
description: "Will be deleted".to_string(),
|
description: Some("Will be deleted".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -995,7 +995,7 @@ async fn test_find_rules_by_pack() {
|
|||||||
pack: pack1.id,
|
pack: pack1.id,
|
||||||
pack_ref: pack1.r#ref.clone(),
|
pack_ref: pack1.r#ref.clone(),
|
||||||
label: format!("Rule {}", i),
|
label: format!("Rule {}", i),
|
||||||
description: format!("Rule {}", i),
|
description: Some(format!("Rule {}", i)),
|
||||||
action: action1.id,
|
action: action1.id,
|
||||||
action_ref: action1.r#ref.clone(),
|
action_ref: action1.r#ref.clone(),
|
||||||
trigger: trigger1.id,
|
trigger: trigger1.id,
|
||||||
@@ -1016,7 +1016,7 @@ async fn test_find_rules_by_pack() {
|
|||||||
pack: pack2.id,
|
pack: pack2.id,
|
||||||
pack_ref: pack2.r#ref.clone(),
|
pack_ref: pack2.r#ref.clone(),
|
||||||
label: "Pack2 Rule".to_string(),
|
label: "Pack2 Rule".to_string(),
|
||||||
description: "Pack2".to_string(),
|
description: Some("Pack2".to_string()),
|
||||||
action: action2.id,
|
action: action2.id,
|
||||||
action_ref: action2.r#ref.clone(),
|
action_ref: action2.r#ref.clone(),
|
||||||
trigger: trigger2.id,
|
trigger: trigger2.id,
|
||||||
@@ -1073,7 +1073,7 @@ async fn test_find_rules_by_action() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: format!("Action1 Rule {}", i),
|
label: format!("Action1 Rule {}", i),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action1.id,
|
action: action1.id,
|
||||||
action_ref: action1.r#ref.clone(),
|
action_ref: action1.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1094,7 +1094,7 @@ async fn test_find_rules_by_action() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Action2 Rule".to_string(),
|
label: "Action2 Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action2.id,
|
action: action2.id,
|
||||||
action_ref: action2.r#ref.clone(),
|
action_ref: action2.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1155,7 +1155,7 @@ async fn test_find_rules_by_trigger() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: format!("Trigger1 Rule {}", i),
|
label: format!("Trigger1 Rule {}", i),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger1.id,
|
trigger: trigger1.id,
|
||||||
@@ -1176,7 +1176,7 @@ async fn test_find_rules_by_trigger() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Trigger2 Rule".to_string(),
|
label: "Trigger2 Rule".to_string(),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger2.id,
|
trigger: trigger2.id,
|
||||||
@@ -1234,7 +1234,7 @@ async fn test_find_enabled_rules() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: format!("Enabled {}", i),
|
label: format!("Enabled {}", i),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1256,7 +1256,7 @@ async fn test_find_enabled_rules() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: format!("Disabled {}", i),
|
label: format!("Disabled {}", i),
|
||||||
description: "Test".to_string(),
|
description: Some("Test".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1312,7 +1312,7 @@ async fn test_cascade_delete_pack_deletes_rules() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Cascade Rule".to_string(),
|
label: "Cascade Rule".to_string(),
|
||||||
description: "Will be cascade deleted".to_string(),
|
description: Some("Will be cascade deleted".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
@@ -1368,7 +1368,7 @@ async fn test_rule_timestamps() {
|
|||||||
pack: pack.id,
|
pack: pack.id,
|
||||||
pack_ref: pack.r#ref.clone(),
|
pack_ref: pack.r#ref.clone(),
|
||||||
label: "Timestamp Rule".to_string(),
|
label: "Timestamp Rule".to_string(),
|
||||||
description: "Test timestamps".to_string(),
|
description: Some("Test timestamps".to_string()),
|
||||||
action: action.id,
|
action: action.id,
|
||||||
action_ref: action.r#ref.clone(),
|
action_ref: action.r#ref.clone(),
|
||||||
trigger: trigger.id,
|
trigger: trigger.id,
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ async fn test_create_sensor_duplicate_ref_fails() {
|
|||||||
pack: Some(pack.id),
|
pack: Some(pack.id),
|
||||||
pack_ref: Some(pack.r#ref.clone()),
|
pack_ref: Some(pack.r#ref.clone()),
|
||||||
label: "Duplicate Sensor".to_string(),
|
label: "Duplicate Sensor".to_string(),
|
||||||
description: "Test sensor".to_string(),
|
description: Some("Test sensor".to_string()),
|
||||||
entrypoint: "sensors/dup.py".to_string(),
|
entrypoint: "sensors/dup.py".to_string(),
|
||||||
runtime: runtime.id,
|
runtime: runtime.id,
|
||||||
runtime_ref: runtime.r#ref.clone(),
|
runtime_ref: runtime.r#ref.clone(),
|
||||||
@@ -235,7 +235,7 @@ async fn test_create_sensor_invalid_ref_format_fails() {
|
|||||||
pack: Some(pack.id),
|
pack: Some(pack.id),
|
||||||
pack_ref: Some(pack.r#ref.clone()),
|
pack_ref: Some(pack.r#ref.clone()),
|
||||||
label: "Invalid Sensor".to_string(),
|
label: "Invalid Sensor".to_string(),
|
||||||
description: "Test sensor".to_string(),
|
description: Some("Test sensor".to_string()),
|
||||||
entrypoint: "sensors/invalid.py".to_string(),
|
entrypoint: "sensors/invalid.py".to_string(),
|
||||||
runtime: runtime.id,
|
runtime: runtime.id,
|
||||||
runtime_ref: runtime.r#ref.clone(),
|
runtime_ref: runtime.r#ref.clone(),
|
||||||
@@ -276,7 +276,7 @@ async fn test_create_sensor_invalid_pack_fails() {
|
|||||||
pack: Some(99999), // Non-existent pack
|
pack: Some(99999), // Non-existent pack
|
||||||
pack_ref: Some("invalid".to_string()),
|
pack_ref: Some("invalid".to_string()),
|
||||||
label: "Invalid Pack Sensor".to_string(),
|
label: "Invalid Pack Sensor".to_string(),
|
||||||
description: "Test sensor".to_string(),
|
description: Some("Test sensor".to_string()),
|
||||||
entrypoint: "sensors/invalid.py".to_string(),
|
entrypoint: "sensors/invalid.py".to_string(),
|
||||||
runtime: runtime.id,
|
runtime: runtime.id,
|
||||||
runtime_ref: runtime.r#ref.clone(),
|
runtime_ref: runtime.r#ref.clone(),
|
||||||
@@ -308,7 +308,7 @@ async fn test_create_sensor_invalid_trigger_fails() {
|
|||||||
pack: None,
|
pack: None,
|
||||||
pack_ref: None,
|
pack_ref: None,
|
||||||
label: "Invalid Trigger Sensor".to_string(),
|
label: "Invalid Trigger Sensor".to_string(),
|
||||||
description: "Test sensor".to_string(),
|
description: Some("Test sensor".to_string()),
|
||||||
entrypoint: "sensors/invalid.py".to_string(),
|
entrypoint: "sensors/invalid.py".to_string(),
|
||||||
runtime: runtime.id,
|
runtime: runtime.id,
|
||||||
runtime_ref: runtime.r#ref.clone(),
|
runtime_ref: runtime.r#ref.clone(),
|
||||||
@@ -340,7 +340,7 @@ async fn test_create_sensor_invalid_runtime_fails() {
|
|||||||
pack: None,
|
pack: None,
|
||||||
pack_ref: None,
|
pack_ref: None,
|
||||||
label: "Invalid Runtime Sensor".to_string(),
|
label: "Invalid Runtime Sensor".to_string(),
|
||||||
description: "Test sensor".to_string(),
|
description: Some("Test sensor".to_string()),
|
||||||
entrypoint: "sensors/invalid.py".to_string(),
|
entrypoint: "sensors/invalid.py".to_string(),
|
||||||
runtime: 99999, // Non-existent runtime
|
runtime: 99999, // Non-existent runtime
|
||||||
runtime_ref: "invalid.runtime".to_string(),
|
runtime_ref: "invalid.runtime".to_string(),
|
||||||
@@ -728,7 +728,7 @@ async fn test_update_description() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let input = UpdateSensorInput {
|
let input = UpdateSensorInput {
|
||||||
description: Some("New description for the sensor".to_string()),
|
description: Some(Patch::Set("New description for the sensor".to_string())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -736,7 +736,10 @@ async fn test_update_description() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(updated.description, "New description for the sensor");
|
assert_eq!(
|
||||||
|
updated.description,
|
||||||
|
Some("New description for the sensor".to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -934,7 +937,7 @@ async fn test_update_multiple_fields() {
|
|||||||
|
|
||||||
let input = UpdateSensorInput {
|
let input = UpdateSensorInput {
|
||||||
label: Some("Multi Update".to_string()),
|
label: Some("Multi Update".to_string()),
|
||||||
description: Some("Updated multiple fields".to_string()),
|
description: Some(Patch::Set("Updated multiple fields".to_string())),
|
||||||
entrypoint: Some("sensors/multi.py".to_string()),
|
entrypoint: Some("sensors/multi.py".to_string()),
|
||||||
enabled: Some(false),
|
enabled: Some(false),
|
||||||
param_schema: Some(Patch::Set(json!({"type": "object"}))),
|
param_schema: Some(Patch::Set(json!({"type": "object"}))),
|
||||||
@@ -946,7 +949,10 @@ async fn test_update_multiple_fields() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(updated.label, "Multi Update");
|
assert_eq!(updated.label, "Multi Update");
|
||||||
assert_eq!(updated.description, "Updated multiple fields");
|
assert_eq!(
|
||||||
|
updated.description,
|
||||||
|
Some("Updated multiple fields".to_string())
|
||||||
|
);
|
||||||
assert_eq!(updated.entrypoint, "sensors/multi.py");
|
assert_eq!(updated.entrypoint, "sensors/multi.py");
|
||||||
assert!(!updated.enabled);
|
assert!(!updated.enabled);
|
||||||
assert_eq!(updated.param_schema, Some(json!({"type": "object"})));
|
assert_eq!(updated.param_schema, Some(json!({"type": "object"})));
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ mod tests {
|
|||||||
pack: 1,
|
pack: 1,
|
||||||
pack_ref: "test".to_string(),
|
pack_ref: "test".to_string(),
|
||||||
label: "Test Rule".to_string(),
|
label: "Test Rule".to_string(),
|
||||||
description: "Test rule description".to_string(),
|
description: Some("Test rule description".to_string()),
|
||||||
trigger_ref: "test.trigger".to_string(),
|
trigger_ref: "test.trigger".to_string(),
|
||||||
trigger: Some(1),
|
trigger: Some(1),
|
||||||
action_ref: "test.action".to_string(),
|
action_ref: "test.action".to_string(),
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ async fn create_test_action(pool: &PgPool, pack_id: i64, pack_ref: &str, suffix:
|
|||||||
pack: pack_id,
|
pack: pack_id,
|
||||||
pack_ref: pack_ref.to_string(),
|
pack_ref: pack_ref.to_string(),
|
||||||
label: format!("FIFO Test Action {}", suffix),
|
label: format!("FIFO Test Action {}", suffix),
|
||||||
description: format!("Test action {}", suffix),
|
description: Some(format!("Test action {}", suffix)),
|
||||||
entrypoint: "echo test".to_string(),
|
entrypoint: "echo test".to_string(),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async fn create_test_action(pool: &PgPool, pack_id: i64, suffix: &str) -> i64 {
|
|||||||
pack: pack_id,
|
pack: pack_id,
|
||||||
pack_ref: format!("test_pack_{}", suffix),
|
pack_ref: format!("test_pack_{}", suffix),
|
||||||
label: format!("Test Action {}", suffix),
|
label: format!("Test Action {}", suffix),
|
||||||
description: format!("Test action {}", suffix),
|
description: Some(format!("Test action {}", suffix)),
|
||||||
entrypoint: "echo test".to_string(),
|
entrypoint: "echo test".to_string(),
|
||||||
runtime: None,
|
runtime: None,
|
||||||
runtime_version_constraint: None,
|
runtime_version_constraint: None,
|
||||||
|
|||||||
@@ -27,6 +27,37 @@ use tracing::{debug, error, info, warn};
|
|||||||
|
|
||||||
use crate::api_client::ApiClient;
|
use crate::api_client::ApiClient;
|
||||||
|
|
||||||
|
fn existing_command_env(cmd: &Command, key: &str) -> Option<String> {
|
||||||
|
cmd.as_std()
|
||||||
|
.get_envs()
|
||||||
|
.find_map(|(env_key, value)| {
|
||||||
|
if env_key == key {
|
||||||
|
value.map(|value| value.to_string_lossy().into_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_else(|| std::env::var(key).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_runtime_env_vars(
|
||||||
|
cmd: &mut Command,
|
||||||
|
exec_config: &RuntimeExecutionConfig,
|
||||||
|
pack_dir: &std::path::Path,
|
||||||
|
env_dir: Option<&std::path::Path>,
|
||||||
|
) {
|
||||||
|
if exec_config.env_vars.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vars = exec_config.build_template_vars_with_env(pack_dir, env_dir);
|
||||||
|
for (key, env_var_config) in &exec_config.env_vars {
|
||||||
|
let resolved = env_var_config.resolve(&vars, existing_command_env(cmd, key).as_deref());
|
||||||
|
debug!("Setting sensor runtime env var: {}={}", key, resolved);
|
||||||
|
cmd.env(key, resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sensor manager that coordinates all sensor instances
|
/// Sensor manager that coordinates all sensor instances
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SensorManager {
|
pub struct SensorManager {
|
||||||
@@ -502,20 +533,7 @@ impl SensorManager {
|
|||||||
.env("ATTUNE_MQ_EXCHANGE", "attune.events")
|
.env("ATTUNE_MQ_EXCHANGE", "attune.events")
|
||||||
.env("ATTUNE_LOG_LEVEL", "info");
|
.env("ATTUNE_LOG_LEVEL", "info");
|
||||||
|
|
||||||
if !exec_config.env_vars.is_empty() {
|
apply_runtime_env_vars(&mut cmd, &exec_config, &pack_dir, env_dir_opt);
|
||||||
let vars = exec_config.build_template_vars_with_env(&pack_dir, env_dir_opt);
|
|
||||||
for (key, value_template) in &exec_config.env_vars {
|
|
||||||
let resolved = attune_common::models::RuntimeExecutionConfig::resolve_template(
|
|
||||||
value_template,
|
|
||||||
&vars,
|
|
||||||
);
|
|
||||||
debug!(
|
|
||||||
"Setting sensor runtime env var: {}={} (template: {})",
|
|
||||||
key, resolved, value_template
|
|
||||||
);
|
|
||||||
cmd.env(key, resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut child = cmd
|
let mut child = cmd
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
@@ -904,6 +922,10 @@ pub struct SensorStatus {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use attune_common::models::runtime::{
|
||||||
|
RuntimeEnvVarConfig, RuntimeEnvVarOperation, RuntimeEnvVarSpec,
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sensor_status_default() {
|
fn test_sensor_status_default() {
|
||||||
@@ -913,4 +935,46 @@ mod tests {
|
|||||||
assert_eq!(status.failure_count, 0);
|
assert_eq!(status.failure_count, 0);
|
||||||
assert!(status.last_poll.is_none());
|
assert!(status.last_poll.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_apply_runtime_env_vars_prepends_to_existing_command_env() {
|
||||||
|
let mut env_vars = HashMap::new();
|
||||||
|
env_vars.insert(
|
||||||
|
"PYTHONPATH".to_string(),
|
||||||
|
RuntimeEnvVarConfig::Spec(RuntimeEnvVarSpec {
|
||||||
|
value: "{pack_dir}/lib".to_string(),
|
||||||
|
operation: RuntimeEnvVarOperation::Prepend,
|
||||||
|
separator: ":".to_string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let exec_config = RuntimeExecutionConfig {
|
||||||
|
env_vars,
|
||||||
|
..RuntimeExecutionConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = Command::new("python3");
|
||||||
|
cmd.env("PYTHONPATH", "/existing/pythonpath");
|
||||||
|
|
||||||
|
apply_runtime_env_vars(
|
||||||
|
&mut cmd,
|
||||||
|
&exec_config,
|
||||||
|
std::path::Path::new("/packs/testpack"),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolved = cmd
|
||||||
|
.as_std()
|
||||||
|
.get_envs()
|
||||||
|
.find_map(|(key, value)| {
|
||||||
|
if key == "PYTHONPATH" {
|
||||||
|
value.map(|value| value.to_string_lossy().into_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("PYTHONPATH should be set");
|
||||||
|
|
||||||
|
assert_eq!(resolved, "/packs/testpack/lib:/existing/pythonpath");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,52 @@ fn bash_single_quote_escape(s: &str) -> String {
|
|||||||
s.replace('\'', "'\\''")
|
s.replace('\'', "'\\''")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_command_for_log(cmd: &Command) -> String {
|
||||||
|
let program = cmd.as_std().get_program().to_string_lossy().into_owned();
|
||||||
|
let args = cmd
|
||||||
|
.as_std()
|
||||||
|
.get_args()
|
||||||
|
.map(|arg| arg.to_string_lossy().into_owned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let cwd = cmd
|
||||||
|
.as_std()
|
||||||
|
.get_current_dir()
|
||||||
|
.map(|dir| dir.display().to_string())
|
||||||
|
.unwrap_or_else(|| "<inherit>".to_string());
|
||||||
|
let env = cmd
|
||||||
|
.as_std()
|
||||||
|
.get_envs()
|
||||||
|
.map(|(key, value)| {
|
||||||
|
let key = key.to_string_lossy().into_owned();
|
||||||
|
let value = value
|
||||||
|
.map(|v| {
|
||||||
|
if is_sensitive_env_var(&key) {
|
||||||
|
"<redacted>".to_string()
|
||||||
|
} else {
|
||||||
|
v.to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "<unset>".to_string());
|
||||||
|
format!("{key}={value}")
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"program={program}, args={args:?}, cwd={cwd}, env={env:?}",
|
||||||
|
args = args,
|
||||||
|
env = env,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_sensitive_env_var(key: &str) -> bool {
|
||||||
|
let upper = key.to_ascii_uppercase();
|
||||||
|
upper.contains("TOKEN")
|
||||||
|
|| upper.contains("SECRET")
|
||||||
|
|| upper.contains("PASSWORD")
|
||||||
|
|| upper.ends_with("_KEY")
|
||||||
|
|| upper == "KEY"
|
||||||
|
}
|
||||||
|
|
||||||
/// A generic runtime driven by `RuntimeExecutionConfig` from the database.
|
/// A generic runtime driven by `RuntimeExecutionConfig` from the database.
|
||||||
///
|
///
|
||||||
/// Each `ProcessRuntime` instance corresponds to a row in the `runtime` table.
|
/// Each `ProcessRuntime` instance corresponds to a row in the `runtime` table.
|
||||||
@@ -784,12 +830,9 @@ impl Runtime for ProcessRuntime {
|
|||||||
// resolved against the current pack/env directories.
|
// resolved against the current pack/env directories.
|
||||||
if !effective_config.env_vars.is_empty() {
|
if !effective_config.env_vars.is_empty() {
|
||||||
let vars = effective_config.build_template_vars_with_env(&pack_dir, env_dir_opt);
|
let vars = effective_config.build_template_vars_with_env(&pack_dir, env_dir_opt);
|
||||||
for (key, value_template) in &effective_config.env_vars {
|
for (key, env_var_config) in &effective_config.env_vars {
|
||||||
let resolved = RuntimeExecutionConfig::resolve_template(value_template, &vars);
|
let resolved = env_var_config.resolve(&vars, env.get(key).map(String::as_str));
|
||||||
debug!(
|
debug!("Setting runtime env var: {}={}", key, resolved);
|
||||||
"Setting runtime env var: {}={} (template: {})",
|
|
||||||
key, resolved, value_template
|
|
||||||
);
|
|
||||||
env.insert(key.clone(), resolved);
|
env.insert(key.clone(), resolved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -897,10 +940,10 @@ impl Runtime for ProcessRuntime {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log the full command about to be executed
|
// Log the spawned process accurately instead of using Command's shell-like Debug output.
|
||||||
info!(
|
info!(
|
||||||
"Running command: {:?} (action: '{}', execution_id: {}, working_dir: {:?})",
|
"Running command: {} (action: '{}', execution_id: {}, working_dir: {:?})",
|
||||||
cmd,
|
format_command_for_log(&cmd),
|
||||||
context.action_ref,
|
context.action_ref,
|
||||||
context.execution_id,
|
context.execution_id,
|
||||||
working_dir
|
working_dir
|
||||||
@@ -1016,7 +1059,8 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use attune_common::models::runtime::{
|
use attune_common::models::runtime::{
|
||||||
DependencyConfig, EnvironmentConfig, InlineExecutionConfig, InlineExecutionStrategy,
|
DependencyConfig, EnvironmentConfig, InlineExecutionConfig, InlineExecutionStrategy,
|
||||||
InterpreterConfig, RuntimeExecutionConfig,
|
InterpreterConfig, RuntimeEnvVarConfig, RuntimeEnvVarOperation, RuntimeEnvVarSpec,
|
||||||
|
RuntimeExecutionConfig,
|
||||||
};
|
};
|
||||||
use attune_common::models::{OutputFormat, ParameterDelivery, ParameterFormat};
|
use attune_common::models::{OutputFormat, ParameterDelivery, ParameterFormat};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -1331,6 +1375,88 @@ mod tests {
|
|||||||
assert!(result.stdout.contains("hello from python process runtime"));
|
assert!(result.stdout.contains("hello from python process runtime"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_python_file_with_pack_lib_on_pythonpath() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let packs_dir = temp_dir.path().join("packs");
|
||||||
|
let pack_dir = packs_dir.join("testpack");
|
||||||
|
let actions_dir = pack_dir.join("actions");
|
||||||
|
let lib_dir = pack_dir.join("lib");
|
||||||
|
std::fs::create_dir_all(&actions_dir).unwrap();
|
||||||
|
std::fs::create_dir_all(&lib_dir).unwrap();
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
lib_dir.join("helper.py"),
|
||||||
|
"def message():\n return 'hello from pack lib'\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
actions_dir.join("hello.py"),
|
||||||
|
"import helper\nimport os\nprint(helper.message())\nprint(os.environ['PYTHONPATH'])\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut env_vars = HashMap::new();
|
||||||
|
env_vars.insert(
|
||||||
|
"PYTHONPATH".to_string(),
|
||||||
|
RuntimeEnvVarConfig::Spec(RuntimeEnvVarSpec {
|
||||||
|
value: "{pack_dir}/lib".to_string(),
|
||||||
|
operation: RuntimeEnvVarOperation::Prepend,
|
||||||
|
separator: ":".to_string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let runtime = ProcessRuntime::new(
|
||||||
|
"python".to_string(),
|
||||||
|
RuntimeExecutionConfig {
|
||||||
|
interpreter: InterpreterConfig {
|
||||||
|
binary: "python3".to_string(),
|
||||||
|
args: vec![],
|
||||||
|
file_extension: Some(".py".to_string()),
|
||||||
|
},
|
||||||
|
inline_execution: InlineExecutionConfig::default(),
|
||||||
|
environment: None,
|
||||||
|
dependencies: None,
|
||||||
|
env_vars,
|
||||||
|
},
|
||||||
|
packs_dir,
|
||||||
|
temp_dir.path().join("runtime_envs"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut env = HashMap::new();
|
||||||
|
env.insert("PYTHONPATH".to_string(), "/existing/pythonpath".to_string());
|
||||||
|
|
||||||
|
let context = ExecutionContext {
|
||||||
|
execution_id: 3,
|
||||||
|
action_ref: "testpack.hello".to_string(),
|
||||||
|
parameters: HashMap::new(),
|
||||||
|
env,
|
||||||
|
secrets: HashMap::new(),
|
||||||
|
timeout: Some(10),
|
||||||
|
working_dir: None,
|
||||||
|
entry_point: "hello.py".to_string(),
|
||||||
|
code: None,
|
||||||
|
code_path: Some(actions_dir.join("hello.py")),
|
||||||
|
runtime_name: Some("python".to_string()),
|
||||||
|
runtime_config_override: None,
|
||||||
|
runtime_env_dir_suffix: None,
|
||||||
|
selected_runtime_version: None,
|
||||||
|
max_stdout_bytes: 1024 * 1024,
|
||||||
|
max_stderr_bytes: 1024 * 1024,
|
||||||
|
parameter_delivery: ParameterDelivery::default(),
|
||||||
|
parameter_format: ParameterFormat::default(),
|
||||||
|
output_format: OutputFormat::default(),
|
||||||
|
cancel_token: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = runtime.execute(context).await.unwrap();
|
||||||
|
assert_eq!(result.exit_code, 0);
|
||||||
|
assert!(result.stdout.contains("hello from pack lib"));
|
||||||
|
assert!(result
|
||||||
|
.stdout
|
||||||
|
.contains(&format!("{}/lib:/existing/pythonpath", pack_dir.display())));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_execute_inline_code() {
|
async fn test_execute_inline_code() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
|||||||
6
docker/Dockerfile.agent-package
Normal file
6
docker/Dockerfile.agent-package
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM busybox:1.36
|
||||||
|
|
||||||
|
COPY dist/attune-agent /usr/local/bin/attune-agent
|
||||||
|
COPY dist/attune-sensor-agent /usr/local/bin/attune-sensor-agent
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/attune-agent"]
|
||||||
33
docker/Dockerfile.runtime
Normal file
33
docker/Dockerfile.runtime
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
ARG DEBIAN_VERSION=bookworm
|
||||||
|
|
||||||
|
FROM debian:${DEBIAN_VERSION}-slim AS runtime
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
libssl3 \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN useradd -m -u 1000 attune && \
|
||||||
|
mkdir -p /opt/attune/packs /opt/attune/logs /opt/attune/runtime_envs /opt/attune/config /opt/attune/artifacts /opt/attune/agent && \
|
||||||
|
chown -R attune:attune /opt/attune
|
||||||
|
|
||||||
|
WORKDIR /opt/attune
|
||||||
|
|
||||||
|
COPY dist/attune-service-binary /usr/local/bin/attune-service
|
||||||
|
COPY migrations/ ./migrations/
|
||||||
|
|
||||||
|
RUN chown -R attune:attune /opt/attune
|
||||||
|
|
||||||
|
USER attune
|
||||||
|
|
||||||
|
ENV RUST_LOG=info
|
||||||
|
ENV ATTUNE_CONFIG=/opt/attune/config/config.yaml
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/attune-service"]
|
||||||
@@ -115,6 +115,61 @@ COMMENT ON COLUMN permission_assignment.permset IS 'Permission set being assigne
|
|||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE identity
|
||||||
|
ADD COLUMN frozen BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
CREATE INDEX idx_identity_frozen ON identity(frozen);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN identity.frozen IS 'If true, authentication is blocked for this identity';
|
||||||
|
|
||||||
|
CREATE TABLE identity_role_assignment (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
identity BIGINT NOT NULL REFERENCES identity(id) ON DELETE CASCADE,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL DEFAULT 'manual',
|
||||||
|
managed BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_identity_role_assignment UNIQUE (identity, role)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_identity_role_assignment_identity
|
||||||
|
ON identity_role_assignment(identity);
|
||||||
|
CREATE INDEX idx_identity_role_assignment_role
|
||||||
|
ON identity_role_assignment(role);
|
||||||
|
CREATE INDEX idx_identity_role_assignment_source
|
||||||
|
ON identity_role_assignment(source);
|
||||||
|
|
||||||
|
CREATE TRIGGER update_identity_role_assignment_updated
|
||||||
|
BEFORE UPDATE ON identity_role_assignment
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_updated_column();
|
||||||
|
|
||||||
|
COMMENT ON TABLE identity_role_assignment IS 'Links identities to role labels from manual assignment or external identity providers';
|
||||||
|
COMMENT ON COLUMN identity_role_assignment.role IS 'Opaque role/group label (e.g. IDP group name)';
|
||||||
|
COMMENT ON COLUMN identity_role_assignment.source IS 'Where the role assignment originated (manual, oidc, ldap, sync, etc.)';
|
||||||
|
COMMENT ON COLUMN identity_role_assignment.managed IS 'True when the assignment is managed by external sync and should not be edited manually';
|
||||||
|
|
||||||
|
CREATE TABLE permission_set_role_assignment (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
permset BIGINT NOT NULL REFERENCES permission_set(id) ON DELETE CASCADE,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT unique_permission_set_role_assignment UNIQUE (permset, role)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_permission_set_role_assignment_permset
|
||||||
|
ON permission_set_role_assignment(permset);
|
||||||
|
CREATE INDEX idx_permission_set_role_assignment_role
|
||||||
|
ON permission_set_role_assignment(role);
|
||||||
|
|
||||||
|
COMMENT ON TABLE permission_set_role_assignment IS 'Links permission sets to role labels for role-based grant expansion';
|
||||||
|
COMMENT ON COLUMN permission_set_role_assignment.role IS 'Opaque role/group label associated with the permission set';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- POLICY TABLE
|
-- POLICY TABLE
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ CREATE TABLE sensor (
|
|||||||
pack BIGINT REFERENCES pack(id) ON DELETE CASCADE,
|
pack BIGINT REFERENCES pack(id) ON DELETE CASCADE,
|
||||||
pack_ref TEXT,
|
pack_ref TEXT,
|
||||||
label TEXT NOT NULL,
|
label TEXT NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT,
|
||||||
entrypoint TEXT NOT NULL,
|
entrypoint TEXT NOT NULL,
|
||||||
runtime BIGINT NOT NULL REFERENCES runtime(id) ON DELETE CASCADE,
|
runtime BIGINT NOT NULL REFERENCES runtime(id) ON DELETE CASCADE,
|
||||||
runtime_ref TEXT NOT NULL,
|
runtime_ref TEXT NOT NULL,
|
||||||
@@ -223,7 +223,7 @@ CREATE TABLE action (
|
|||||||
pack BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE,
|
pack BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE,
|
||||||
pack_ref TEXT NOT NULL,
|
pack_ref TEXT NOT NULL,
|
||||||
label TEXT NOT NULL,
|
label TEXT NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT,
|
||||||
entrypoint TEXT NOT NULL,
|
entrypoint TEXT NOT NULL,
|
||||||
runtime BIGINT REFERENCES runtime(id),
|
runtime BIGINT REFERENCES runtime(id),
|
||||||
param_schema JSONB,
|
param_schema JSONB,
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ CREATE TABLE rule (
|
|||||||
pack BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE,
|
pack BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE,
|
||||||
pack_ref TEXT NOT NULL,
|
pack_ref TEXT NOT NULL,
|
||||||
label TEXT NOT NULL,
|
label TEXT NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT,
|
||||||
action BIGINT REFERENCES action(id) ON DELETE SET NULL,
|
action BIGINT REFERENCES action(id) ON DELETE SET NULL,
|
||||||
action_ref TEXT NOT NULL,
|
action_ref TEXT NOT NULL,
|
||||||
trigger BIGINT REFERENCES trigger(id) ON DELETE SET NULL,
|
trigger BIGINT REFERENCES trigger(id) ON DELETE SET NULL,
|
||||||
|
|||||||
@@ -11,25 +11,17 @@ grants:
|
|||||||
- resource: triggers
|
- resource: triggers
|
||||||
actions: [read, create, update, delete]
|
actions: [read, create, update, delete]
|
||||||
- resource: executions
|
- resource: executions
|
||||||
actions: [read, create, update, delete, cancel]
|
actions: [read, update, cancel]
|
||||||
- resource: events
|
- resource: events
|
||||||
actions: [read, create, delete]
|
actions: [read]
|
||||||
- resource: enforcements
|
- resource: enforcements
|
||||||
actions: [read, create, delete]
|
actions: [read]
|
||||||
- resource: inquiries
|
- resource: inquiries
|
||||||
actions: [read, create, update, delete, respond]
|
actions: [read, create, update, delete, respond]
|
||||||
- resource: keys
|
- resource: keys
|
||||||
actions: [read, create, update, delete]
|
actions: [read, create, update, delete, decrypt]
|
||||||
- resource: artifacts
|
- resource: artifacts
|
||||||
actions: [read, create, update, delete]
|
actions: [read, create, update, delete]
|
||||||
- resource: workflows
|
|
||||||
actions: [read, create, update, delete]
|
|
||||||
- resource: webhooks
|
|
||||||
actions: [read, create, update, delete]
|
|
||||||
- resource: analytics
|
|
||||||
actions: [read]
|
|
||||||
- resource: history
|
|
||||||
actions: [read]
|
|
||||||
- resource: identities
|
- resource: identities
|
||||||
actions: [read, create, update, delete]
|
actions: [read, create, update, delete]
|
||||||
- resource: permissions
|
- resource: permissions
|
||||||
|
|||||||
@@ -11,14 +11,8 @@ grants:
|
|||||||
- resource: triggers
|
- resource: triggers
|
||||||
actions: [read]
|
actions: [read]
|
||||||
- resource: executions
|
- resource: executions
|
||||||
actions: [read, create, cancel]
|
actions: [read, cancel]
|
||||||
- resource: keys
|
- resource: keys
|
||||||
actions: [read, update]
|
actions: [read, update, decrypt]
|
||||||
- resource: artifacts
|
- resource: artifacts
|
||||||
actions: [read]
|
actions: [read]
|
||||||
- resource: workflows
|
|
||||||
actions: [read, create, update]
|
|
||||||
- resource: analytics
|
|
||||||
actions: [read]
|
|
||||||
- resource: history
|
|
||||||
actions: [read]
|
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ grants:
|
|||||||
- resource: triggers
|
- resource: triggers
|
||||||
actions: [read]
|
actions: [read]
|
||||||
- resource: executions
|
- resource: executions
|
||||||
actions: [read, create]
|
actions: [read]
|
||||||
|
- resource: keys
|
||||||
|
actions: [read]
|
||||||
- resource: artifacts
|
- resource: artifacts
|
||||||
actions: [read]
|
actions: [read]
|
||||||
- resource: analytics
|
|
||||||
actions: [read]
|
|
||||||
- resource: history
|
|
||||||
actions: [read]
|
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ grants:
|
|||||||
actions: [read]
|
actions: [read]
|
||||||
- resource: executions
|
- resource: executions
|
||||||
actions: [read]
|
actions: [read]
|
||||||
|
- resource: keys
|
||||||
|
actions: [read]
|
||||||
- resource: artifacts
|
- resource: artifacts
|
||||||
actions: [read]
|
actions: [read]
|
||||||
- resource: analytics
|
|
||||||
actions: [read]
|
|
||||||
- resource: history
|
|
||||||
actions: [read]
|
|
||||||
|
|||||||
@@ -12,6 +12,41 @@ Each runtime YAML file contains only the fields that are stored in the database:
|
|||||||
- `description` - Brief description of the runtime
|
- `description` - Brief description of the runtime
|
||||||
- `distributions` - Runtime verification and capability metadata (JSONB)
|
- `distributions` - Runtime verification and capability metadata (JSONB)
|
||||||
- `installation` - Installation requirements and metadata (JSONB)
|
- `installation` - Installation requirements and metadata (JSONB)
|
||||||
|
- `execution_config` - Interpreter, environment, dependency, and execution-time env var metadata
|
||||||
|
|
||||||
|
## `execution_config.env_vars`
|
||||||
|
|
||||||
|
Runtime authors can declare execution-time environment variables in a purely declarative way.
|
||||||
|
|
||||||
|
String values replace the variable entirely:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env_vars:
|
||||||
|
NODE_PATH: "{env_dir}/node_modules"
|
||||||
|
```
|
||||||
|
|
||||||
|
Object values support merge semantics against an existing value already present in the execution environment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env_vars:
|
||||||
|
PYTHONPATH:
|
||||||
|
operation: prepend
|
||||||
|
value: "{pack_dir}/lib"
|
||||||
|
separator: ":"
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported operations:
|
||||||
|
|
||||||
|
- `set` - Replace the variable with the resolved value
|
||||||
|
- `prepend` - Add the resolved value before the existing value
|
||||||
|
- `append` - Add the resolved value after the existing value
|
||||||
|
|
||||||
|
Supported template variables:
|
||||||
|
|
||||||
|
- `{pack_dir}`
|
||||||
|
- `{env_dir}`
|
||||||
|
- `{interpreter}`
|
||||||
|
- `{manifest_path}`
|
||||||
|
|
||||||
## Available Runtimes
|
## Available Runtimes
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ execution_config:
|
|||||||
- install
|
- install
|
||||||
- "-r"
|
- "-r"
|
||||||
- "{manifest_path}"
|
- "{manifest_path}"
|
||||||
|
env_vars:
|
||||||
|
PYTHONPATH:
|
||||||
|
operation: prepend
|
||||||
|
value: "{pack_dir}/lib"
|
||||||
|
separator: ":"
|
||||||
|
|
||||||
# Version-specific execution configurations.
|
# Version-specific execution configurations.
|
||||||
# Each entry describes how to invoke a particular Python version.
|
# Each entry describes how to invoke a particular Python version.
|
||||||
@@ -96,6 +101,11 @@ versions:
|
|||||||
- install
|
- install
|
||||||
- "-r"
|
- "-r"
|
||||||
- "{manifest_path}"
|
- "{manifest_path}"
|
||||||
|
env_vars:
|
||||||
|
PYTHONPATH:
|
||||||
|
operation: prepend
|
||||||
|
value: "{pack_dir}/lib"
|
||||||
|
separator: ":"
|
||||||
|
|
||||||
- version: "3.12"
|
- version: "3.12"
|
||||||
is_default: true
|
is_default: true
|
||||||
@@ -133,6 +143,11 @@ versions:
|
|||||||
- install
|
- install
|
||||||
- "-r"
|
- "-r"
|
||||||
- "{manifest_path}"
|
- "{manifest_path}"
|
||||||
|
env_vars:
|
||||||
|
PYTHONPATH:
|
||||||
|
operation: prepend
|
||||||
|
value: "{pack_dir}/lib"
|
||||||
|
separator: ":"
|
||||||
|
|
||||||
- version: "3.13"
|
- version: "3.13"
|
||||||
distributions:
|
distributions:
|
||||||
@@ -169,3 +184,8 @@ versions:
|
|||||||
- install
|
- install
|
||||||
- "-r"
|
- "-r"
|
||||||
- "{manifest_path}"
|
- "{manifest_path}"
|
||||||
|
env_vars:
|
||||||
|
PYTHONPATH:
|
||||||
|
operation: prepend
|
||||||
|
value: "{pack_dir}/lib"
|
||||||
|
separator: ":"
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ const TriggerCreatePage = lazy(
|
|||||||
);
|
);
|
||||||
const TriggerEditPage = lazy(() => import("@/pages/triggers/TriggerEditPage"));
|
const TriggerEditPage = lazy(() => import("@/pages/triggers/TriggerEditPage"));
|
||||||
const SensorsPage = lazy(() => import("@/pages/sensors/SensorsPage"));
|
const SensorsPage = lazy(() => import("@/pages/sensors/SensorsPage"));
|
||||||
|
const AccessControlPage = lazy(
|
||||||
|
() => import("@/pages/access-control/AccessControlPage"),
|
||||||
|
);
|
||||||
|
const IdentityDetailPage = lazy(
|
||||||
|
() => import("@/pages/access-control/IdentityDetailPage"),
|
||||||
|
);
|
||||||
|
const PermissionSetDetailPage = lazy(
|
||||||
|
() => import("@/pages/access-control/PermissionSetDetailPage"),
|
||||||
|
);
|
||||||
|
|
||||||
function PageLoader() {
|
function PageLoader() {
|
||||||
return (
|
return (
|
||||||
@@ -134,6 +143,18 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
<Route path="sensors" element={<SensorsPage />} />
|
<Route path="sensors" element={<SensorsPage />} />
|
||||||
<Route path="sensors/:ref" element={<SensorsPage />} />
|
<Route path="sensors/:ref" element={<SensorsPage />} />
|
||||||
|
<Route
|
||||||
|
path="access-control"
|
||||||
|
element={<AccessControlPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="access-control/identities/:id"
|
||||||
|
element={<IdentityDetailPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="access-control/permission-sets/:ref"
|
||||||
|
element={<PermissionSetDetailPage />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Catch all - redirect to dashboard */}
|
{/* Catch all - redirect to dashboard */}
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ export type { OpenAPIConfig } from './core/OpenAPI';
|
|||||||
|
|
||||||
export type { ActionResponse } from './models/ActionResponse';
|
export type { ActionResponse } from './models/ActionResponse';
|
||||||
export type { ActionSummary } from './models/ActionSummary';
|
export type { ActionSummary } from './models/ActionSummary';
|
||||||
|
export type { AgentArchInfo } from './models/AgentArchInfo';
|
||||||
|
export type { AgentBinaryInfo } from './models/AgentBinaryInfo';
|
||||||
export type { ApiResponse_ActionResponse } from './models/ApiResponse_ActionResponse';
|
export type { ApiResponse_ActionResponse } from './models/ApiResponse_ActionResponse';
|
||||||
|
export type { ApiResponse_AuthSettingsResponse } from './models/ApiResponse_AuthSettingsResponse';
|
||||||
export type { ApiResponse_CurrentUserResponse } from './models/ApiResponse_CurrentUserResponse';
|
export type { ApiResponse_CurrentUserResponse } from './models/ApiResponse_CurrentUserResponse';
|
||||||
export type { ApiResponse_EnforcementResponse } from './models/ApiResponse_EnforcementResponse';
|
export type { ApiResponse_EnforcementResponse } from './models/ApiResponse_EnforcementResponse';
|
||||||
export type { ApiResponse_EventResponse } from './models/ApiResponse_EventResponse';
|
export type { ApiResponse_EventResponse } from './models/ApiResponse_EventResponse';
|
||||||
export type { ApiResponse_ExecutionResponse } from './models/ApiResponse_ExecutionResponse';
|
export type { ApiResponse_ExecutionResponse } from './models/ApiResponse_ExecutionResponse';
|
||||||
export type { ApiResponse_IdentitySummary } from './models/ApiResponse_IdentitySummary';
|
export type { ApiResponse_IdentityResponse } from './models/ApiResponse_IdentityResponse';
|
||||||
export type { ApiResponse_InquiryResponse } from './models/ApiResponse_InquiryResponse';
|
export type { ApiResponse_InquiryResponse } from './models/ApiResponse_InquiryResponse';
|
||||||
export type { ApiResponse_KeyResponse } from './models/ApiResponse_KeyResponse';
|
export type { ApiResponse_KeyResponse } from './models/ApiResponse_KeyResponse';
|
||||||
export type { ApiResponse_PackInstallResponse } from './models/ApiResponse_PackInstallResponse';
|
export type { ApiResponse_PackInstallResponse } from './models/ApiResponse_PackInstallResponse';
|
||||||
@@ -32,10 +35,12 @@ export type { ApiResponse_WorkflowResponse } from './models/ApiResponse_Workflow
|
|||||||
export type { ChangePasswordRequest } from './models/ChangePasswordRequest';
|
export type { ChangePasswordRequest } from './models/ChangePasswordRequest';
|
||||||
export type { CreateActionRequest } from './models/CreateActionRequest';
|
export type { CreateActionRequest } from './models/CreateActionRequest';
|
||||||
export type { CreateIdentityRequest } from './models/CreateIdentityRequest';
|
export type { CreateIdentityRequest } from './models/CreateIdentityRequest';
|
||||||
|
export type { CreateIdentityRoleAssignmentRequest } from './models/CreateIdentityRoleAssignmentRequest';
|
||||||
export type { CreateInquiryRequest } from './models/CreateInquiryRequest';
|
export type { CreateInquiryRequest } from './models/CreateInquiryRequest';
|
||||||
export type { CreateKeyRequest } from './models/CreateKeyRequest';
|
export type { CreateKeyRequest } from './models/CreateKeyRequest';
|
||||||
export type { CreatePackRequest } from './models/CreatePackRequest';
|
export type { CreatePackRequest } from './models/CreatePackRequest';
|
||||||
export type { CreatePermissionAssignmentRequest } from './models/CreatePermissionAssignmentRequest';
|
export type { CreatePermissionAssignmentRequest } from './models/CreatePermissionAssignmentRequest';
|
||||||
|
export type { CreatePermissionSetRoleAssignmentRequest } from './models/CreatePermissionSetRoleAssignmentRequest';
|
||||||
export type { CreateRuleRequest } from './models/CreateRuleRequest';
|
export type { CreateRuleRequest } from './models/CreateRuleRequest';
|
||||||
export type { CreateRuntimeRequest } from './models/CreateRuntimeRequest';
|
export type { CreateRuntimeRequest } from './models/CreateRuntimeRequest';
|
||||||
export type { CreateSensorRequest } from './models/CreateSensorRequest';
|
export type { CreateSensorRequest } from './models/CreateSensorRequest';
|
||||||
@@ -53,6 +58,8 @@ export { ExecutionStatus } from './models/ExecutionStatus';
|
|||||||
export type { ExecutionSummary } from './models/ExecutionSummary';
|
export type { ExecutionSummary } from './models/ExecutionSummary';
|
||||||
export type { HealthResponse } from './models/HealthResponse';
|
export type { HealthResponse } from './models/HealthResponse';
|
||||||
export type { i64 } from './models/i64';
|
export type { i64 } from './models/i64';
|
||||||
|
export type { IdentityResponse } from './models/IdentityResponse';
|
||||||
|
export type { IdentityRoleAssignmentResponse } from './models/IdentityRoleAssignmentResponse';
|
||||||
export type { IdentitySummary } from './models/IdentitySummary';
|
export type { IdentitySummary } from './models/IdentitySummary';
|
||||||
export type { InquiryRespondRequest } from './models/InquiryRespondRequest';
|
export type { InquiryRespondRequest } from './models/InquiryRespondRequest';
|
||||||
export type { InquiryResponse } from './models/InquiryResponse';
|
export type { InquiryResponse } from './models/InquiryResponse';
|
||||||
@@ -61,10 +68,12 @@ export type { InquirySummary } from './models/InquirySummary';
|
|||||||
export type { InstallPackRequest } from './models/InstallPackRequest';
|
export type { InstallPackRequest } from './models/InstallPackRequest';
|
||||||
export type { KeyResponse } from './models/KeyResponse';
|
export type { KeyResponse } from './models/KeyResponse';
|
||||||
export type { KeySummary } from './models/KeySummary';
|
export type { KeySummary } from './models/KeySummary';
|
||||||
|
export type { LdapLoginRequest } from './models/LdapLoginRequest';
|
||||||
export type { LoginRequest } from './models/LoginRequest';
|
export type { LoginRequest } from './models/LoginRequest';
|
||||||
export { NullableJsonPatch } from './models/NullableJsonPatch';
|
export { NullableJsonPatch } from './models/NullableJsonPatch';
|
||||||
export { NullableStringPatch } from './models/NullableStringPatch';
|
export { NullableStringPatch } from './models/NullableStringPatch';
|
||||||
export { OwnerType } from './models/OwnerType';
|
export { OwnerType } from './models/OwnerType';
|
||||||
|
export { PackDescriptionPatch } from './models/PackDescriptionPatch';
|
||||||
export type { PackInstallResponse } from './models/PackInstallResponse';
|
export type { PackInstallResponse } from './models/PackInstallResponse';
|
||||||
export type { PackResponse } from './models/PackResponse';
|
export type { PackResponse } from './models/PackResponse';
|
||||||
export type { PackSummary } from './models/PackSummary';
|
export type { PackSummary } from './models/PackSummary';
|
||||||
@@ -89,6 +98,7 @@ export type { PaginatedResponse_TriggerSummary } from './models/PaginatedRespons
|
|||||||
export type { PaginatedResponse_WorkflowSummary } from './models/PaginatedResponse_WorkflowSummary';
|
export type { PaginatedResponse_WorkflowSummary } from './models/PaginatedResponse_WorkflowSummary';
|
||||||
export type { PaginationMeta } from './models/PaginationMeta';
|
export type { PaginationMeta } from './models/PaginationMeta';
|
||||||
export type { PermissionAssignmentResponse } from './models/PermissionAssignmentResponse';
|
export type { PermissionAssignmentResponse } from './models/PermissionAssignmentResponse';
|
||||||
|
export type { PermissionSetRoleAssignmentResponse } from './models/PermissionSetRoleAssignmentResponse';
|
||||||
export type { PermissionSetSummary } from './models/PermissionSetSummary';
|
export type { PermissionSetSummary } from './models/PermissionSetSummary';
|
||||||
export type { QueueStatsResponse } from './models/QueueStatsResponse';
|
export type { QueueStatsResponse } from './models/QueueStatsResponse';
|
||||||
export type { RefreshTokenRequest } from './models/RefreshTokenRequest';
|
export type { RefreshTokenRequest } from './models/RefreshTokenRequest';
|
||||||
@@ -98,6 +108,7 @@ export type { RuleResponse } from './models/RuleResponse';
|
|||||||
export type { RuleSummary } from './models/RuleSummary';
|
export type { RuleSummary } from './models/RuleSummary';
|
||||||
export type { RuntimeResponse } from './models/RuntimeResponse';
|
export type { RuntimeResponse } from './models/RuntimeResponse';
|
||||||
export type { RuntimeSummary } from './models/RuntimeSummary';
|
export type { RuntimeSummary } from './models/RuntimeSummary';
|
||||||
|
export { RuntimeVersionConstraintPatch } from './models/RuntimeVersionConstraintPatch';
|
||||||
export type { SensorResponse } from './models/SensorResponse';
|
export type { SensorResponse } from './models/SensorResponse';
|
||||||
export type { SensorSummary } from './models/SensorSummary';
|
export type { SensorSummary } from './models/SensorSummary';
|
||||||
export type { SuccessResponse } from './models/SuccessResponse';
|
export type { SuccessResponse } from './models/SuccessResponse';
|
||||||
@@ -106,6 +117,7 @@ export { TestStatus } from './models/TestStatus';
|
|||||||
export type { TestSuiteResult } from './models/TestSuiteResult';
|
export type { TestSuiteResult } from './models/TestSuiteResult';
|
||||||
export type { TokenResponse } from './models/TokenResponse';
|
export type { TokenResponse } from './models/TokenResponse';
|
||||||
export type { TriggerResponse } from './models/TriggerResponse';
|
export type { TriggerResponse } from './models/TriggerResponse';
|
||||||
|
export { TriggerStringPatch } from './models/TriggerStringPatch';
|
||||||
export type { TriggerSummary } from './models/TriggerSummary';
|
export type { TriggerSummary } from './models/TriggerSummary';
|
||||||
export type { UpdateActionRequest } from './models/UpdateActionRequest';
|
export type { UpdateActionRequest } from './models/UpdateActionRequest';
|
||||||
export type { UpdateIdentityRequest } from './models/UpdateIdentityRequest';
|
export type { UpdateIdentityRequest } from './models/UpdateIdentityRequest';
|
||||||
@@ -126,6 +138,7 @@ export type { WorkflowSummary } from './models/WorkflowSummary';
|
|||||||
export type { WorkflowSyncResult } from './models/WorkflowSyncResult';
|
export type { WorkflowSyncResult } from './models/WorkflowSyncResult';
|
||||||
|
|
||||||
export { ActionsService } from './services/ActionsService';
|
export { ActionsService } from './services/ActionsService';
|
||||||
|
export { AgentService } from './services/AgentService';
|
||||||
export { AuthService } from './services/AuthService';
|
export { AuthService } from './services/AuthService';
|
||||||
export { EnforcementsService } from './services/EnforcementsService';
|
export { EnforcementsService } from './services/EnforcementsService';
|
||||||
export { EventsService } from './services/EventsService';
|
export { EventsService } from './services/EventsService';
|
||||||
|
|||||||
@@ -6,65 +6,64 @@
|
|||||||
* Response DTO for action information
|
* Response DTO for action information
|
||||||
*/
|
*/
|
||||||
export type ActionResponse = {
|
export type ActionResponse = {
|
||||||
/**
|
/**
|
||||||
* Creation timestamp
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
created: string;
|
created: string;
|
||||||
/**
|
/**
|
||||||
* Action description
|
* Action description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string | null;
|
||||||
/**
|
/**
|
||||||
* Entry point
|
* Entry point
|
||||||
*/
|
*/
|
||||||
entrypoint: string;
|
entrypoint: string;
|
||||||
/**
|
/**
|
||||||
* Action ID
|
* Action ID
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Whether this is an ad-hoc action (not from pack installation)
|
* Whether this is an ad-hoc action (not from pack installation)
|
||||||
*/
|
*/
|
||||||
is_adhoc: boolean;
|
is_adhoc: boolean;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Output schema
|
* Output schema
|
||||||
*/
|
*/
|
||||||
out_schema: any | null;
|
out_schema: any | null;
|
||||||
/**
|
/**
|
||||||
* Pack ID
|
* Pack ID
|
||||||
*/
|
*/
|
||||||
pack: number;
|
pack: number;
|
||||||
/**
|
/**
|
||||||
* Pack reference
|
* Pack reference
|
||||||
*/
|
*/
|
||||||
pack_ref: string;
|
pack_ref: string;
|
||||||
/**
|
/**
|
||||||
* Parameter schema (StackStorm-style with inline required/secret)
|
* Parameter schema (StackStorm-style with inline required/secret)
|
||||||
*/
|
*/
|
||||||
param_schema: any | null;
|
param_schema: any | null;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier
|
* Unique reference identifier
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Runtime ID
|
* Runtime ID
|
||||||
*/
|
*/
|
||||||
runtime?: number | null;
|
runtime?: number | null;
|
||||||
/**
|
/**
|
||||||
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||||
*/
|
*/
|
||||||
runtime_version_constraint?: string | null;
|
runtime_version_constraint?: string | null;
|
||||||
/**
|
/**
|
||||||
* Last update timestamp
|
* Last update timestamp
|
||||||
*/
|
*/
|
||||||
updated: string;
|
updated: string;
|
||||||
/**
|
/**
|
||||||
* Workflow definition ID (non-null if this action is a workflow)
|
* Workflow definition ID (non-null if this action is a workflow)
|
||||||
*/
|
*/
|
||||||
workflow_def?: number | null;
|
workflow_def?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,49 +6,48 @@
|
|||||||
* Simplified action response (for list endpoints)
|
* Simplified action response (for list endpoints)
|
||||||
*/
|
*/
|
||||||
export type ActionSummary = {
|
export type ActionSummary = {
|
||||||
/**
|
/**
|
||||||
* Creation timestamp
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
created: string;
|
created: string;
|
||||||
/**
|
/**
|
||||||
* Action description
|
* Action description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string | null;
|
||||||
/**
|
/**
|
||||||
* Entry point
|
* Entry point
|
||||||
*/
|
*/
|
||||||
entrypoint: string;
|
entrypoint: string;
|
||||||
/**
|
/**
|
||||||
* Action ID
|
* Action ID
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Pack reference
|
* Pack reference
|
||||||
*/
|
*/
|
||||||
pack_ref: string;
|
pack_ref: string;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier
|
* Unique reference identifier
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Runtime ID
|
* Runtime ID
|
||||||
*/
|
*/
|
||||||
runtime?: number | null;
|
runtime?: number | null;
|
||||||
/**
|
/**
|
||||||
* Semver version constraint for the runtime
|
* Semver version constraint for the runtime
|
||||||
*/
|
*/
|
||||||
runtime_version_constraint?: string | null;
|
runtime_version_constraint?: string | null;
|
||||||
/**
|
/**
|
||||||
* Last update timestamp
|
* Last update timestamp
|
||||||
*/
|
*/
|
||||||
updated: string;
|
updated: string;
|
||||||
/**
|
/**
|
||||||
* Workflow definition ID (non-null if this action is a workflow)
|
* Workflow definition ID (non-null if this action is a workflow)
|
||||||
*/
|
*/
|
||||||
workflow_def?: number | null;
|
workflow_def?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
22
web/src/api/models/AgentArchInfo.ts
Normal file
22
web/src/api/models/AgentArchInfo.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Per-architecture binary info
|
||||||
|
*/
|
||||||
|
export type AgentArchInfo = {
|
||||||
|
/**
|
||||||
|
* Architecture name
|
||||||
|
*/
|
||||||
|
arch: string;
|
||||||
|
/**
|
||||||
|
* Whether this binary is available
|
||||||
|
*/
|
||||||
|
available: boolean;
|
||||||
|
/**
|
||||||
|
* Binary size in bytes
|
||||||
|
*/
|
||||||
|
size_bytes: number;
|
||||||
|
};
|
||||||
|
|
||||||
19
web/src/api/models/AgentBinaryInfo.ts
Normal file
19
web/src/api/models/AgentBinaryInfo.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { AgentArchInfo } from './AgentArchInfo';
|
||||||
|
/**
|
||||||
|
* Agent binary metadata
|
||||||
|
*/
|
||||||
|
export type AgentBinaryInfo = {
|
||||||
|
/**
|
||||||
|
* Available architectures
|
||||||
|
*/
|
||||||
|
architectures: Array<AgentArchInfo>;
|
||||||
|
/**
|
||||||
|
* Agent version (from build)
|
||||||
|
*/
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -6,74 +6,73 @@
|
|||||||
* Standard API response wrapper
|
* Standard API response wrapper
|
||||||
*/
|
*/
|
||||||
export type ApiResponse_ActionResponse = {
|
export type ApiResponse_ActionResponse = {
|
||||||
|
/**
|
||||||
|
* Response DTO for action information
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
/**
|
/**
|
||||||
* Response DTO for action information
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
data: {
|
created: string;
|
||||||
/**
|
|
||||||
* Creation timestamp
|
|
||||||
*/
|
|
||||||
created: string;
|
|
||||||
/**
|
|
||||||
* Action description
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* Entry point
|
|
||||||
*/
|
|
||||||
entrypoint: string;
|
|
||||||
/**
|
|
||||||
* Action ID
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Whether this is an ad-hoc action (not from pack installation)
|
|
||||||
*/
|
|
||||||
is_adhoc: boolean;
|
|
||||||
/**
|
|
||||||
* Human-readable label
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Output schema
|
|
||||||
*/
|
|
||||||
out_schema: any | null;
|
|
||||||
/**
|
|
||||||
* Pack ID
|
|
||||||
*/
|
|
||||||
pack: number;
|
|
||||||
/**
|
|
||||||
* Pack reference
|
|
||||||
*/
|
|
||||||
pack_ref: string;
|
|
||||||
/**
|
|
||||||
* Parameter schema (StackStorm-style with inline required/secret)
|
|
||||||
*/
|
|
||||||
param_schema: any | null;
|
|
||||||
/**
|
|
||||||
* Unique reference identifier
|
|
||||||
*/
|
|
||||||
ref: string;
|
|
||||||
/**
|
|
||||||
* Runtime ID
|
|
||||||
*/
|
|
||||||
runtime?: number | null;
|
|
||||||
/**
|
|
||||||
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
|
||||||
*/
|
|
||||||
runtime_version_constraint?: string | null;
|
|
||||||
/**
|
|
||||||
* Last update timestamp
|
|
||||||
*/
|
|
||||||
updated: string;
|
|
||||||
/**
|
|
||||||
* Workflow definition ID (non-null if this action is a workflow)
|
|
||||||
*/
|
|
||||||
workflow_def?: number | null;
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Optional message
|
* Action description
|
||||||
*/
|
*/
|
||||||
message?: string | null;
|
description: string | null;
|
||||||
|
/**
|
||||||
|
* Entry point
|
||||||
|
*/
|
||||||
|
entrypoint: string;
|
||||||
|
/**
|
||||||
|
* Action ID
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Whether this is an ad-hoc action (not from pack installation)
|
||||||
|
*/
|
||||||
|
is_adhoc: boolean;
|
||||||
|
/**
|
||||||
|
* Human-readable label
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* Output schema
|
||||||
|
*/
|
||||||
|
out_schema: any | null;
|
||||||
|
/**
|
||||||
|
* Pack ID
|
||||||
|
*/
|
||||||
|
pack: number;
|
||||||
|
/**
|
||||||
|
* Pack reference
|
||||||
|
*/
|
||||||
|
pack_ref: string;
|
||||||
|
/**
|
||||||
|
* Parameter schema (StackStorm-style with inline required/secret)
|
||||||
|
*/
|
||||||
|
param_schema: any | null;
|
||||||
|
/**
|
||||||
|
* Unique reference identifier
|
||||||
|
*/
|
||||||
|
ref: string;
|
||||||
|
/**
|
||||||
|
* Runtime ID
|
||||||
|
*/
|
||||||
|
runtime?: number | null;
|
||||||
|
/**
|
||||||
|
* Semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||||
|
*/
|
||||||
|
runtime_version_constraint?: string | null;
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updated: string;
|
||||||
|
/**
|
||||||
|
* Workflow definition ID (non-null if this action is a workflow)
|
||||||
|
*/
|
||||||
|
workflow_def?: number | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Optional message
|
||||||
|
*/
|
||||||
|
message?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
75
web/src/api/models/ApiResponse_AuthSettingsResponse.ts
Normal file
75
web/src/api/models/ApiResponse_AuthSettingsResponse.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Standard API response wrapper
|
||||||
|
*/
|
||||||
|
export type ApiResponse_AuthSettingsResponse = {
|
||||||
|
/**
|
||||||
|
* Public authentication settings for the login page.
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* Whether authentication is enabled for the server.
|
||||||
|
*/
|
||||||
|
authentication_enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Whether LDAP login is configured and enabled.
|
||||||
|
*/
|
||||||
|
ldap_enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Optional icon URL shown beside the provider label.
|
||||||
|
*/
|
||||||
|
ldap_provider_icon_url?: string | null;
|
||||||
|
/**
|
||||||
|
* User-facing provider label for the login button.
|
||||||
|
*/
|
||||||
|
ldap_provider_label?: string | null;
|
||||||
|
/**
|
||||||
|
* Provider name for `?auth=<provider>`.
|
||||||
|
*/
|
||||||
|
ldap_provider_name?: string | null;
|
||||||
|
/**
|
||||||
|
* Whether LDAP login should be shown by default.
|
||||||
|
*/
|
||||||
|
ldap_visible_by_default: boolean;
|
||||||
|
/**
|
||||||
|
* Whether local username/password login is configured.
|
||||||
|
*/
|
||||||
|
local_password_enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Whether local username/password login should be shown by default.
|
||||||
|
*/
|
||||||
|
local_password_visible_by_default: boolean;
|
||||||
|
/**
|
||||||
|
* Whether OIDC login is configured and enabled.
|
||||||
|
*/
|
||||||
|
oidc_enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Optional icon URL shown beside the provider label.
|
||||||
|
*/
|
||||||
|
oidc_provider_icon_url?: string | null;
|
||||||
|
/**
|
||||||
|
* User-facing provider label for the login button.
|
||||||
|
*/
|
||||||
|
oidc_provider_label?: string | null;
|
||||||
|
/**
|
||||||
|
* Provider name for `?auth=<provider>`.
|
||||||
|
*/
|
||||||
|
oidc_provider_name?: string | null;
|
||||||
|
/**
|
||||||
|
* Whether OIDC login should be shown by default.
|
||||||
|
*/
|
||||||
|
oidc_visible_by_default: boolean;
|
||||||
|
/**
|
||||||
|
* Whether unauthenticated self-service registration is allowed.
|
||||||
|
*/
|
||||||
|
self_registration_enabled: boolean;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Optional message
|
||||||
|
*/
|
||||||
|
message?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -2,16 +2,21 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import type { IdentityRoleAssignmentResponse } from './IdentityRoleAssignmentResponse';
|
||||||
|
import type { PermissionAssignmentResponse } from './PermissionAssignmentResponse';
|
||||||
import type { Value } from './Value';
|
import type { Value } from './Value';
|
||||||
/**
|
/**
|
||||||
* Standard API response wrapper
|
* Standard API response wrapper
|
||||||
*/
|
*/
|
||||||
export type ApiResponse_IdentitySummary = {
|
export type ApiResponse_IdentityResponse = {
|
||||||
data: {
|
data: {
|
||||||
attributes: Value;
|
attributes: Value;
|
||||||
|
direct_permissions: Array<PermissionAssignmentResponse>;
|
||||||
display_name?: string | null;
|
display_name?: string | null;
|
||||||
|
frozen: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
login: string;
|
login: string;
|
||||||
|
roles: Array<IdentityRoleAssignmentResponse>;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Optional message
|
* Optional message
|
||||||
@@ -6,82 +6,81 @@
|
|||||||
* Standard API response wrapper
|
* Standard API response wrapper
|
||||||
*/
|
*/
|
||||||
export type ApiResponse_RuleResponse = {
|
export type ApiResponse_RuleResponse = {
|
||||||
|
/**
|
||||||
|
* Response DTO for rule information
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
/**
|
/**
|
||||||
* Response DTO for rule information
|
* Action ID (null if the referenced action has been deleted)
|
||||||
*/
|
*/
|
||||||
data: {
|
action?: number | null;
|
||||||
/**
|
|
||||||
* Action ID (null if the referenced action has been deleted)
|
|
||||||
*/
|
|
||||||
action?: number | null;
|
|
||||||
/**
|
|
||||||
* Parameters to pass to the action when rule is triggered
|
|
||||||
*/
|
|
||||||
action_params: Record<string, any>;
|
|
||||||
/**
|
|
||||||
* Action reference
|
|
||||||
*/
|
|
||||||
action_ref: string;
|
|
||||||
/**
|
|
||||||
* Conditions for rule evaluation
|
|
||||||
*/
|
|
||||||
conditions: Record<string, any>;
|
|
||||||
/**
|
|
||||||
* Creation timestamp
|
|
||||||
*/
|
|
||||||
created: string;
|
|
||||||
/**
|
|
||||||
* Rule description
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* Whether the rule is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
|
||||||
* Rule ID
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Whether this is an ad-hoc rule (not from pack installation)
|
|
||||||
*/
|
|
||||||
is_adhoc: boolean;
|
|
||||||
/**
|
|
||||||
* Human-readable label
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Pack ID
|
|
||||||
*/
|
|
||||||
pack: number;
|
|
||||||
/**
|
|
||||||
* Pack reference
|
|
||||||
*/
|
|
||||||
pack_ref: string;
|
|
||||||
/**
|
|
||||||
* Unique reference identifier
|
|
||||||
*/
|
|
||||||
ref: string;
|
|
||||||
/**
|
|
||||||
* Trigger ID (null if the referenced trigger has been deleted)
|
|
||||||
*/
|
|
||||||
trigger?: number | null;
|
|
||||||
/**
|
|
||||||
* Parameters for trigger configuration and event filtering
|
|
||||||
*/
|
|
||||||
trigger_params: Record<string, any>;
|
|
||||||
/**
|
|
||||||
* Trigger reference
|
|
||||||
*/
|
|
||||||
trigger_ref: string;
|
|
||||||
/**
|
|
||||||
* Last update timestamp
|
|
||||||
*/
|
|
||||||
updated: string;
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Optional message
|
* Parameters to pass to the action when rule is triggered
|
||||||
*/
|
*/
|
||||||
message?: string | null;
|
action_params: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* Action reference
|
||||||
|
*/
|
||||||
|
action_ref: string;
|
||||||
|
/**
|
||||||
|
* Conditions for rule evaluation
|
||||||
|
*/
|
||||||
|
conditions: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* Creation timestamp
|
||||||
|
*/
|
||||||
|
created: string;
|
||||||
|
/**
|
||||||
|
* Rule description
|
||||||
|
*/
|
||||||
|
description: string | null;
|
||||||
|
/**
|
||||||
|
* Whether the rule is enabled
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Rule ID
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Whether this is an ad-hoc rule (not from pack installation)
|
||||||
|
*/
|
||||||
|
is_adhoc: boolean;
|
||||||
|
/**
|
||||||
|
* Human-readable label
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* Pack ID
|
||||||
|
*/
|
||||||
|
pack: number;
|
||||||
|
/**
|
||||||
|
* Pack reference
|
||||||
|
*/
|
||||||
|
pack_ref: string;
|
||||||
|
/**
|
||||||
|
* Unique reference identifier
|
||||||
|
*/
|
||||||
|
ref: string;
|
||||||
|
/**
|
||||||
|
* Trigger ID (null if the referenced trigger has been deleted)
|
||||||
|
*/
|
||||||
|
trigger?: number | null;
|
||||||
|
/**
|
||||||
|
* Parameters for trigger configuration and event filtering
|
||||||
|
*/
|
||||||
|
trigger_params: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* Trigger reference
|
||||||
|
*/
|
||||||
|
trigger_ref: string;
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updated: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Optional message
|
||||||
|
*/
|
||||||
|
message?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,74 +6,73 @@
|
|||||||
* Standard API response wrapper
|
* Standard API response wrapper
|
||||||
*/
|
*/
|
||||||
export type ApiResponse_SensorResponse = {
|
export type ApiResponse_SensorResponse = {
|
||||||
|
/**
|
||||||
|
* Response DTO for sensor information
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
/**
|
/**
|
||||||
* Response DTO for sensor information
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
data: {
|
created: string;
|
||||||
/**
|
|
||||||
* Creation timestamp
|
|
||||||
*/
|
|
||||||
created: string;
|
|
||||||
/**
|
|
||||||
* Sensor description
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* Whether the sensor is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
|
||||||
* Entry point
|
|
||||||
*/
|
|
||||||
entrypoint: string;
|
|
||||||
/**
|
|
||||||
* Sensor ID
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Human-readable label
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Pack ID (optional)
|
|
||||||
*/
|
|
||||||
pack?: number | null;
|
|
||||||
/**
|
|
||||||
* Pack reference (optional)
|
|
||||||
*/
|
|
||||||
pack_ref?: string | null;
|
|
||||||
/**
|
|
||||||
* Parameter schema (StackStorm-style with inline required/secret)
|
|
||||||
*/
|
|
||||||
param_schema: any | null;
|
|
||||||
/**
|
|
||||||
* Unique reference identifier
|
|
||||||
*/
|
|
||||||
ref: string;
|
|
||||||
/**
|
|
||||||
* Runtime ID
|
|
||||||
*/
|
|
||||||
runtime: number;
|
|
||||||
/**
|
|
||||||
* Runtime reference
|
|
||||||
*/
|
|
||||||
runtime_ref: string;
|
|
||||||
/**
|
|
||||||
* Trigger ID
|
|
||||||
*/
|
|
||||||
trigger: number;
|
|
||||||
/**
|
|
||||||
* Trigger reference
|
|
||||||
*/
|
|
||||||
trigger_ref: string;
|
|
||||||
/**
|
|
||||||
* Last update timestamp
|
|
||||||
*/
|
|
||||||
updated: string;
|
|
||||||
};
|
|
||||||
/**
|
/**
|
||||||
* Optional message
|
* Sensor description
|
||||||
*/
|
*/
|
||||||
message?: string | null;
|
description: string | null;
|
||||||
|
/**
|
||||||
|
* Whether the sensor is enabled
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Entry point
|
||||||
|
*/
|
||||||
|
entrypoint: string;
|
||||||
|
/**
|
||||||
|
* Sensor ID
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Human-readable label
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* Pack ID (optional)
|
||||||
|
*/
|
||||||
|
pack?: number | null;
|
||||||
|
/**
|
||||||
|
* Pack reference (optional)
|
||||||
|
*/
|
||||||
|
pack_ref?: string | null;
|
||||||
|
/**
|
||||||
|
* Parameter schema (StackStorm-style with inline required/secret)
|
||||||
|
*/
|
||||||
|
param_schema: any | null;
|
||||||
|
/**
|
||||||
|
* Unique reference identifier
|
||||||
|
*/
|
||||||
|
ref: string;
|
||||||
|
/**
|
||||||
|
* Runtime ID
|
||||||
|
*/
|
||||||
|
runtime: number;
|
||||||
|
/**
|
||||||
|
* Runtime reference
|
||||||
|
*/
|
||||||
|
runtime_ref: string;
|
||||||
|
/**
|
||||||
|
* Trigger ID
|
||||||
|
*/
|
||||||
|
trigger: number;
|
||||||
|
/**
|
||||||
|
* Trigger reference
|
||||||
|
*/
|
||||||
|
trigger_ref: string;
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updated: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Optional message
|
||||||
|
*/
|
||||||
|
message?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,41 +6,40 @@
|
|||||||
* Request DTO for creating a new action
|
* Request DTO for creating a new action
|
||||||
*/
|
*/
|
||||||
export type CreateActionRequest = {
|
export type CreateActionRequest = {
|
||||||
/**
|
/**
|
||||||
* Action description
|
* Action description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description?: string | null;
|
||||||
/**
|
/**
|
||||||
* Entry point for action execution (e.g., path to script, function name)
|
* Entry point for action execution (e.g., path to script, function name)
|
||||||
*/
|
*/
|
||||||
entrypoint: string;
|
entrypoint: string;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Output schema (flat format) defining expected outputs with inline required/secret
|
* Output schema (flat format) defining expected outputs with inline required/secret
|
||||||
*/
|
*/
|
||||||
out_schema?: any | null;
|
out_schema?: any | null;
|
||||||
/**
|
/**
|
||||||
* Pack reference this action belongs to
|
* Pack reference this action belongs to
|
||||||
*/
|
*/
|
||||||
pack_ref: string;
|
pack_ref: string;
|
||||||
/**
|
/**
|
||||||
* Parameter schema (StackStorm-style) defining expected inputs with inline required/secret
|
* Parameter schema (StackStorm-style) defining expected inputs with inline required/secret
|
||||||
*/
|
*/
|
||||||
param_schema?: any | null;
|
param_schema?: any | null;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier (e.g., "core.http", "aws.ec2.start_instance")
|
* Unique reference identifier (e.g., "core.http", "aws.ec2.start_instance")
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Optional runtime ID for this action
|
* Optional runtime ID for this action
|
||||||
*/
|
*/
|
||||||
runtime?: number | null;
|
runtime?: number | null;
|
||||||
/**
|
/**
|
||||||
* Optional semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
* Optional semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
||||||
*/
|
*/
|
||||||
runtime_version_constraint?: string | null;
|
runtime_version_constraint?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type CreateIdentityRoleAssignmentRequest = {
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type CreatePermissionSetRoleAssignmentRequest = {
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -6,45 +6,44 @@
|
|||||||
* Request DTO for creating a new rule
|
* Request DTO for creating a new rule
|
||||||
*/
|
*/
|
||||||
export type CreateRuleRequest = {
|
export type CreateRuleRequest = {
|
||||||
/**
|
/**
|
||||||
* Parameters to pass to the action when rule is triggered
|
* Parameters to pass to the action when rule is triggered
|
||||||
*/
|
*/
|
||||||
action_params?: Record<string, any>;
|
action_params?: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
* Action reference to execute when rule matches
|
* Action reference to execute when rule matches
|
||||||
*/
|
*/
|
||||||
action_ref: string;
|
action_ref: string;
|
||||||
/**
|
/**
|
||||||
* Conditions for rule evaluation (JSON Logic or custom format)
|
* Conditions for rule evaluation (JSON Logic or custom format)
|
||||||
*/
|
*/
|
||||||
conditions?: Record<string, any>;
|
conditions?: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
* Rule description
|
* Rule description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description?: string | null;
|
||||||
/**
|
/**
|
||||||
* Whether the rule is enabled
|
* Whether the rule is enabled
|
||||||
*/
|
*/
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Pack reference this rule belongs to
|
* Pack reference this rule belongs to
|
||||||
*/
|
*/
|
||||||
pack_ref: string;
|
pack_ref: string;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier (e.g., "mypack.notify_on_error")
|
* Unique reference identifier (e.g., "mypack.notify_on_error")
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Parameters for trigger configuration and event filtering
|
* Parameters for trigger configuration and event filtering
|
||||||
*/
|
*/
|
||||||
trigger_params?: Record<string, any>;
|
trigger_params?: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
* Trigger reference that activates this rule
|
* Trigger reference that activates this rule
|
||||||
*/
|
*/
|
||||||
trigger_ref: string;
|
trigger_ref: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,45 +6,44 @@
|
|||||||
* Request DTO for creating a new sensor
|
* Request DTO for creating a new sensor
|
||||||
*/
|
*/
|
||||||
export type CreateSensorRequest = {
|
export type CreateSensorRequest = {
|
||||||
/**
|
/**
|
||||||
* Configuration values for this sensor instance (conforms to param_schema)
|
* Configuration values for this sensor instance (conforms to param_schema)
|
||||||
*/
|
*/
|
||||||
config?: any | null;
|
config?: any | null;
|
||||||
/**
|
/**
|
||||||
* Sensor description
|
* Sensor description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description?: string | null;
|
||||||
/**
|
/**
|
||||||
* Whether the sensor is enabled
|
* Whether the sensor is enabled
|
||||||
*/
|
*/
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
/**
|
/**
|
||||||
* Entry point for sensor execution (e.g., path to script, function name)
|
* Entry point for sensor execution (e.g., path to script, function name)
|
||||||
*/
|
*/
|
||||||
entrypoint: string;
|
entrypoint: string;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Pack reference this sensor belongs to
|
* Pack reference this sensor belongs to
|
||||||
*/
|
*/
|
||||||
pack_ref: string;
|
pack_ref: string;
|
||||||
/**
|
/**
|
||||||
* Parameter schema (flat format) for sensor configuration
|
* Parameter schema (flat format) for sensor configuration
|
||||||
*/
|
*/
|
||||||
param_schema?: any | null;
|
param_schema?: any | null;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier (e.g., "mypack.cpu_monitor")
|
* Unique reference identifier (e.g., "mypack.cpu_monitor")
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Runtime reference for this sensor
|
* Runtime reference for this sensor
|
||||||
*/
|
*/
|
||||||
runtime_ref: string;
|
runtime_ref: string;
|
||||||
/**
|
/**
|
||||||
* Trigger reference this sensor monitors for
|
* Trigger reference this sensor monitors for
|
||||||
*/
|
*/
|
||||||
trigger_ref: string;
|
trigger_ref: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
17
web/src/api/models/IdentityResponse.ts
Normal file
17
web/src/api/models/IdentityResponse.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { IdentityRoleAssignmentResponse } from './IdentityRoleAssignmentResponse';
|
||||||
|
import type { PermissionAssignmentResponse } from './PermissionAssignmentResponse';
|
||||||
|
import type { Value } from './Value';
|
||||||
|
export type IdentityResponse = {
|
||||||
|
attributes: Value;
|
||||||
|
direct_permissions: Array<PermissionAssignmentResponse>;
|
||||||
|
display_name?: string | null;
|
||||||
|
frozen: boolean;
|
||||||
|
id: number;
|
||||||
|
login: string;
|
||||||
|
roles: Array<IdentityRoleAssignmentResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
14
web/src/api/models/IdentityRoleAssignmentResponse.ts
Normal file
14
web/src/api/models/IdentityRoleAssignmentResponse.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type IdentityRoleAssignmentResponse = {
|
||||||
|
created: string;
|
||||||
|
id: number;
|
||||||
|
identity_id: number;
|
||||||
|
managed: boolean;
|
||||||
|
role: string;
|
||||||
|
source: string;
|
||||||
|
updated: string;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -6,7 +6,9 @@ import type { Value } from './Value';
|
|||||||
export type IdentitySummary = {
|
export type IdentitySummary = {
|
||||||
attributes: Value;
|
attributes: Value;
|
||||||
display_name?: string | null;
|
display_name?: string | null;
|
||||||
|
frozen: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
login: string;
|
login: string;
|
||||||
|
roles: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
18
web/src/api/models/LdapLoginRequest.ts
Normal file
18
web/src/api/models/LdapLoginRequest.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Request body for LDAP login.
|
||||||
|
*/
|
||||||
|
export type LdapLoginRequest = {
|
||||||
|
/**
|
||||||
|
* User login name (uid, sAMAccountName, etc.)
|
||||||
|
*/
|
||||||
|
login: string;
|
||||||
|
/**
|
||||||
|
* User password
|
||||||
|
*/
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
18
web/src/api/models/PackDescriptionPatch.ts
Normal file
18
web/src/api/models/PackDescriptionPatch.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type PackDescriptionPatch =
|
||||||
|
| {
|
||||||
|
op: PackDescriptionPatch.op;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
op: PackDescriptionPatch.op;
|
||||||
|
};
|
||||||
|
export namespace PackDescriptionPatch {
|
||||||
|
export enum op {
|
||||||
|
SET = "set",
|
||||||
|
CLEAR = "clear",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,63 +2,62 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import type { PaginationMeta } from './PaginationMeta';
|
import type { PaginationMeta } from "./PaginationMeta";
|
||||||
/**
|
/**
|
||||||
* Paginated response wrapper
|
* Paginated response wrapper
|
||||||
*/
|
*/
|
||||||
export type PaginatedResponse_ActionSummary = {
|
export type PaginatedResponse_ActionSummary = {
|
||||||
|
/**
|
||||||
|
* The data items
|
||||||
|
*/
|
||||||
|
data: Array<{
|
||||||
/**
|
/**
|
||||||
* The data items
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
data: Array<{
|
created: string;
|
||||||
/**
|
|
||||||
* Creation timestamp
|
|
||||||
*/
|
|
||||||
created: string;
|
|
||||||
/**
|
|
||||||
* Action description
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* Entry point
|
|
||||||
*/
|
|
||||||
entrypoint: string;
|
|
||||||
/**
|
|
||||||
* Action ID
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Human-readable label
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Pack reference
|
|
||||||
*/
|
|
||||||
pack_ref: string;
|
|
||||||
/**
|
|
||||||
* Unique reference identifier
|
|
||||||
*/
|
|
||||||
ref: string;
|
|
||||||
/**
|
|
||||||
* Runtime ID
|
|
||||||
*/
|
|
||||||
runtime?: number | null;
|
|
||||||
/**
|
|
||||||
* Semver version constraint for the runtime
|
|
||||||
*/
|
|
||||||
runtime_version_constraint?: string | null;
|
|
||||||
/**
|
|
||||||
* Last update timestamp
|
|
||||||
*/
|
|
||||||
updated: string;
|
|
||||||
/**
|
|
||||||
* Workflow definition ID (non-null if this action is a workflow)
|
|
||||||
*/
|
|
||||||
workflow_def?: number | null;
|
|
||||||
}>;
|
|
||||||
/**
|
/**
|
||||||
* Pagination metadata
|
* Action description
|
||||||
*/
|
*/
|
||||||
pagination: PaginationMeta;
|
description: string | null;
|
||||||
|
/**
|
||||||
|
* Entry point
|
||||||
|
*/
|
||||||
|
entrypoint: string;
|
||||||
|
/**
|
||||||
|
* Action ID
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Human-readable label
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* Pack reference
|
||||||
|
*/
|
||||||
|
pack_ref: string;
|
||||||
|
/**
|
||||||
|
* Unique reference identifier
|
||||||
|
*/
|
||||||
|
ref: string;
|
||||||
|
/**
|
||||||
|
* Runtime ID
|
||||||
|
*/
|
||||||
|
runtime?: number | null;
|
||||||
|
/**
|
||||||
|
* Semver version constraint for the runtime
|
||||||
|
*/
|
||||||
|
runtime_version_constraint?: string | null;
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updated: string;
|
||||||
|
/**
|
||||||
|
* Workflow definition ID (non-null if this action is a workflow)
|
||||||
|
*/
|
||||||
|
workflow_def?: number | null;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Pagination metadata
|
||||||
|
*/
|
||||||
|
pagination: PaginationMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ export type PaginatedResponse_IdentitySummary = {
|
|||||||
data: Array<{
|
data: Array<{
|
||||||
attributes: Value;
|
attributes: Value;
|
||||||
display_name?: string | null;
|
display_name?: string | null;
|
||||||
|
frozen: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
login: string;
|
login: string;
|
||||||
|
roles: Array<string>;
|
||||||
}>;
|
}>;
|
||||||
/**
|
/**
|
||||||
* Pagination metadata
|
* Pagination metadata
|
||||||
|
|||||||
@@ -2,67 +2,66 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import type { PaginationMeta } from './PaginationMeta';
|
import type { PaginationMeta } from "./PaginationMeta";
|
||||||
/**
|
/**
|
||||||
* Paginated response wrapper
|
* Paginated response wrapper
|
||||||
*/
|
*/
|
||||||
export type PaginatedResponse_RuleSummary = {
|
export type PaginatedResponse_RuleSummary = {
|
||||||
|
/**
|
||||||
|
* The data items
|
||||||
|
*/
|
||||||
|
data: Array<{
|
||||||
/**
|
/**
|
||||||
* The data items
|
* Parameters to pass to the action when rule is triggered
|
||||||
*/
|
*/
|
||||||
data: Array<{
|
action_params: Record<string, any>;
|
||||||
/**
|
|
||||||
* Parameters to pass to the action when rule is triggered
|
|
||||||
*/
|
|
||||||
action_params: Record<string, any>;
|
|
||||||
/**
|
|
||||||
* Action reference
|
|
||||||
*/
|
|
||||||
action_ref: string;
|
|
||||||
/**
|
|
||||||
* Creation timestamp
|
|
||||||
*/
|
|
||||||
created: string;
|
|
||||||
/**
|
|
||||||
* Rule description
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* Whether the rule is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
|
||||||
* Rule ID
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Human-readable label
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Pack reference
|
|
||||||
*/
|
|
||||||
pack_ref: string;
|
|
||||||
/**
|
|
||||||
* Unique reference identifier
|
|
||||||
*/
|
|
||||||
ref: string;
|
|
||||||
/**
|
|
||||||
* Parameters for trigger configuration and event filtering
|
|
||||||
*/
|
|
||||||
trigger_params: Record<string, any>;
|
|
||||||
/**
|
|
||||||
* Trigger reference
|
|
||||||
*/
|
|
||||||
trigger_ref: string;
|
|
||||||
/**
|
|
||||||
* Last update timestamp
|
|
||||||
*/
|
|
||||||
updated: string;
|
|
||||||
}>;
|
|
||||||
/**
|
/**
|
||||||
* Pagination metadata
|
* Action reference
|
||||||
*/
|
*/
|
||||||
pagination: PaginationMeta;
|
action_ref: string;
|
||||||
|
/**
|
||||||
|
* Creation timestamp
|
||||||
|
*/
|
||||||
|
created: string;
|
||||||
|
/**
|
||||||
|
* Rule description
|
||||||
|
*/
|
||||||
|
description: string | null;
|
||||||
|
/**
|
||||||
|
* Whether the rule is enabled
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Rule ID
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Human-readable label
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* Pack reference
|
||||||
|
*/
|
||||||
|
pack_ref: string;
|
||||||
|
/**
|
||||||
|
* Unique reference identifier
|
||||||
|
*/
|
||||||
|
ref: string;
|
||||||
|
/**
|
||||||
|
* Parameters for trigger configuration and event filtering
|
||||||
|
*/
|
||||||
|
trigger_params: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* Trigger reference
|
||||||
|
*/
|
||||||
|
trigger_ref: string;
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updated: string;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Pagination metadata
|
||||||
|
*/
|
||||||
|
pagination: PaginationMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,55 +2,54 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import type { PaginationMeta } from './PaginationMeta';
|
import type { PaginationMeta } from "./PaginationMeta";
|
||||||
/**
|
/**
|
||||||
* Paginated response wrapper
|
* Paginated response wrapper
|
||||||
*/
|
*/
|
||||||
export type PaginatedResponse_SensorSummary = {
|
export type PaginatedResponse_SensorSummary = {
|
||||||
|
/**
|
||||||
|
* The data items
|
||||||
|
*/
|
||||||
|
data: Array<{
|
||||||
/**
|
/**
|
||||||
* The data items
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
data: Array<{
|
created: string;
|
||||||
/**
|
|
||||||
* Creation timestamp
|
|
||||||
*/
|
|
||||||
created: string;
|
|
||||||
/**
|
|
||||||
* Sensor description
|
|
||||||
*/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* Whether the sensor is enabled
|
|
||||||
*/
|
|
||||||
enabled: boolean;
|
|
||||||
/**
|
|
||||||
* Sensor ID
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* Human-readable label
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Pack reference (optional)
|
|
||||||
*/
|
|
||||||
pack_ref?: string | null;
|
|
||||||
/**
|
|
||||||
* Unique reference identifier
|
|
||||||
*/
|
|
||||||
ref: string;
|
|
||||||
/**
|
|
||||||
* Trigger reference
|
|
||||||
*/
|
|
||||||
trigger_ref: string;
|
|
||||||
/**
|
|
||||||
* Last update timestamp
|
|
||||||
*/
|
|
||||||
updated: string;
|
|
||||||
}>;
|
|
||||||
/**
|
/**
|
||||||
* Pagination metadata
|
* Sensor description
|
||||||
*/
|
*/
|
||||||
pagination: PaginationMeta;
|
description: string | null;
|
||||||
|
/**
|
||||||
|
* Whether the sensor is enabled
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Sensor ID
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Human-readable label
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* Pack reference (optional)
|
||||||
|
*/
|
||||||
|
pack_ref?: string | null;
|
||||||
|
/**
|
||||||
|
* Unique reference identifier
|
||||||
|
*/
|
||||||
|
ref: string;
|
||||||
|
/**
|
||||||
|
* Trigger reference
|
||||||
|
*/
|
||||||
|
trigger_ref: string;
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updated: string;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Pagination metadata
|
||||||
|
*/
|
||||||
|
pagination: PaginationMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
12
web/src/api/models/PermissionSetRoleAssignmentResponse.ts
Normal file
12
web/src/api/models/PermissionSetRoleAssignmentResponse.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type PermissionSetRoleAssignmentResponse = {
|
||||||
|
created: string;
|
||||||
|
id: number;
|
||||||
|
permission_set_id: number;
|
||||||
|
permission_set_ref?: string | null;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import type { PermissionSetRoleAssignmentResponse } from './PermissionSetRoleAssignmentResponse';
|
||||||
import type { Value } from './Value';
|
import type { Value } from './Value';
|
||||||
export type PermissionSetSummary = {
|
export type PermissionSetSummary = {
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
@@ -10,5 +11,6 @@ export type PermissionSetSummary = {
|
|||||||
label?: string | null;
|
label?: string | null;
|
||||||
pack_ref?: string | null;
|
pack_ref?: string | null;
|
||||||
ref: string;
|
ref: string;
|
||||||
|
roles: Array<PermissionSetRoleAssignmentResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,73 +6,72 @@
|
|||||||
* Response DTO for rule information
|
* Response DTO for rule information
|
||||||
*/
|
*/
|
||||||
export type RuleResponse = {
|
export type RuleResponse = {
|
||||||
/**
|
/**
|
||||||
* Action ID (null if the referenced action has been deleted)
|
* Action ID (null if the referenced action has been deleted)
|
||||||
*/
|
*/
|
||||||
action?: number | null;
|
action?: number | null;
|
||||||
/**
|
/**
|
||||||
* Parameters to pass to the action when rule is triggered
|
* Parameters to pass to the action when rule is triggered
|
||||||
*/
|
*/
|
||||||
action_params: Record<string, any>;
|
action_params: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
* Action reference
|
* Action reference
|
||||||
*/
|
*/
|
||||||
action_ref: string;
|
action_ref: string;
|
||||||
/**
|
/**
|
||||||
* Conditions for rule evaluation
|
* Conditions for rule evaluation
|
||||||
*/
|
*/
|
||||||
conditions: Record<string, any>;
|
conditions: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
* Creation timestamp
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
created: string;
|
created: string;
|
||||||
/**
|
/**
|
||||||
* Rule description
|
* Rule description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string | null;
|
||||||
/**
|
/**
|
||||||
* Whether the rule is enabled
|
* Whether the rule is enabled
|
||||||
*/
|
*/
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Rule ID
|
* Rule ID
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Whether this is an ad-hoc rule (not from pack installation)
|
* Whether this is an ad-hoc rule (not from pack installation)
|
||||||
*/
|
*/
|
||||||
is_adhoc: boolean;
|
is_adhoc: boolean;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Pack ID
|
* Pack ID
|
||||||
*/
|
*/
|
||||||
pack: number;
|
pack: number;
|
||||||
/**
|
/**
|
||||||
* Pack reference
|
* Pack reference
|
||||||
*/
|
*/
|
||||||
pack_ref: string;
|
pack_ref: string;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier
|
* Unique reference identifier
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Trigger ID (null if the referenced trigger has been deleted)
|
* Trigger ID (null if the referenced trigger has been deleted)
|
||||||
*/
|
*/
|
||||||
trigger?: number | null;
|
trigger?: number | null;
|
||||||
/**
|
/**
|
||||||
* Parameters for trigger configuration and event filtering
|
* Parameters for trigger configuration and event filtering
|
||||||
*/
|
*/
|
||||||
trigger_params: Record<string, any>;
|
trigger_params: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
* Trigger reference
|
* Trigger reference
|
||||||
*/
|
*/
|
||||||
trigger_ref: string;
|
trigger_ref: string;
|
||||||
/**
|
/**
|
||||||
* Last update timestamp
|
* Last update timestamp
|
||||||
*/
|
*/
|
||||||
updated: string;
|
updated: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,53 +6,52 @@
|
|||||||
* Simplified rule response (for list endpoints)
|
* Simplified rule response (for list endpoints)
|
||||||
*/
|
*/
|
||||||
export type RuleSummary = {
|
export type RuleSummary = {
|
||||||
/**
|
/**
|
||||||
* Parameters to pass to the action when rule is triggered
|
* Parameters to pass to the action when rule is triggered
|
||||||
*/
|
*/
|
||||||
action_params: Record<string, any>;
|
action_params: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
* Action reference
|
* Action reference
|
||||||
*/
|
*/
|
||||||
action_ref: string;
|
action_ref: string;
|
||||||
/**
|
/**
|
||||||
* Creation timestamp
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
created: string;
|
created: string;
|
||||||
/**
|
/**
|
||||||
* Rule description
|
* Rule description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string | null;
|
||||||
/**
|
/**
|
||||||
* Whether the rule is enabled
|
* Whether the rule is enabled
|
||||||
*/
|
*/
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Rule ID
|
* Rule ID
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Pack reference
|
* Pack reference
|
||||||
*/
|
*/
|
||||||
pack_ref: string;
|
pack_ref: string;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier
|
* Unique reference identifier
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Parameters for trigger configuration and event filtering
|
* Parameters for trigger configuration and event filtering
|
||||||
*/
|
*/
|
||||||
trigger_params: Record<string, any>;
|
trigger_params: Record<string, any>;
|
||||||
/**
|
/**
|
||||||
* Trigger reference
|
* Trigger reference
|
||||||
*/
|
*/
|
||||||
trigger_ref: string;
|
trigger_ref: string;
|
||||||
/**
|
/**
|
||||||
* Last update timestamp
|
* Last update timestamp
|
||||||
*/
|
*/
|
||||||
updated: string;
|
updated: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
19
web/src/api/models/RuntimeVersionConstraintPatch.ts
Normal file
19
web/src/api/models/RuntimeVersionConstraintPatch.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Explicit patch operation for a nullable runtime version constraint.
|
||||||
|
*/
|
||||||
|
export type RuntimeVersionConstraintPatch = ({
|
||||||
|
op: RuntimeVersionConstraintPatch.op;
|
||||||
|
value: string;
|
||||||
|
} | {
|
||||||
|
op: RuntimeVersionConstraintPatch.op;
|
||||||
|
});
|
||||||
|
export namespace RuntimeVersionConstraintPatch {
|
||||||
|
export enum op {
|
||||||
|
SET = 'set',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,65 +6,64 @@
|
|||||||
* Response DTO for sensor information
|
* Response DTO for sensor information
|
||||||
*/
|
*/
|
||||||
export type SensorResponse = {
|
export type SensorResponse = {
|
||||||
/**
|
/**
|
||||||
* Creation timestamp
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
created: string;
|
created: string;
|
||||||
/**
|
/**
|
||||||
* Sensor description
|
* Sensor description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string | null;
|
||||||
/**
|
/**
|
||||||
* Whether the sensor is enabled
|
* Whether the sensor is enabled
|
||||||
*/
|
*/
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Entry point
|
* Entry point
|
||||||
*/
|
*/
|
||||||
entrypoint: string;
|
entrypoint: string;
|
||||||
/**
|
/**
|
||||||
* Sensor ID
|
* Sensor ID
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Pack ID (optional)
|
* Pack ID (optional)
|
||||||
*/
|
*/
|
||||||
pack?: number | null;
|
pack?: number | null;
|
||||||
/**
|
/**
|
||||||
* Pack reference (optional)
|
* Pack reference (optional)
|
||||||
*/
|
*/
|
||||||
pack_ref?: string | null;
|
pack_ref?: string | null;
|
||||||
/**
|
/**
|
||||||
* Parameter schema (StackStorm-style with inline required/secret)
|
* Parameter schema (StackStorm-style with inline required/secret)
|
||||||
*/
|
*/
|
||||||
param_schema: any | null;
|
param_schema: any | null;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier
|
* Unique reference identifier
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Runtime ID
|
* Runtime ID
|
||||||
*/
|
*/
|
||||||
runtime: number;
|
runtime: number;
|
||||||
/**
|
/**
|
||||||
* Runtime reference
|
* Runtime reference
|
||||||
*/
|
*/
|
||||||
runtime_ref: string;
|
runtime_ref: string;
|
||||||
/**
|
/**
|
||||||
* Trigger ID
|
* Trigger ID
|
||||||
*/
|
*/
|
||||||
trigger: number;
|
trigger: number;
|
||||||
/**
|
/**
|
||||||
* Trigger reference
|
* Trigger reference
|
||||||
*/
|
*/
|
||||||
trigger_ref: string;
|
trigger_ref: string;
|
||||||
/**
|
/**
|
||||||
* Last update timestamp
|
* Last update timestamp
|
||||||
*/
|
*/
|
||||||
updated: string;
|
updated: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,41 +6,40 @@
|
|||||||
* Simplified sensor response (for list endpoints)
|
* Simplified sensor response (for list endpoints)
|
||||||
*/
|
*/
|
||||||
export type SensorSummary = {
|
export type SensorSummary = {
|
||||||
/**
|
/**
|
||||||
* Creation timestamp
|
* Creation timestamp
|
||||||
*/
|
*/
|
||||||
created: string;
|
created: string;
|
||||||
/**
|
/**
|
||||||
* Sensor description
|
* Sensor description
|
||||||
*/
|
*/
|
||||||
description: string;
|
description: string | null;
|
||||||
/**
|
/**
|
||||||
* Whether the sensor is enabled
|
* Whether the sensor is enabled
|
||||||
*/
|
*/
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Sensor ID
|
* Sensor ID
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
/**
|
/**
|
||||||
* Human-readable label
|
* Human-readable label
|
||||||
*/
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
/**
|
/**
|
||||||
* Pack reference (optional)
|
* Pack reference (optional)
|
||||||
*/
|
*/
|
||||||
pack_ref?: string | null;
|
pack_ref?: string | null;
|
||||||
/**
|
/**
|
||||||
* Unique reference identifier
|
* Unique reference identifier
|
||||||
*/
|
*/
|
||||||
ref: string;
|
ref: string;
|
||||||
/**
|
/**
|
||||||
* Trigger reference
|
* Trigger reference
|
||||||
*/
|
*/
|
||||||
trigger_ref: string;
|
trigger_ref: string;
|
||||||
/**
|
/**
|
||||||
* Last update timestamp
|
* Last update timestamp
|
||||||
*/
|
*/
|
||||||
updated: string;
|
updated: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
18
web/src/api/models/TriggerStringPatch.ts
Normal file
18
web/src/api/models/TriggerStringPatch.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type TriggerStringPatch =
|
||||||
|
| {
|
||||||
|
op: TriggerStringPatch.op;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
op: TriggerStringPatch.op;
|
||||||
|
};
|
||||||
|
export namespace TriggerStringPatch {
|
||||||
|
export enum op {
|
||||||
|
SET = "set",
|
||||||
|
CLEAR = "clear",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import type { RuntimeVersionConstraintPatch } from './RuntimeVersionConstraintPatch';
|
||||||
/**
|
/**
|
||||||
* Request DTO for updating an action
|
* Request DTO for updating an action
|
||||||
*/
|
*/
|
||||||
@@ -30,9 +31,6 @@ export type UpdateActionRequest = {
|
|||||||
* Runtime ID
|
* Runtime ID
|
||||||
*/
|
*/
|
||||||
runtime?: number | null;
|
runtime?: number | null;
|
||||||
/**
|
runtime_version_constraint?: (null | RuntimeVersionConstraintPatch);
|
||||||
* Optional semver version constraint for the runtime (e.g., ">=3.12", ">=3.12,<4.0", "~18.0")
|
|
||||||
*/
|
|
||||||
runtime_version_constraint?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { Value } from './Value';
|
|||||||
export type UpdateIdentityRequest = {
|
export type UpdateIdentityRequest = {
|
||||||
attributes?: (null | Value);
|
attributes?: (null | Value);
|
||||||
display_name?: string | null;
|
display_name?: string | null;
|
||||||
|
frozen?: boolean | null;
|
||||||
password?: string | null;
|
password?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import type { PackDescriptionPatch } from './PackDescriptionPatch';
|
||||||
/**
|
/**
|
||||||
* Request DTO for updating a pack
|
* Request DTO for updating a pack
|
||||||
*/
|
*/
|
||||||
@@ -18,10 +19,7 @@ export type UpdatePackRequest = {
|
|||||||
* Pack dependencies (refs of required packs)
|
* Pack dependencies (refs of required packs)
|
||||||
*/
|
*/
|
||||||
dependencies?: any[] | null;
|
dependencies?: any[] | null;
|
||||||
/**
|
description?: (null | PackDescriptionPatch);
|
||||||
* Pack description
|
|
||||||
*/
|
|
||||||
description?: string | null;
|
|
||||||
/**
|
/**
|
||||||
* Whether this is a standard pack
|
* Whether this is a standard pack
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
import type { TriggerStringPatch } from './TriggerStringPatch';
|
||||||
/**
|
/**
|
||||||
* Request DTO for updating a trigger
|
* Request DTO for updating a trigger
|
||||||
*/
|
*/
|
||||||
export type UpdateTriggerRequest = {
|
export type UpdateTriggerRequest = {
|
||||||
/**
|
description?: (null | TriggerStringPatch);
|
||||||
* Trigger description
|
|
||||||
*/
|
|
||||||
description?: string | null;
|
|
||||||
/**
|
/**
|
||||||
* Whether the trigger is enabled
|
* Whether the trigger is enabled
|
||||||
*/
|
*/
|
||||||
|
|||||||
61
web/src/api/services/AgentService.ts
Normal file
61
web/src/api/services/AgentService.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
import type { AgentBinaryInfo } from '../models/AgentBinaryInfo';
|
||||||
|
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||||
|
import { OpenAPI } from '../core/OpenAPI';
|
||||||
|
import { request as __request } from '../core/request';
|
||||||
|
export class AgentService {
|
||||||
|
/**
|
||||||
|
* Download the agent binary
|
||||||
|
* Returns the statically-linked attune-agent binary for the requested architecture.
|
||||||
|
* The binary can be injected into any container to turn it into an Attune worker.
|
||||||
|
* @returns any Agent binary
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static downloadAgentBinary({
|
||||||
|
arch,
|
||||||
|
token,
|
||||||
|
}: {
|
||||||
|
/**
|
||||||
|
* Target architecture (x86_64, aarch64). Defaults to x86_64.
|
||||||
|
*/
|
||||||
|
arch?: string | null,
|
||||||
|
/**
|
||||||
|
* Optional bootstrap token for authentication
|
||||||
|
*/
|
||||||
|
token?: string | null,
|
||||||
|
}): CancelablePromise<any> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/agent/binary',
|
||||||
|
query: {
|
||||||
|
'arch': arch,
|
||||||
|
'token': token,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
400: `Invalid architecture`,
|
||||||
|
401: `Invalid or missing bootstrap token`,
|
||||||
|
404: `Agent binary not found`,
|
||||||
|
503: `Agent binary distribution not configured`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get agent binary metadata
|
||||||
|
* Returns information about available agent binaries, including
|
||||||
|
* supported architectures and binary sizes.
|
||||||
|
* @returns AgentBinaryInfo Agent binary info
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static agentInfo(): CancelablePromise<AgentBinaryInfo> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/agent/info',
|
||||||
|
errors: {
|
||||||
|
503: `Agent binary distribution not configured`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import type { ChangePasswordRequest } from '../models/ChangePasswordRequest';
|
import type { ChangePasswordRequest } from '../models/ChangePasswordRequest';
|
||||||
|
import type { LdapLoginRequest } from '../models/LdapLoginRequest';
|
||||||
import type { LoginRequest } from '../models/LoginRequest';
|
import type { LoginRequest } from '../models/LoginRequest';
|
||||||
import type { RefreshTokenRequest } from '../models/RefreshTokenRequest';
|
import type { RefreshTokenRequest } from '../models/RefreshTokenRequest';
|
||||||
import type { RegisterRequest } from '../models/RegisterRequest';
|
import type { RegisterRequest } from '../models/RegisterRequest';
|
||||||
@@ -52,6 +53,55 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Authenticate via LDAP directory.
|
||||||
|
* POST /auth/ldap/login
|
||||||
|
* @returns any Successfully authenticated via LDAP
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static ldapLogin({
|
||||||
|
requestBody,
|
||||||
|
}: {
|
||||||
|
requestBody: LdapLoginRequest,
|
||||||
|
}): CancelablePromise<{
|
||||||
|
/**
|
||||||
|
* Token response
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* Access token (JWT)
|
||||||
|
*/
|
||||||
|
access_token: string;
|
||||||
|
/**
|
||||||
|
* Access token expiration in seconds
|
||||||
|
*/
|
||||||
|
expires_in: number;
|
||||||
|
/**
|
||||||
|
* Refresh token
|
||||||
|
*/
|
||||||
|
refresh_token: string;
|
||||||
|
/**
|
||||||
|
* Token type (always "Bearer")
|
||||||
|
*/
|
||||||
|
token_type: string;
|
||||||
|
user?: (null | UserInfo);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Optional message
|
||||||
|
*/
|
||||||
|
message?: string | null;
|
||||||
|
}> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/auth/ldap/login',
|
||||||
|
body: requestBody,
|
||||||
|
mediaType: 'application/json',
|
||||||
|
errors: {
|
||||||
|
401: `Invalid LDAP credentials`,
|
||||||
|
501: `LDAP not configured`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Login endpoint
|
* Login endpoint
|
||||||
* POST /auth/login
|
* POST /auth/login
|
||||||
@@ -237,4 +287,82 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Authentication settings endpoint
|
||||||
|
* GET /auth/settings
|
||||||
|
* @returns any Authentication settings
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public static authSettings(): CancelablePromise<{
|
||||||
|
/**
|
||||||
|
* Public authentication settings for the login page.
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* Whether authentication is enabled for the server.
|
||||||
|
*/
|
||||||
|
authentication_enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Whether LDAP login is configured and enabled.
|
||||||
|
*/
|
||||||
|
ldap_enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Optional icon URL shown beside the provider label.
|
||||||
|
*/
|
||||||
|
ldap_provider_icon_url?: string | null;
|
||||||
|
/**
|
||||||
|
* User-facing provider label for the login button.
|
||||||
|
*/
|
||||||
|
ldap_provider_label?: string | null;
|
||||||
|
/**
|
||||||
|
* Provider name for `?auth=<provider>`.
|
||||||
|
*/
|
||||||
|
ldap_provider_name?: string | null;
|
||||||
|
/**
|
||||||
|
* Whether LDAP login should be shown by default.
|
||||||
|
*/
|
||||||
|
ldap_visible_by_default: boolean;
|
||||||
|
/**
|
||||||
|
* Whether local username/password login is configured.
|
||||||
|
*/
|
||||||
|
local_password_enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Whether local username/password login should be shown by default.
|
||||||
|
*/
|
||||||
|
local_password_visible_by_default: boolean;
|
||||||
|
/**
|
||||||
|
* Whether OIDC login is configured and enabled.
|
||||||
|
*/
|
||||||
|
oidc_enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Optional icon URL shown beside the provider label.
|
||||||
|
*/
|
||||||
|
oidc_provider_icon_url?: string | null;
|
||||||
|
/**
|
||||||
|
* User-facing provider label for the login button.
|
||||||
|
*/
|
||||||
|
oidc_provider_label?: string | null;
|
||||||
|
/**
|
||||||
|
* Provider name for `?auth=<provider>`.
|
||||||
|
*/
|
||||||
|
oidc_provider_name?: string | null;
|
||||||
|
/**
|
||||||
|
* Whether OIDC login should be shown by default.
|
||||||
|
*/
|
||||||
|
oidc_visible_by_default: boolean;
|
||||||
|
/**
|
||||||
|
* Whether unauthenticated self-service registration is allowed.
|
||||||
|
*/
|
||||||
|
self_registration_enabled: boolean;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Optional message
|
||||||
|
*/
|
||||||
|
message?: string | null;
|
||||||
|
}> {
|
||||||
|
return __request(OpenAPI, {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/auth/settings',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user