mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Merge branch 'main' into feat-byod-enrollment
This commit is contained in:
commit
910b5a7b2b
102 changed files with 1669 additions and 1057 deletions
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install Go Dependencies
|
||||
run: make deps-go
|
||||
|
|
|
|||
8
.github/workflows/build-binaries.yaml
vendored
8
.github/workflows/build-binaries.yaml
vendored
|
|
@ -29,10 +29,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# Set the Node.js version
|
||||
- name: Set up Node.js ${{ vars.NODE_VERSION }}
|
||||
|
|
@ -40,9 +43,6 @@ jobs:
|
|||
with:
|
||||
node-version: ${{ vars.NODE_VERSION }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: JS Dependency Cache
|
||||
id: js-cache
|
||||
uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v2
|
||||
|
|
|
|||
2
.github/workflows/build-orbit.yaml
vendored
2
.github/workflows/build-orbit.yaml
vendored
|
|
@ -59,7 +59,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build, codesign and notarize orbit
|
||||
run: go run ./orbit/tools/build/build.go
|
||||
|
|
|
|||
9
.github/workflows/check-automated-doc.yml
vendored
9
.github/workflows/check-automated-doc.yml
vendored
|
|
@ -36,15 +36,16 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Verify golang generated documentation is up-to-date
|
||||
run: |
|
||||
make generate-doc
|
||||
|
|
|
|||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
|
|
|||
2
.github/workflows/deploy-fleet-website.yml
vendored
2
.github/workflows/deploy-fleet-website.yml
vendored
|
|
@ -64,7 +64,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# Download top-level dependencies and build Storybook in the website's assets/ folder
|
||||
- run: npm install --legacy-peer-deps && npm run build-storybook -- -o ./website/assets/storybook --loglevel verbose
|
||||
|
|
|
|||
5
.github/workflows/dogfood-deploy.yml
vendored
5
.github/workflows/dogfood-deploy.yml
vendored
|
|
@ -51,14 +51,17 @@ jobs:
|
|||
- id: fail-on-main
|
||||
run: "false"
|
||||
if: ${{ github.ref == 'main' }}
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@67fbcbb121271f7775d2e7715933280b06314838 # v1.7.0
|
||||
with:
|
||||
role-to-assume: ${{env.AWS_IAM_ROLE}}
|
||||
aws-region: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3
|
||||
with:
|
||||
terraform_version: 1.6.3
|
||||
|
|
|
|||
42
.github/workflows/fleet-and-orbit.yml
vendored
42
.github/workflows/fleet-and-orbit.yml
vendored
|
|
@ -62,7 +62,6 @@ jobs:
|
|||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ["${{ vars.GO_VERSION }}"]
|
||||
mysql: ["mysql:8.0.36"]
|
||||
runs-on: ubuntu-latest
|
||||
needs: gen
|
||||
|
|
@ -72,10 +71,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# Set the Node.js version
|
||||
- name: Set up Node.js ${{ vars.NODE_VERSION }}
|
||||
|
|
@ -83,9 +85,6 @@ jobs:
|
|||
with:
|
||||
node-version: ${{ vars.NODE_VERSION }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Start tunnel
|
||||
env:
|
||||
CERT_PEM: ${{ secrets.CLOUDFLARE_TUNNEL_FLEETUEM_CERT_B64 }}
|
||||
|
|
@ -175,9 +174,6 @@ jobs:
|
|||
# This job also makes sure the Fleet server is up and running.
|
||||
set-enroll-secret:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ["${{ vars.GO_VERSION }}"]
|
||||
runs-on: ubuntu-latest
|
||||
needs: gen
|
||||
steps:
|
||||
|
|
@ -186,13 +182,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build Fleetctl
|
||||
run: make fleetctl
|
||||
|
|
@ -218,9 +214,6 @@ jobs:
|
|||
# Here we generate the Fleet Desktop and osqueryd targets for
|
||||
# macOS which can only be generated from a macOS host.
|
||||
build-macos-targets:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ["${{ vars.GO_VERSION }}"]
|
||||
# Set macOS version to '12' (current equivalent to macos-latest) for
|
||||
# building the binary. This ensures compatibility with macOS version 13 and
|
||||
# later, avoiding runtime errors on systems using macOS 13 or newer.
|
||||
|
|
@ -234,13 +227,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build desktop.app.tar.gz and osqueryd.app.tar.gz
|
||||
run: |
|
||||
|
|
@ -269,9 +262,6 @@ jobs:
|
|||
# installed, and installing it is time consuming and unreliable.
|
||||
run-tuf-and-gen-pkgs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ["${{ vars.GO_VERSION }}"]
|
||||
runs-on: ubuntu-latest
|
||||
needs: [gen, build-macos-targets]
|
||||
steps:
|
||||
|
|
@ -280,13 +270,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Download macos pre-built apps
|
||||
id: download
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ jobs:
|
|||
# - Unattended installation of Docker on macOS fails. (see
|
||||
# https://github.com/docker/for-mac/issues/6450)
|
||||
os: [ubuntu-latest]
|
||||
go-version: ['${{ vars.GO_VERSION }}']
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
|
|
@ -62,13 +61,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build Fleetctl
|
||||
run: make fleetctl
|
||||
|
|
|
|||
10
.github/workflows/fleetd-tuf.yml
vendored
10
.github/workflows/fleetd-tuf.yml
vendored
|
|
@ -30,16 +30,16 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Update orbit/TUF.md
|
||||
run: |
|
||||
make fleetd-tuf
|
||||
|
|
|
|||
32
.github/workflows/generate-desktop-targets.yml
vendored
32
.github/workflows/generate-desktop-targets.yml
vendored
|
|
@ -45,13 +45,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Import signing keys
|
||||
env:
|
||||
|
|
@ -98,13 +98,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Generate fleet-desktop.exe
|
||||
run: |
|
||||
|
|
@ -139,13 +139,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Generate desktop.tar.gz
|
||||
run: |
|
||||
|
|
@ -167,13 +167,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Generate desktop.tar.gz
|
||||
run: |
|
||||
|
|
|
|||
3
.github/workflows/golangci-lint.yml
vendored
3
.github/workflows/golangci-lint.yml
vendored
|
|
@ -38,7 +38,6 @@ jobs:
|
|||
matrix:
|
||||
# See #9943, we just need to add windows-latest here once all issues are fixed.
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
go-version: ['${{ vars.GO_VERSION }}']
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
|
|
@ -52,7 +51,7 @@ jobs:
|
|||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install dependencies (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
|
|
|
|||
2
.github/workflows/goreleaser-fleet.yaml
vendored
2
.github/workflows/goreleaser-fleet.yaml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# Set the Node.js version
|
||||
- name: Set up Node.js ${{ vars.NODE_VERSION }}
|
||||
|
|
|
|||
8
.github/workflows/goreleaser-orbit.yaml
vendored
8
.github/workflows/goreleaser-orbit.yaml
vendored
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run GoReleaser
|
||||
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-macos.yml # v1.20.0
|
||||
|
|
@ -95,7 +95,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run GoReleaser
|
||||
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux.yml # v1.20.0
|
||||
|
|
@ -128,7 +128,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run GoReleaser
|
||||
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-linux-arm64.yml # v1.20.0
|
||||
|
|
@ -161,7 +161,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run GoReleaser
|
||||
run: go run github.com/goreleaser/goreleaser@56c9d09a1b925e2549631c6d180b0a1c2ebfac82 release --debug --rm-dist --skip-publish -f orbit/goreleaser-windows.yml # v1.20.0
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# Set the Node.js version
|
||||
- name: Set up Node.js ${{ vars.NODE_VERSION }}
|
||||
|
|
|
|||
8
.github/workflows/integration.yml
vendored
8
.github/workflows/integration.yml
vendored
|
|
@ -264,13 +264,13 @@ jobs:
|
|||
npm install -g fleetctl
|
||||
fleetctl config set --address ${{ needs.gen.outputs.address }} --token ${{ needs.login.outputs.token }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build Fleetctl
|
||||
run: make fleetctl
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
|
|
|
|||
10
.github/workflows/release-fleetd-base.yml
vendored
10
.github/workflows/release-fleetd-base.yml
vendored
|
|
@ -51,16 +51,16 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Check for fleetd component updates
|
||||
id: check-for-fleetd-component-updates
|
||||
run: |
|
||||
|
|
|
|||
9
.github/workflows/test-db-changes.yml
vendored
9
.github/workflows/test-db-changes.yml
vendored
|
|
@ -35,15 +35,16 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ vars.GO_VERSION }}
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Start Infra Dependencies
|
||||
# Use & to background this
|
||||
run: docker compose up -d mysql_test &
|
||||
|
|
|
|||
3
.github/workflows/test-fleetd-chrome.yml
vendored
3
.github/workflows/test-fleetd-chrome.yml
vendored
|
|
@ -66,7 +66,8 @@ jobs:
|
|||
npm test
|
||||
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1
|
||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: ./ee/fleetd-chrome/coverage
|
||||
flags: fleetd-chrome
|
||||
|
|
|
|||
45
.github/workflows/test-go.yaml
vendored
45
.github/workflows/test-go.yaml
vendored
|
|
@ -44,7 +44,6 @@ jobs:
|
|||
matrix:
|
||||
suite: ["integration", "core"]
|
||||
os: [ubuntu-latest]
|
||||
go-version: ['${{ vars.GO_VERSION }}']
|
||||
mysql: ["mysql:8.0.36", "mysql:8.4.2"]
|
||||
continue-on-error: ${{ matrix.suite == 'integration' }} # Since integration tests have a higher chance of failing, often for unrelated reasons, we don't want to fail the whole job if they fail
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
|
@ -65,7 +64,7 @@ jobs:
|
|||
- name: Install Go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
# Pre-starting dependencies here means they are ready to go when we need them.
|
||||
- name: Start Infra Dependencies
|
||||
|
|
@ -131,13 +130,17 @@ jobs:
|
|||
NETWORK_TEST_GITHUB_TOKEN=${{ secrets.FLEET_RELEASE_GITHUB_PAT }} \
|
||||
make test-go 2>&1 | tee /tmp/gotest.log
|
||||
|
||||
# note: it's fine to upload multiple reports (one per matrix combination)
|
||||
# for the same run, see https://docs.codecov.com/docs/merging-reports
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70
|
||||
- name: Create mysql identifier without colon
|
||||
if: always()
|
||||
run: |
|
||||
echo "MATRIX_MYSQL_ID=$(echo ${{ matrix.mysql }} | tr -d ':')" >> $GITHUB_ENV
|
||||
|
||||
- name: Save coverage
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
files: coverage.txt
|
||||
flags: backend
|
||||
name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-coverage
|
||||
path: ./coverage.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Generate summary of errors
|
||||
if: failure()
|
||||
|
|
@ -167,14 +170,9 @@ jobs:
|
|||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_G_HELP_ENGINEERING_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: Create mysql identifier without colon
|
||||
if: always()
|
||||
run: |
|
||||
echo "MATRIX_MYSQL_ID=$(echo ${{ matrix.mysql }} | tr -d ':')" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload test log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-test-log
|
||||
path: /tmp/gotest.log
|
||||
|
|
@ -182,7 +180,24 @@ jobs:
|
|||
|
||||
- name: Upload summary test log
|
||||
if: always()
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
with:
|
||||
name: ${{ matrix.suite }}-${{ env.MATRIX_MYSQL_ID }}-summary-test-log
|
||||
path: /tmp/summary.txt
|
||||
|
||||
# We upload all backend coverage in one step so that we're less like to end up in a situation with a partial coverage report.
|
||||
upload-coverage:
|
||||
needs: [test-go]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6
|
||||
with:
|
||||
pattern: '*-coverage'
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: backend
|
||||
|
|
|
|||
3
.github/workflows/test-js.yml
vendored
3
.github/workflows/test-js.yml
vendored
|
|
@ -69,8 +69,9 @@ jobs:
|
|||
yarn test:ci
|
||||
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70
|
||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: frontend
|
||||
|
||||
lint-js:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go-version: ['${{ vars.GO_VERSION }}']
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
|
|
@ -50,13 +49,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install Go Dependencies
|
||||
run: make deps-go
|
||||
|
|
|
|||
9
.github/workflows/test-packaging.yml
vendored
9
.github/workflows/test-packaging.yml
vendored
|
|
@ -47,7 +47,6 @@ jobs:
|
|||
# `macos-latest` uses arm64 by default now, so please be careful when
|
||||
# updating this version.
|
||||
os: [ubuntu-latest, macos-13]
|
||||
go-version: ['${{ vars.GO_VERSION }}']
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
|
|
@ -83,13 +82,13 @@ jobs:
|
|||
brew install colima
|
||||
colima start --mount $TMPDIR:w
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install wine and wix
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
|
|
|
|||
9
.github/workflows/test-yml-specs.yml
vendored
9
.github/workflows/test-yml-specs.yml
vendored
|
|
@ -33,7 +33,6 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go-version: ['${{ vars.GO_VERSION }}']
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
|
|
@ -42,13 +41,13 @@ jobs:
|
|||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Run apply spec tests
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
# MDM migration
|
||||
|
||||
This section provides instructions for migrating your hosts away from your old MDM solution to Fleet.
|
||||
This guide provides instructions for migrating devices from your current MDM solution to Fleet.
|
||||
|
||||
> For seamless MDM migration, [view this guide](https://fleetdm.com/guides/seamless-mdm-migration).
|
||||
|
||||
## Requirements
|
||||
|
||||
1. A [deployed Fleet instance](https://fleetdm.com/docs/deploy/deploy-fleet)
|
||||
2. [Fleet connected to Apple](https://fleetdm.com/guides/macos-mdm-setup)
|
||||
|
||||
- A [deployed Fleet instance](https://fleetdm.com/docs/deploy/deploy-fleet)
|
||||
- Fleet is connected to Apple Push Notification service (APNs) and Apple Business Manager (ABM). [See macOS MDM setup](https://fleetdm.com/guides/macos-mdm-setup)
|
||||
|
||||
|
||||
## Migrate manually enrolled hosts
|
||||
|
||||
|
|
|
|||
1
changes/19551-policy-software-automations
Normal file
1
changes/19551-policy-software-automations
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Implement features allowing automatic installation of software on hosts that fail policies.
|
||||
1
changes/21315-vpp-premium-license
Normal file
1
changes/21315-vpp-premium-license
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Verify user has premium license before uploading VPP tokens
|
||||
1
changes/21757-fix-scheduling-cron-jobs-at-startup
Normal file
1
changes/21757-fix-scheduling-cron-jobs-at-startup
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fixed an issue with the scheduling of cron jobs at startup if the job has never run, which caused it to be delayed.
|
||||
1
changes/apns-errors
Normal file
1
changes/apns-errors
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fixed logic to properly catch and log APNs errors.
|
||||
|
|
@ -633,6 +633,12 @@ the way that the Fleet server works.
|
|||
appCfg.MDM.AppleBMEnabledAndConfigured = count > 0
|
||||
}
|
||||
}
|
||||
if appCfg.MDM.EnabledAndConfigured {
|
||||
level.Info(logger).Log("msg", "Apple MDM enabled")
|
||||
}
|
||||
if appCfg.MDM.AppleBMEnabledAndConfigured {
|
||||
level.Info(logger).Log("msg", "Apple Business Manager enabled")
|
||||
}
|
||||
|
||||
// register the Microsoft MDM services
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -1090,7 +1090,7 @@ Modifies the Fleet's configuration with the supplied information.
|
|||
| integrations | object | body | Includes `jira`, `zendesk`, and `google_calendar` arrays. See [integrations](#integrations) for details. |
|
||||
| mdm | object | body | See [mdm](#mdm). |
|
||||
| features | object | body | See [features](#features). |
|
||||
| scripts | list | body | A list of script files to add so they can be executed at a later time. |
|
||||
| scripts | array | body | A list of script files to add so they can be executed at a later time. |
|
||||
| force | boolean | query | Whether to force-apply the agent options even if there are validation errors. |
|
||||
| dry_run | boolean | query | Whether to validate the configuration and return any validation errors **without** applying changes. |
|
||||
|
||||
|
|
@ -1518,10 +1518,10 @@ _Available in Fleet Premium._
|
|||
|
||||
| Name | Type | Description |
|
||||
| --------------------- | ----- | ---------------------------------------------------------------------------------------------- |
|
||||
| host_status_webhook | list | See [`webhook_settings.host_status_webhook`](#webhook-settings-host-status-webhook). |
|
||||
| failing_policies_webhook | list | See [`webhook_settings.failing_policies_webhook`](#webhook-settings-failing-policies-webhook). |
|
||||
| vulnerabilities_webhook | list | See [`webhook_settings.vulnerabilities_webhook`](#webhook-settings-vulnerabilities-webhook). |
|
||||
| activities_webhook | list | See [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook). |
|
||||
| host_status_webhook | array | See [`webhook_settings.host_status_webhook`](#webhook-settings-host-status-webhook). |
|
||||
| failing_policies_webhook | array | See [`webhook_settings.failing_policies_webhook`](#webhook-settings-failing-policies-webhook). |
|
||||
| vulnerabilities_webhook | array | See [`webhook_settings.vulnerabilities_webhook`](#webhook-settings-vulnerabilities-webhook). |
|
||||
| activities_webhook | array | See [`webhook_settings.activities_webhook`](#webhook-settings-activities-webhook). |
|
||||
|
||||
<br/>
|
||||
|
||||
|
|
@ -1614,9 +1614,9 @@ _Available in Fleet Premium._
|
|||
|
||||
| Name | Type | Description |
|
||||
| --------------------- | ----- | -------------------------------------------------------------------- |
|
||||
| jira | list | See [`integrations.jira`](#integrations-jira). |
|
||||
| zendesk | list | See [`integrations.zendesk`](#integrations-zendesk). |
|
||||
| google_calendar | list | See [`integrations.google_calendar`](#integrations-google-calendar). |
|
||||
| jira | array | See [`integrations.jira`](#integrations-jira). |
|
||||
| zendesk | array | See [`integrations.zendesk`](#integrations-zendesk). |
|
||||
| google_calendar | array | See [`integrations.google_calendar`](#integrations-google-calendar). |
|
||||
|
||||
|
||||
> Note that when making changes to the `integrations` object, all integrations must be provided (not just the one being modified). This is because the endpoint will consider missing integrations as deleted.
|
||||
|
|
@ -1792,7 +1792,7 @@ _Available in Fleet Premium._
|
|||
|
||||
| Name | Type | Description |
|
||||
| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| custom_settings | list | macOS hosts that belong to no team will have custom profiles applied. |
|
||||
| custom_settings | array | macOS hosts that belong to no team will have custom profiles applied. |
|
||||
|
||||
<br/>
|
||||
|
||||
|
|
@ -1802,7 +1802,7 @@ _Available in Fleet Premium._
|
|||
|
||||
| Name | Type | Description |
|
||||
| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| custom_settings | list | Windows hosts that belong to no team will have custom profiles applied. |
|
||||
| custom_settings | array | Windows hosts that belong to no team will have custom profiles applied. |
|
||||
|
||||
<br/>
|
||||
|
||||
|
|
@ -2099,7 +2099,7 @@ Delete all of a team's existing enroll secrets
|
|||
| email | string | body | **Required.** The email of the invited user. This email will receive the invitation link. |
|
||||
| name | string | body | **Required.** The name of the invited user. |
|
||||
| sso_enabled | boolean | body | **Required.** Whether or not SSO will be enabled for the invited user. |
|
||||
| teams | list | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. |
|
||||
| teams | array | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -2299,7 +2299,7 @@ Verify the specified invite.
|
|||
| email | string | body | The email of the invited user. Updates on the email won't resend the invitation. |
|
||||
| name | string | body | The name of the invited user. |
|
||||
| sso_enabled | boolean | body | Whether or not SSO will be enabled for the invited user. |
|
||||
| teams | list | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. |
|
||||
| teams | array | body | _Available in Fleet Premium_. A list of the teams the user is a member of. Each item includes the team's ID and the user's role in the specified team. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -2489,6 +2489,7 @@ the `software` table.
|
|||
| mdm_id | integer | query | The ID of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider and URL). |
|
||||
| mdm_name | string | query | The name of the _mobile device management_ (MDM) solution to filter hosts by (that is, filter hosts that use a specific MDM provider). |
|
||||
| mdm_enrollment_status | string | query | The _mobile device management_ (MDM) enrollment status to filter hosts by. Valid options are 'manual', 'automatic', 'enrolled', 'pending', or 'unenrolled'. |
|
||||
| connected_to_fleet | boolean | query | Filter hosts that are talking to this Fleet server for MDM features. In rare cases, hosts can be enrolled to one Fleet server but talk to a different Fleet server for MDM features. In this case, the value would be `false`. Always `false` for Linux hosts. |
|
||||
| macos_settings | string | query | Filters the hosts by the status of the _mobile device management_ (MDM) profiles applied to hosts. Valid options are 'verified', 'verifying', 'pending', or 'failed'. **Note: If this filter is used in Fleet Premium without a team ID filter, the results include only hosts that are not assigned to any team.** |
|
||||
| munki_issue_id | integer | query | The ID of the _munki issue_ (a Munki-reported error or warning message) to filter hosts by (that is, filter hosts that are affected by that corresponding error or warning message). |
|
||||
| low_disk_space | integer | query | _Available in Fleet Premium_. Filters the hosts to only include hosts with less GB of disk space available than this value. Must be a number between 1-100. |
|
||||
|
|
@ -3099,10 +3100,11 @@ Returns the information of the specified host.
|
|||
"timezone": "America/New_York"
|
||||
},
|
||||
"mdm": {
|
||||
"encryption_key_available": false,
|
||||
"enrollment_status": null,
|
||||
"name": "",
|
||||
"server_url": null,
|
||||
"encryption_key_available": true,
|
||||
"enrollment_status": "On (manual)",
|
||||
"name": "Fleet",
|
||||
"connected_to_fleet": true,
|
||||
"server_url": "https://acme.com/mdm/apple/mdm",
|
||||
"device_status": "unlocked",
|
||||
"pending_action": "",
|
||||
"macos_settings": {
|
||||
|
|
@ -3518,10 +3520,11 @@ This is the API route used by the **My device** page in Fleet desktop to display
|
|||
}
|
||||
],
|
||||
"mdm": {
|
||||
"encryption_key_available": false,
|
||||
"enrollment_status": null,
|
||||
"name": "",
|
||||
"server_url": null,
|
||||
"encryption_key_available": true,
|
||||
"enrollment_status": "On (manual)",
|
||||
"name": "Fleet",
|
||||
"connected_to_fleet": true,
|
||||
"server_url": "https://acme.com/mdm/apple/mdm",
|
||||
"macos_settings": {
|
||||
"disk_encryption": null,
|
||||
"action_required": null
|
||||
|
|
@ -3696,7 +3699,7 @@ _Available in Fleet Premium_
|
|||
|
||||
| Name | Type | In | Description |
|
||||
| ------- | ------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ids | list | body | A list of the host IDs you'd like to delete. If `ids` is specified, `filters` cannot be specified. |
|
||||
| ids | array | body | A list of the host IDs you'd like to delete. If `ids` is specified, `filters` cannot be specified. |
|
||||
| filters | object | body | Contains any of the following four properties: `query` for search query keywords. Searchable fields include `hostname`, `hardware_serial`, `uuid`, and `ipv4`. `status` to indicate the status of the hosts to return. Can either be `new`, `online`, `offline`, `mia` or `missing`. `label_id` to indicate the selected label. `team_id` to indicate the selected team. If `filters` is specified, `id` cannot be specified. `label_id` and `status` cannot be used at the same time. |
|
||||
|
||||
Either ids or filters are required.
|
||||
|
|
@ -4663,9 +4666,9 @@ Adds manual labels to a host.
|
|||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ------- | ---- | ---------------------------- |
|
||||
| labels | list | body | The list of label names to add to the host. |
|
||||
| Name | Type | In | Description |
|
||||
| ------ | ------- | ---- | ---------------------------- |
|
||||
| labels | array | body | The list of label names to add to the host. |
|
||||
|
||||
|
||||
#### Example
|
||||
|
|
@ -4692,9 +4695,9 @@ Removes manual labels from a host.
|
|||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ------- | ---- | ---------------------------- |
|
||||
| labels | list | body | The list of label names to delete from the host. |
|
||||
| Name | Type | In | Description |
|
||||
| ------ | ------- | ---- | ---------------------------- |
|
||||
| labels | array | body | The list of label names to delete from the host. |
|
||||
|
||||
|
||||
#### Example
|
||||
|
|
@ -5424,7 +5427,6 @@ List all configuration profiles for macOS and Windows hosts enrolled to Fleet's
|
|||
{
|
||||
"name": "Label name 2",
|
||||
"broken": true,
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"name": "Label name 3",
|
||||
|
|
@ -6559,7 +6561,7 @@ For example, a policy might ask “Is Gatekeeper enabled on macOS devices?“ Th
|
|||
|
||||
| Name | Type | In | Description |
|
||||
| -------- | ------- | ---- | ------------------------------------------------- |
|
||||
| ids | list | body | **Required.** The IDs of the policies to delete. |
|
||||
| ids | array | body | **Required.** The IDs of the policies to delete. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -6653,8 +6655,8 @@ Triggers [automations](https://fleetdm.com/docs/using-fleet/automations#policy-a
|
|||
|
||||
| Name | Type | In | Description |
|
||||
| ---------- | -------- | ---- | -------------------------------------------------------- |
|
||||
| policy_ids | list | body | Filters to only run policy automations for the specified policies. |
|
||||
| team_ids | list | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified teams. |
|
||||
| policy_ids | array | body | Filters to only run policy automations for the specified policies. |
|
||||
| team_ids | array | body | _Available in Fleet Premium_. Filters to only run policy automations for hosts in the specified teams. |
|
||||
|
||||
|
||||
#### Example
|
||||
|
|
@ -6994,7 +6996,7 @@ Either `query` or `query_id` must be provided.
|
|||
| Name | Type | In | Description |
|
||||
| -------- | ------- | ---- | ------------------------------------------------- |
|
||||
| team_id | integer | path | **Required.** Defines what team ID to operate on |
|
||||
| ids | list | body | **Required.** The IDs of the policies to delete. |
|
||||
| ids | array | body | **Required.** The IDs of the policies to delete. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -7431,14 +7433,14 @@ Creates a global query or team query.
|
|||
| name | string | body | **Required**. The name of the query. |
|
||||
| query | string | body | **Required**. The query in SQL syntax. |
|
||||
| description | string | body | The query's description. |
|
||||
| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). |
|
||||
| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). |
|
||||
| team_id | integer | body | _Available in Fleet Premium_. The parent team to which the new query should be added. If omitted, the query will be global. |
|
||||
| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. |
|
||||
| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. |
|
||||
| platform | string | body | The OS platforms where this query will run (other platforms ignored). Comma-separated string. If omitted, runs on all compatible platforms. |
|
||||
| min_osquery_version | string | body | The minimum required osqueryd version installed on a host. If omitted, all osqueryd versions are acceptable. |
|
||||
| automations_enabled | boolean | body | Whether to send data to the configured log destination according to the query's `interval`. |
|
||||
| logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. |
|
||||
| discard_data | bool | body | Whether to skip saving the latest query results for each host. Default: `false`. |
|
||||
| logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. |
|
||||
| discard_data | boolean | body | Whether to skip saving the latest query results for each host. Default: `false`. |
|
||||
|
||||
|
||||
#### Example
|
||||
|
|
@ -7505,13 +7507,13 @@ Modifies the query specified by ID.
|
|||
| name | string | body | The name of the query. |
|
||||
| query | string | body | The query in SQL syntax. |
|
||||
| description | string | body | The query's description. |
|
||||
| observer_can_run | bool | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). |
|
||||
| observer_can_run | boolean | body | Whether or not users with the `observer` role can run the query. In Fleet 4.0.0, 3 user roles were introduced (`admin`, `maintainer`, and `observer`). This field is only relevant for the `observer` role. The `observer_plus` role can run any query and is not limited by this flag (`observer_plus` role was added in Fleet 4.30.0). |
|
||||
| interval | integer | body | The amount of time, in seconds, the query waits before running. Can be set to `0` to never run. Default: 0. |
|
||||
| platform | string | body | The OS platforms where this query will run (other platforms ignored). Comma-separated string. If set to "", runs on all compatible platforms. |
|
||||
| min_osquery_version | string | body | The minimum required osqueryd version installed on a host. If omitted, all osqueryd versions are acceptable. |
|
||||
| automations_enabled | boolean | body | Whether to send data to the configured log destination according to the query's `interval`. |
|
||||
| logging | string | body | The type of log output for this query. Valid values: `"snapshot"`(default), `"differential"`, or `"differential_ignore_removals"`. |
|
||||
| discard_data | bool | body | Whether to skip saving the latest query results for each host. |
|
||||
| discard_data | boolean | body | Whether to skip saving the latest query results for each host. |
|
||||
|
||||
> Note that any of the following conditions will cause the existing query report to be deleted:
|
||||
> - Updating the `query` (SQL) field
|
||||
|
|
@ -7615,9 +7617,9 @@ Deletes the queries specified by ID. Returns the count of queries successfully d
|
|||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ---- | ---- | ------------------------------------- |
|
||||
| ids | list | body | **Required.** The IDs of the queries. |
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ----- | ---- | ------------------------------------- |
|
||||
| ids | array | body | **Required.** The IDs of the queries. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -8506,9 +8508,9 @@ Get a list of all software.
|
|||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
|
||||
| query | string | query | Search query keywords. Searchable fields include `title` and `cve`. |
|
||||
| team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". |
|
||||
| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. |
|
||||
| available_for_install | bool | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. |
|
||||
| self_service | bool | query | If `true` or `1`, only lists self-service software. Default is `false`. |
|
||||
| vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. |
|
||||
| available_for_install | boolean | query | If `true` or `1`, only list software that is available for install (added by the user). Default is `false`. |
|
||||
| self_service | boolean | query | If `true` or `1`, only lists self-service software. Default is `false`. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -8627,7 +8629,7 @@ Get a list of all software versions.
|
|||
| order_direction | string | query | **Requires `order_key`**. The direction of the order given the order key. Options include `asc` and `desc`. Default is `asc`. |
|
||||
| query | string | query | Search query keywords. Searchable fields include `name`, `version`, and `cve`. |
|
||||
| team_id | integer | query | _Available in Fleet Premium_. Filters the software to only include the software installed on the hosts that are assigned to the specified team. Use `0` to filter by hosts assigned to "No team". |
|
||||
| vulnerable | bool | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. |
|
||||
| vulnerable | boolean | query | If true or 1, only list software that has detected vulnerabilities. Default is `false`. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
@ -8990,6 +8992,8 @@ OS vulnerability data is currently available for Windows and macOS. For other pl
|
|||
|
||||
### Add package
|
||||
|
||||
> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
|
||||
|
||||
_Available in Fleet Premium._
|
||||
|
||||
Add a package (.pkg, .msi, .exe, .deb) to install on macOS, Windows, or Linux (Ubuntu) hosts.
|
||||
|
|
@ -9136,6 +9140,8 @@ Add App Store (VPP) app purchased in Apple Business Manager.
|
|||
|
||||
### Download package
|
||||
|
||||
> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
|
||||
|
||||
_Available in Fleet Premium._
|
||||
|
||||
`GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media`
|
||||
|
|
@ -9191,6 +9197,8 @@ Install software (package or App Store app) on a macOS, iOS, iPadOS, Windows, or
|
|||
|
||||
### Get package install result
|
||||
|
||||
> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
|
||||
|
||||
_Available in Fleet Premium._
|
||||
|
||||
`GET /api/v1/fleet/software/install/results/:install_uuid`
|
||||
|
|
@ -9828,8 +9836,8 @@ _Available in Fleet Premium_
|
|||
| ------------------------------------------------------- | ------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| id | integer | path | **Required.** The desired team's ID. |
|
||||
| name | string | body | The team's name. |
|
||||
| host_ids | list | body | A list of hosts that belong to the team. |
|
||||
| user_ids | list | body | A list of users on the team. |
|
||||
| host_ids | array | body | A list of hosts that belong to the team. |
|
||||
| user_ids | array | body | A list of users on the team. |
|
||||
| webhook_settings | object | body | Webhook settings contains for the team. |
|
||||
| failing_policies_webhook | object | body | Failing policies webhook settings. |
|
||||
| enable_failing_policies_webhook | boolean | body | Whether or not the failing policies webhook is enabled. |
|
||||
|
|
@ -9864,10 +9872,10 @@ _Available in Fleet Premium_
|
|||
| deadline_days | integer | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before updates are installed on Windows. |
|
||||
| grace_period_days | integer | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have this number of days before Windows restarts to install updates. |
|
||||
| macos_settings | object | body | macOS-specific settings. |
|
||||
| custom_settings | list | body | The list of objects where each object includes .mobileconfig or JSON file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. |
|
||||
| custom_settings | array | body | The list of objects where each object includes .mobileconfig or JSON file (configuration profile) and label name to apply to macOS hosts that belong to this team and are members of the specified label. |
|
||||
| enable_disk_encryption | boolean | body | Hosts that belong to this team and are enrolled into Fleet's MDM will have disk encryption enabled if set to true. |
|
||||
| windows_settings | object | body | Windows-specific settings. |
|
||||
| custom_settings | list | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. |
|
||||
| custom_settings | array | body | The list of objects where each object includes XML file (configuration profile) and label name to apply to Windows hosts that belong to this team and are members of the specified label. |
|
||||
| macos_setup | object | body | Setup for automatic MDM enrollment of macOS hosts. |
|
||||
| enable_end_user_authentication | boolean | body | If set to true, end user authentication will be required during automatic MDM enrollment of new macOS hosts. Settings for your IdP provider must also be [configured](https://fleetdm.com/docs/using-fleet/mdm-macos-setup-experience#end-user-authentication-and-eula). |
|
||||
| integrations | object | body | Integration settings for this team. |
|
||||
|
|
@ -10087,8 +10095,8 @@ _Available in Fleet Premium_
|
|||
| Name | Type | In | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| id | integer | path | **Required.** The desired team's ID. |
|
||||
| force | bool | query | Force apply the options even if there are validation errors. |
|
||||
| dry_run | bool | query | Validate the options and return any validation errors, but do not apply the changes. |
|
||||
| force | boolean | query | Force apply the options even if there are validation errors. |
|
||||
| dry_run | boolean | query | Validate the options and return any validation errors, but do not apply the changes. |
|
||||
| _JSON data_ | object | body | The JSON to use as agent options for this team. See [Agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) for details. |
|
||||
|
||||
#### Example
|
||||
|
|
@ -10199,9 +10207,9 @@ Transforms a host name into a host id. For example, the Fleet UI use this endpoi
|
|||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ----- | ---- | ---------------------------------------- |
|
||||
| list | array | body | **Required** list of items to translate. |
|
||||
| Name | Type | In | Description |
|
||||
| ----- | ----- | ---- | ---------------------------------------- |
|
||||
| array | array | body | **Required** list of items to translate. |
|
||||
|
||||
#### Example
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
|
@ -17,6 +18,9 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
|
||||
)
|
||||
|
||||
// Used for overriding the env var value in testing
|
||||
var testSetEmptyPrivateKey bool
|
||||
|
||||
// getVPPToken returns the base64 encoded VPP token, ready for use in requests to Apple's VPP API.
|
||||
// It returns an error if the token is expired.
|
||||
func (svc *Service) getVPPToken(ctx context.Context, teamID *uint) (string, error) {
|
||||
|
|
@ -413,3 +417,143 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V
|
|||
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UploadVPPToken(ctx context.Context, token io.ReadSeeker) (*fleet.VPPTokenDB, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey := svc.config.Server.PrivateKey
|
||||
if testSetEmptyPrivateKey {
|
||||
privateKey = ""
|
||||
}
|
||||
|
||||
if len(privateKey) == 0 {
|
||||
return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager."))
|
||||
}
|
||||
|
||||
tokenBytes, err := io.ReadAll(token)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "reading VPP token")
|
||||
}
|
||||
|
||||
locName, err := vpp.GetConfig(string(tokenBytes))
|
||||
if err != nil {
|
||||
var vppErr *vpp.ErrorResponse
|
||||
if errors.As(err, &vppErr) {
|
||||
// Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes
|
||||
if vppErr.ErrorNumber == 9622 {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager."))
|
||||
}
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple")
|
||||
}
|
||||
|
||||
data := fleet.VPPTokenData{
|
||||
Token: string(tokenBytes),
|
||||
Location: locName,
|
||||
}
|
||||
|
||||
tok, err := svc.ds.InsertVPPToken(ctx, &data)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "writing VPP token to db")
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEnabledVPP{
|
||||
Location: locName,
|
||||
}); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "create activity for upload VPP token")
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey := svc.config.Server.PrivateKey
|
||||
if testSetEmptyPrivateKey {
|
||||
privateKey = ""
|
||||
}
|
||||
|
||||
if len(privateKey) == 0 {
|
||||
return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager."))
|
||||
}
|
||||
|
||||
tokenBytes, err := io.ReadAll(token)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "reading VPP token")
|
||||
}
|
||||
|
||||
locName, err := vpp.GetConfig(string(tokenBytes))
|
||||
if err != nil {
|
||||
var vppErr *vpp.ErrorResponse
|
||||
if errors.As(err, &vppErr) {
|
||||
// Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes
|
||||
if vppErr.ErrorNumber == 9622 {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager."))
|
||||
}
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple")
|
||||
}
|
||||
|
||||
data := fleet.VPPTokenData{
|
||||
Token: string(tokenBytes),
|
||||
Location: locName,
|
||||
}
|
||||
|
||||
tok, err := svc.ds.UpdateVPPToken(ctx, tokenID, &data)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "updating vpp token")
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tok, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, teamIDs)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "updating vpp token team")
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svc.ds.ListVPPTokens(ctx)
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
tok, err := svc.ds.GetVPPToken(ctx, tokenID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting vpp token")
|
||||
}
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDisabledVPP{
|
||||
Location: tok.Location,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for delete VPP token")
|
||||
}
|
||||
|
||||
return svc.ds.DeleteVPPToken(ctx, tokenID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ const DEFAULT_POLICY_MOCK: IPolicyStats = {
|
|||
has_run: true,
|
||||
next_update_ms: 3600000,
|
||||
calendar_events_enabled: true,
|
||||
install_software: {
|
||||
name: "testSw0",
|
||||
software_title_id: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const createMockPolicy = (overrides?: Partial<IPolicyStats>): IPolicyStats => {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ interface IEditorProps {
|
|||
* @default "editor"
|
||||
*/
|
||||
name?: string;
|
||||
/** Include correct styles as a form field.
|
||||
* @default false
|
||||
*/
|
||||
isFormField?: boolean;
|
||||
maxLines?: number;
|
||||
className?: string;
|
||||
onChange?: (value: string, event?: any) => void;
|
||||
|
|
@ -52,11 +56,13 @@ const Editor = ({
|
|||
readOnly = false,
|
||||
wrapEnabled = false,
|
||||
name = "editor",
|
||||
isFormField = false,
|
||||
maxLines = 20,
|
||||
className,
|
||||
onChange,
|
||||
}: IEditorProps) => {
|
||||
const classNames = classnames(baseClass, className, {
|
||||
"form-field": isFormField,
|
||||
[`${baseClass}__error`]: !!error,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
&__label {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
margin-bottom: $pad-small;
|
||||
|
||||
&--error {
|
||||
color: $core-vibrant-red;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export interface IFleetAceProps {
|
|||
label?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
maxLines?: number;
|
||||
showGutter?: boolean;
|
||||
|
|
@ -55,6 +56,7 @@ const FleetAce = ({
|
|||
labelActionComponent,
|
||||
name = "query-editor",
|
||||
value,
|
||||
placeholder,
|
||||
readOnly,
|
||||
maxLines = 20,
|
||||
showGutter = true,
|
||||
|
|
@ -266,6 +268,7 @@ const FleetAce = ({
|
|||
showPrintMargin={false}
|
||||
theme="fleet"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
width="100%"
|
||||
wrapEnabled={wrapEnabled}
|
||||
style={style}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ace_content {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.ace_placeholder {
|
||||
font-family: "SourceCodePro", $monospace;
|
||||
margin: initial;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&__help-text {
|
||||
@include help-text;
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,19 @@ class Dropdown extends Component {
|
|||
onClose: PropTypes.func,
|
||||
options: PropTypes.arrayOf(dropdownOptionInterface).isRequired,
|
||||
placeholder: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
|
||||
/**
|
||||
value must correspond to the value of a dropdown option to render
|
||||
e.g. with options:
|
||||
|
||||
[
|
||||
{
|
||||
label: "Display name",
|
||||
value: 1, <– the id of the thing
|
||||
}
|
||||
]
|
||||
|
||||
set value to 1, not "Display name"
|
||||
*/
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.string,
|
||||
|
|
@ -75,7 +88,7 @@ class Dropdown extends Component {
|
|||
const { multi, onChange, clearable, name, parseTarget } = this.props;
|
||||
|
||||
if (parseTarget) {
|
||||
// Returns both name and value
|
||||
// Returns both name of the Dropdown and value of the selected option
|
||||
return onChange({ value: selected.value, name });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ export interface IPolicy {
|
|||
updated_at: string;
|
||||
critical: boolean;
|
||||
calendar_events_enabled: boolean;
|
||||
install_software?: IPolicySoftwareToInstall;
|
||||
}
|
||||
|
||||
export interface IPolicySoftwareToInstall {
|
||||
name: string;
|
||||
software_title_id: number;
|
||||
}
|
||||
|
||||
// Used on the manage hosts page and other places where aggregate stats are displayed
|
||||
|
|
@ -94,6 +100,8 @@ export interface IPolicyFormData {
|
|||
team_id?: number | null;
|
||||
id?: number;
|
||||
calendar_events_enabled?: boolean;
|
||||
// undefined from GET/LIST when not set, null for PATCH to unset
|
||||
software_title_id?: number | null;
|
||||
}
|
||||
|
||||
export interface IPolicyNew {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export interface ISoftwareTitleDetails {
|
|||
source: string; // "apps" | "ios_apps" | "ipados_apps" | ?
|
||||
hosts_count: number;
|
||||
versions: ISoftwareTitleVersion[] | null;
|
||||
versions_updated_at?: string;
|
||||
bundle_identifier?: string;
|
||||
browser?: string;
|
||||
versions_count?: number;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ interface IFileDetailsProps {
|
|||
|
||||
// TODO: if we reuse this one more time, we should consider moving this
|
||||
// into FileUploader as a default preview. Currently we have this in
|
||||
// AddSoftwareForm.tsx and here.
|
||||
// AddPackageForm.tsx and here.
|
||||
const FileDetails = ({ details: { name, platform } }: IFileDetailsProps) => (
|
||||
<div className={`${baseClass}__selected-file`}>
|
||||
<ProfileGraphic baseClass={baseClass} />
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const AdvancedOptionsModal = ({
|
|||
helpText="Fleet will run this command on hosts to install software."
|
||||
label="Install script"
|
||||
labelTooltip="For security agents, add the script provided by the vendor."
|
||||
isFormField
|
||||
/>
|
||||
{preInstallQuery && (
|
||||
<div className={`${baseClass}__input-field`}>
|
||||
|
|
@ -72,6 +73,7 @@ const AdvancedOptionsModal = ({
|
|||
maxLines={10}
|
||||
value={postInstallScript}
|
||||
helpText="Shell (macOS and Linux) or PowerShell (Windows)."
|
||||
isFormField
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@ import React, { useCallback, useContext } from "react";
|
|||
import softwareAPI from "services/entities/software";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
const baseClass = "delete-software-modal";
|
||||
|
||||
const DELETE_SW_USED_BY_POLICY_ERROR_MSG =
|
||||
"Couldn't delete. Policy automation uses this software. Please disable policy automation for this software and try again.";
|
||||
interface IDeleteSoftwareModalProps {
|
||||
softwareId: number;
|
||||
teamId: number;
|
||||
|
|
@ -28,8 +32,13 @@ const DeleteSoftwareModal = ({
|
|||
await softwareAPI.deleteSoftwarePackage(softwareId, teamId);
|
||||
renderFlash("success", "Software deleted successfully!");
|
||||
onSuccess();
|
||||
} catch {
|
||||
renderFlash("error", "Couldn't delete. Please try again.");
|
||||
} catch (error) {
|
||||
const reason = getErrorReason(error);
|
||||
if (reason.includes("Policy automation uses this software")) {
|
||||
renderFlash("error", DELETE_SW_USED_BY_POLICY_ERROR_MSG);
|
||||
} else {
|
||||
renderFlash("error", "Couldn't delete. Please try again.");
|
||||
}
|
||||
}
|
||||
onExit();
|
||||
}, [softwareId, teamId, renderFlash, onSuccess, onExit]);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import React, {
|
|||
useLayoutEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import FileSaver from "file-saver";
|
||||
import { parse } from "content-disposition";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
|
|
@ -45,10 +43,15 @@ function useTruncatedElement<T extends HTMLElement>(ref: React.RefObject<T>) {
|
|||
|
||||
useLayoutEffect(() => {
|
||||
const element = ref.current;
|
||||
if (element) {
|
||||
const { scrollWidth, clientWidth } = element;
|
||||
setIsTruncated(scrollWidth > clientWidth);
|
||||
function updateIsTruncated() {
|
||||
if (element) {
|
||||
const { scrollWidth, clientWidth } = element;
|
||||
setIsTruncated(scrollWidth > clientWidth);
|
||||
}
|
||||
}
|
||||
window.addEventListener("resize", updateIsTruncated);
|
||||
updateIsTruncated();
|
||||
return () => window.removeEventListener("resize", updateIsTruncated);
|
||||
}, [ref]);
|
||||
|
||||
return isTruncated;
|
||||
|
|
@ -92,20 +95,29 @@ const STATUS_DISPLAY_OPTIONS: Record<
|
|||
iconName: "success",
|
||||
tooltip: (
|
||||
<>
|
||||
Fleet installed software on these hosts. Currently, if the software is
|
||||
uninstalled, the "Installed" status won't be updated.
|
||||
Software is installed on these hosts (install script finished
|
||||
<br />
|
||||
with exit code 0). Currently, if the software is uninstalled, the
|
||||
<br />
|
||||
"installed" status won't be updated.
|
||||
</>
|
||||
),
|
||||
},
|
||||
pending: {
|
||||
displayName: "Pending",
|
||||
iconName: "pending-outline",
|
||||
tooltip: "Fleet will install software when these hosts come online.",
|
||||
tooltip: "Fleet is installing or will install when the host comes online.",
|
||||
},
|
||||
failed: {
|
||||
displayName: "Failed",
|
||||
iconName: "error",
|
||||
tooltip: "Fleet failed to install software on these hosts.",
|
||||
tooltip: (
|
||||
<>
|
||||
These hosts failed to install software. Click on a host to view
|
||||
<br />
|
||||
error(s).
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -130,16 +142,18 @@ const PackageStatusCount = ({
|
|||
})}`;
|
||||
return (
|
||||
<DataSet
|
||||
className={`${baseClass}__status`}
|
||||
title={
|
||||
<TooltipWrapper
|
||||
position="top"
|
||||
tipContent={displayData.tooltip}
|
||||
underline={false}
|
||||
showArrow
|
||||
tipOffset={10}
|
||||
>
|
||||
<div className={`${baseClass}__status-title`}>
|
||||
<Icon name={displayData.iconName} />
|
||||
<span>{displayData.displayName}</span>
|
||||
<div>{displayData.displayName}</div>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
}
|
||||
|
|
@ -305,7 +319,7 @@ const SoftwarePackageCard = ({
|
|||
|
||||
return (
|
||||
<Card borderRadiusSize="xxlarge" includeShadow className={baseClass}>
|
||||
<div className={`${baseClass}__main-content`}>
|
||||
<div className={`${baseClass}__row-1`}>
|
||||
{/* TODO: main-info could be a seperate component as its reused on a couple
|
||||
pages already. Come back and pull this into a component */}
|
||||
<div className={`${baseClass}__main-info`}>
|
||||
|
|
@ -315,46 +329,46 @@ const SoftwarePackageCard = ({
|
|||
<span className={`${baseClass}__details`}>{renderDetails()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__package-statuses`}>
|
||||
<PackageStatusCount
|
||||
softwareId={softwareId}
|
||||
status="installed"
|
||||
count={status.installed}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<PackageStatusCount
|
||||
softwareId={softwareId}
|
||||
status="pending"
|
||||
count={status.pending}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<PackageStatusCount
|
||||
softwareId={softwareId}
|
||||
status="failed"
|
||||
count={status.failed}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<div className={`${baseClass}__actions-wrapper`}>
|
||||
{isSelfService && (
|
||||
<div className={`${baseClass}__self-service-badge`}>
|
||||
<Icon
|
||||
name="install-self-service"
|
||||
size="small"
|
||||
color="ui-fleet-black-75"
|
||||
/>
|
||||
Self-service
|
||||
</div>
|
||||
)}
|
||||
{showActions && (
|
||||
<ActionsDropdown
|
||||
isSoftwarePackage={!!softwarePackage}
|
||||
onDownloadClick={onDownloadClick}
|
||||
onDeleteClick={onDeleteClick}
|
||||
onAdvancedOptionsClick={onAdvancedOptionsClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__actions-wrapper`}>
|
||||
{isSelfService && (
|
||||
<div className={`${baseClass}__self-service-badge`}>
|
||||
<Icon
|
||||
name="install-self-service"
|
||||
size="small"
|
||||
color="ui-fleet-black-75"
|
||||
/>
|
||||
Self-service
|
||||
</div>
|
||||
)}
|
||||
{showActions && (
|
||||
<ActionsDropdown
|
||||
isSoftwarePackage={!!softwarePackage}
|
||||
onDownloadClick={onDownloadClick}
|
||||
onDeleteClick={onDeleteClick}
|
||||
onAdvancedOptionsClick={onAdvancedOptionsClick}
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__package-statuses`}>
|
||||
<PackageStatusCount
|
||||
softwareId={softwareId}
|
||||
status="installed"
|
||||
count={status.installed}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<PackageStatusCount
|
||||
softwareId={softwareId}
|
||||
status="pending"
|
||||
count={status.pending}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<PackageStatusCount
|
||||
softwareId={softwareId}
|
||||
status="failed"
|
||||
count={status.failed}
|
||||
teamId={teamId}
|
||||
/>
|
||||
</div>
|
||||
{showAdvancedOptionsModal && (
|
||||
<AdvancedOptionsModal
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
.software-package-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
|
||||
&__main-content {
|
||||
&__row-1 {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $pad-xxlarge;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
&__main-info {
|
||||
|
|
@ -19,13 +21,14 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-xsmall;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
@include ellipse-text;
|
||||
max-width: 290px;
|
||||
max-width: 48vw;
|
||||
}
|
||||
|
||||
&__details {
|
||||
|
|
@ -34,13 +37,36 @@
|
|||
|
||||
&__package-statuses {
|
||||
display: flex;
|
||||
gap: $pad-xxlarge;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
border-radius: 6px;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
&__status-title {
|
||||
&__status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 24px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: $pad-xsmall;
|
||||
flex: 1 0 0;
|
||||
border-right: 1px solid var(--UI-Fleet-Black-10, #e2e4ea);
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.react-tooltip {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__status-title{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__status-count {
|
||||
|
|
@ -66,6 +92,7 @@
|
|||
color: $ui-fleet-black-75;
|
||||
font-size: $xx-small;
|
||||
font-weight: $bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ const SoftwareTitleDetailsPage = ({
|
|||
softwareTitle.source
|
||||
)}
|
||||
isAvailableForInstall={isAvailableForInstall}
|
||||
countsUpdatedAt={softwareTitle.versions_updated_at}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import TableContainer from "components/TableContainer";
|
|||
import TableCount from "components/TableContainer/TableCount";
|
||||
import EmptyTable from "components/EmptyTable";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
|
||||
import generateSoftwareTitleDetailsTableConfig from "./SoftwareTitleDetailsTableConfig";
|
||||
|
||||
|
|
@ -21,6 +22,21 @@ const DEFAULT_SORT_DIRECTION = "desc";
|
|||
|
||||
const baseClass = "software-title-details-table";
|
||||
|
||||
const SoftwareLastUpdatedInfo = (lastUpdatedAt: string) => {
|
||||
return (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
customTooltipText={
|
||||
<>
|
||||
The last time software data was <br />
|
||||
updated, including vulnerabilities <br />
|
||||
and host counts.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const NoVersionsDetected = (isAvailableForInstall = false): JSX.Element => {
|
||||
return (
|
||||
<EmptyTable
|
||||
|
|
@ -54,6 +70,7 @@ interface ISoftwareTitleDetailsTableProps {
|
|||
teamIdForApi?: number;
|
||||
isIPadOSOrIOSApp: boolean;
|
||||
isAvailableForInstall?: boolean;
|
||||
countsUpdatedAt?: string;
|
||||
}
|
||||
|
||||
interface IRowProps extends Row {
|
||||
|
|
@ -69,6 +86,7 @@ const SoftwareTitleDetailsTable = ({
|
|||
teamIdForApi,
|
||||
isIPadOSOrIOSApp,
|
||||
isAvailableForInstall,
|
||||
countsUpdatedAt,
|
||||
}: ISoftwareTitleDetailsTableProps) => {
|
||||
const handleRowSelect = (row: IRowProps) => {
|
||||
const hostsBySoftwareParams = {
|
||||
|
|
@ -95,7 +113,10 @@ const SoftwareTitleDetailsTable = ({
|
|||
);
|
||||
|
||||
const renderVersionsCount = () => (
|
||||
<TableCount name="versions" count={data?.length} />
|
||||
<>
|
||||
<TableCount name="versions" count={data?.length} />
|
||||
{countsUpdatedAt && SoftwareLastUpdatedInfo(countsUpdatedAt)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import softwareAPI from "services/entities/software";
|
||||
import { QueryParams, buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import AddPackageForm from "../AddPackageForm";
|
||||
import { IAddSoftwareFormData } from "../AddPackageForm/AddSoftwareForm";
|
||||
import { IAddPackageFormData } from "../AddPackageForm/AddPackageForm";
|
||||
import { getErrorMessage } from "../AddSoftwareModal/helpers";
|
||||
|
||||
const baseClass = "add-package";
|
||||
|
|
@ -60,7 +66,7 @@ const AddPackage = ({
|
|||
};
|
||||
}, [isUploading]);
|
||||
|
||||
const onAddPackage = async (formData: IAddSoftwareFormData) => {
|
||||
const onAddPackage = async (formData: IAddPackageFormData) => {
|
||||
setIsUploading(true);
|
||||
|
||||
if (formData.software && formData.software.size > MAX_FILE_SIZE_BYTES) {
|
||||
|
|
@ -98,6 +104,21 @@ const AddPackage = ({
|
|||
`${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}`
|
||||
);
|
||||
} catch (e) {
|
||||
const reason = getErrorReason(e);
|
||||
if (
|
||||
reason.includes("Couldn't add. Fleet couldn't read the version from")
|
||||
) {
|
||||
renderFlash(
|
||||
"error",
|
||||
`${reason}. ${(
|
||||
<CustomLink
|
||||
newTab
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/read-package-version`}
|
||||
text="Learn more"
|
||||
/>
|
||||
)} `
|
||||
);
|
||||
}
|
||||
renderFlash("error", getErrorMessage(e));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
import Editor from "components/Editor";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import FleetAce from "components/FleetAce";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
|
||||
const baseClass = "add-package-advanced-options";
|
||||
|
||||
interface IAddPackageAdvancedOptionsProps {
|
||||
errors: { preInstallQuery?: string; postInstallScript?: string };
|
||||
preInstallQuery?: string;
|
||||
installScript: string;
|
||||
postInstallScript?: string;
|
||||
onChangePreInstallQuery: (value?: string) => void;
|
||||
onChangeInstallScript: (value: string) => void;
|
||||
onChangePostInstallScript: (value?: string) => void;
|
||||
}
|
||||
|
||||
const AddPackageAdvancedOptions = ({
|
||||
errors,
|
||||
preInstallQuery,
|
||||
installScript,
|
||||
postInstallScript,
|
||||
onChangePreInstallQuery,
|
||||
onChangeInstallScript,
|
||||
onChangePostInstallScript,
|
||||
}: IAddPackageAdvancedOptionsProps) => {
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<RevealButton
|
||||
className={`${baseClass}__accordion-title`}
|
||||
isShowing={showAdvancedOptions}
|
||||
showText="Advanced options"
|
||||
hideText="Advanced options"
|
||||
caretPosition="after"
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<div className={`${baseClass}__input-fields`}>
|
||||
<FleetAce
|
||||
className="form-field"
|
||||
focus
|
||||
error={errors.preInstallQuery}
|
||||
value={preInstallQuery}
|
||||
placeholder="SELECT * FROM osquery_info WHERE start_time > 1"
|
||||
label="Pre-install query"
|
||||
name="preInstallQuery"
|
||||
maxLines={10}
|
||||
onChange={onChangePreInstallQuery}
|
||||
helpText={
|
||||
<>
|
||||
Software will be installed only if the{" "}
|
||||
<CustomLink
|
||||
className={`${baseClass}__table-link`}
|
||||
text="query returns results"
|
||||
url="https://fleetdm.com/tables"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Editor
|
||||
wrapEnabled
|
||||
maxLines={10}
|
||||
name="install-script"
|
||||
onChange={onChangeInstallScript}
|
||||
value={installScript}
|
||||
helpText="Shell (macOS and Linux) or PowerShell (Windows)."
|
||||
label="Install script"
|
||||
labelTooltip={
|
||||
<>
|
||||
Fleet will run this script on hosts to install software. Use the
|
||||
<br />
|
||||
$INSTALLER_PATH variable to point to the installer.
|
||||
</>
|
||||
}
|
||||
isFormField
|
||||
/>
|
||||
<Editor
|
||||
label="Post-install script"
|
||||
labelTooltip="Fleet will run this script after install."
|
||||
focus
|
||||
error={errors.postInstallScript}
|
||||
wrapEnabled
|
||||
name="post-install-script-editor"
|
||||
maxLines={10}
|
||||
onChange={onChangePostInstallScript}
|
||||
value={postInstallScript}
|
||||
helpText="Shell (macOS and Linux) or PowerShell (Windows)."
|
||||
isFormField
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPackageAdvancedOptions;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.add-software-advanced-options {
|
||||
.add-package-advanced-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./AddPackageAdvancedOptions";
|
||||
|
|
@ -6,7 +6,6 @@ import getInstallScript from "utilities/software_install_scripts";
|
|||
|
||||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import Editor from "components/Editor";
|
||||
import {
|
||||
FileUploader,
|
||||
FileDetails,
|
||||
|
|
@ -14,25 +13,25 @@ import {
|
|||
import Spinner from "components/Spinner";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
|
||||
import AddSoftwareAdvancedOptions from "../AddSoftwareAdvancedOptions";
|
||||
import AddPackageAdvancedOptions from "../AddPackageAdvancedOptions";
|
||||
|
||||
import { generateFormValidation } from "./helpers";
|
||||
|
||||
export const baseClass = "add-software-form";
|
||||
export const baseClass = "add-package-form";
|
||||
|
||||
const UploadingSoftware = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__uploading-message`}>
|
||||
<Spinner centered={false} />
|
||||
<p>Uploading. It may take a few minutes to finish.</p>
|
||||
<p>Adding software. This may take a few minutes to finish.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IAddSoftwareFormData {
|
||||
export interface IAddPackageFormData {
|
||||
software: File | null;
|
||||
installScript: string;
|
||||
preInstallCondition?: string;
|
||||
preInstallQuery?: string;
|
||||
postInstallScript?: string;
|
||||
selfService: boolean;
|
||||
}
|
||||
|
|
@ -40,30 +39,28 @@ export interface IAddSoftwareFormData {
|
|||
export interface IFormValidation {
|
||||
isValid: boolean;
|
||||
software: { isValid: boolean };
|
||||
preInstallCondition?: { isValid: boolean; message?: string };
|
||||
preInstallQuery?: { isValid: boolean; message?: string };
|
||||
postInstallScript?: { isValid: boolean; message?: string };
|
||||
selfService?: { isValid: boolean };
|
||||
}
|
||||
|
||||
interface IAddSoftwareFormProps {
|
||||
interface IAddPackageFormProps {
|
||||
isUploading: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: (formData: IAddSoftwareFormData) => void;
|
||||
onSubmit: (formData: IAddPackageFormData) => void;
|
||||
}
|
||||
|
||||
const AddSoftwareForm = ({
|
||||
const AddPackageForm = ({
|
||||
isUploading,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: IAddSoftwareFormProps) => {
|
||||
}: IAddPackageFormProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [showPreInstallCondition, setShowPreInstallCondition] = useState(false);
|
||||
const [showPostInstallScript, setShowPostInstallScript] = useState(false);
|
||||
const [formData, setFormData] = useState<IAddSoftwareFormData>({
|
||||
const [formData, setFormData] = useState<IAddPackageFormData>({
|
||||
software: null,
|
||||
installScript: "",
|
||||
preInstallCondition: undefined,
|
||||
preInstallQuery: undefined,
|
||||
postInstallScript: undefined,
|
||||
selfService: false,
|
||||
});
|
||||
|
|
@ -90,13 +87,7 @@ const AddSoftwareForm = ({
|
|||
installScript,
|
||||
};
|
||||
setFormData(newData);
|
||||
setFormValidation(
|
||||
generateFormValidation(
|
||||
newData,
|
||||
showPreInstallCondition,
|
||||
showPostInstallScript
|
||||
)
|
||||
);
|
||||
setFormValidation(generateFormValidation(newData));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -105,62 +96,26 @@ const AddSoftwareForm = ({
|
|||
onSubmit(formData);
|
||||
};
|
||||
|
||||
const onTogglePreInstallConditionCheckbox = (value: boolean) => {
|
||||
const newData = { ...formData, preInstallCondition: undefined };
|
||||
setShowPreInstallCondition(value);
|
||||
setFormData(newData);
|
||||
setFormValidation(
|
||||
generateFormValidation(newData, value, showPostInstallScript)
|
||||
);
|
||||
};
|
||||
|
||||
const onTogglePostInstallScriptCheckbox = (value: boolean) => {
|
||||
const newData = { ...formData, postInstallScript: undefined };
|
||||
setShowPostInstallScript(value);
|
||||
setFormData(newData);
|
||||
setFormValidation(
|
||||
generateFormValidation(newData, showPreInstallCondition, value)
|
||||
);
|
||||
};
|
||||
|
||||
const onChangeInstallScript = (value: string) => {
|
||||
setFormData({ ...formData, installScript: value });
|
||||
};
|
||||
|
||||
const onChangePreInstallCondition = (value?: string) => {
|
||||
const newData = { ...formData, preInstallCondition: value };
|
||||
const onChangePreInstallQuery = (value?: string) => {
|
||||
const newData = { ...formData, preInstallQuery: value };
|
||||
setFormData(newData);
|
||||
setFormValidation(
|
||||
generateFormValidation(
|
||||
newData,
|
||||
showPreInstallCondition,
|
||||
showPostInstallScript
|
||||
)
|
||||
);
|
||||
setFormValidation(generateFormValidation(newData));
|
||||
};
|
||||
|
||||
const onChangePostInstallScript = (value?: string) => {
|
||||
const newData = { ...formData, postInstallScript: value };
|
||||
setFormData(newData);
|
||||
setFormValidation(
|
||||
generateFormValidation(
|
||||
newData,
|
||||
showPreInstallCondition,
|
||||
showPostInstallScript
|
||||
)
|
||||
);
|
||||
setFormValidation(generateFormValidation(newData));
|
||||
};
|
||||
|
||||
const onToggleSelfServiceCheckbox = (value: boolean) => {
|
||||
const newData = { ...formData, selfService: value };
|
||||
setFormData(newData);
|
||||
setFormValidation(
|
||||
generateFormValidation(
|
||||
newData,
|
||||
showPreInstallCondition,
|
||||
showPostInstallScript
|
||||
)
|
||||
);
|
||||
setFormValidation(generateFormValidation(newData));
|
||||
};
|
||||
|
||||
const isSubmitDisabled = !formValidation.isValid;
|
||||
|
|
@ -185,25 +140,6 @@ const AddSoftwareForm = ({
|
|||
)
|
||||
}
|
||||
/>
|
||||
{formData.software && (
|
||||
<Editor
|
||||
wrapEnabled
|
||||
maxLines={10}
|
||||
name="install-script"
|
||||
onChange={onChangeInstallScript}
|
||||
value={formData.installScript}
|
||||
helpText="Fleet will run this command on hosts to install software."
|
||||
label="Install script"
|
||||
labelTooltip={
|
||||
<>
|
||||
For security agents, add the script provided by the vendor.
|
||||
<br />
|
||||
In custom scripts, you can use the $INSTALLER_PATH variable to
|
||||
point to the installer.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
value={formData.selfService}
|
||||
onChange={onToggleSelfServiceCheckbox}
|
||||
|
|
@ -219,19 +155,17 @@ const AddSoftwareForm = ({
|
|||
Self-service
|
||||
</TooltipWrapper>
|
||||
</Checkbox>
|
||||
<AddSoftwareAdvancedOptions
|
||||
<AddPackageAdvancedOptions
|
||||
errors={{
|
||||
preInstallCondition: formValidation.preInstallCondition?.message,
|
||||
preInstallQuery: formValidation.preInstallQuery?.message,
|
||||
postInstallScript: formValidation.postInstallScript?.message,
|
||||
}}
|
||||
showPreInstallCondition={showPreInstallCondition}
|
||||
showPostInstallScript={showPostInstallScript}
|
||||
preInstallCondition={formData.preInstallCondition}
|
||||
preInstallQuery={formData.preInstallQuery}
|
||||
postInstallScript={formData.postInstallScript}
|
||||
onTogglePreInstallCondition={onTogglePreInstallConditionCheckbox}
|
||||
onTogglePostInstallScript={onTogglePostInstallScriptCheckbox}
|
||||
onChangePreInstallCondition={onChangePreInstallCondition}
|
||||
onChangePreInstallQuery={onChangePreInstallQuery}
|
||||
onChangeInstallScript={onChangeInstallScript}
|
||||
onChangePostInstallScript={onChangePostInstallScript}
|
||||
installScript={formData.installScript}
|
||||
/>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button type="submit" variant="brand" disabled={isSubmitDisabled}>
|
||||
|
|
@ -247,4 +181,4 @@ const AddSoftwareForm = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default AddSoftwareForm;
|
||||
export default AddPackageForm;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.add-software-form {
|
||||
.add-package-form {
|
||||
&__uploading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -3,23 +3,19 @@ import validator from "validator";
|
|||
// @ts-ignore
|
||||
import validateQuery from "components/forms/validators/validate_query";
|
||||
|
||||
import { IAddSoftwareFormData, IFormValidation } from "./AddSoftwareForm";
|
||||
import { IAddPackageFormData, IFormValidation } from "./AddPackageForm";
|
||||
|
||||
type IAddSoftwareFormValidatorKey = Exclude<
|
||||
keyof IAddSoftwareFormData,
|
||||
type IAddPackageFormValidatorKey = Exclude<
|
||||
keyof IAddPackageFormData,
|
||||
"installScript"
|
||||
>;
|
||||
|
||||
type IMessageFunc = (formData: IAddSoftwareFormData) => string;
|
||||
type IMessageFunc = (formData: IAddPackageFormData) => string;
|
||||
type IValidationMessage = string | IMessageFunc;
|
||||
|
||||
interface IValidation {
|
||||
name: string;
|
||||
isValid: (
|
||||
formData: IAddSoftwareFormData,
|
||||
enabledPreInstallCondition?: boolean,
|
||||
enabledPostInstallScript?: boolean
|
||||
) => boolean;
|
||||
isValid: (formData: IAddPackageFormData) => boolean;
|
||||
message?: IValidationMessage;
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +23,7 @@ interface IValidation {
|
|||
* to determine if a field is valid, and rules for generating an error message.
|
||||
*/
|
||||
const FORM_VALIDATION_CONFIG: Record<
|
||||
IAddSoftwareFormValidatorKey,
|
||||
IAddPackageFormValidatorKey,
|
||||
{ validations: IValidation[] }
|
||||
> = {
|
||||
software: {
|
||||
|
|
@ -38,70 +34,23 @@ const FORM_VALIDATION_CONFIG: Record<
|
|||
},
|
||||
],
|
||||
},
|
||||
preInstallCondition: {
|
||||
preInstallQuery: {
|
||||
validations: [
|
||||
{
|
||||
name: "required",
|
||||
isValid: (
|
||||
formData: IAddSoftwareFormData,
|
||||
enabledPreInstallCondition
|
||||
) => {
|
||||
if (!enabledPreInstallCondition) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
formData.preInstallCondition !== undefined &&
|
||||
!validator.isEmpty(formData.preInstallCondition)
|
||||
);
|
||||
},
|
||||
message: (formData) => {
|
||||
// we dont want an error message until the user has interacted with
|
||||
// the field. This is why we check for undefined here.
|
||||
if (formData.preInstallCondition === undefined) {
|
||||
return "";
|
||||
}
|
||||
return "Pre-install condition is required when enabled.";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalidQuery",
|
||||
isValid: (formData, enabledPreInstallCondition) => {
|
||||
if (!enabledPreInstallCondition) {
|
||||
return true;
|
||||
}
|
||||
isValid: (formData) => {
|
||||
const query = formData.preInstallQuery;
|
||||
return (
|
||||
formData.preInstallCondition !== undefined &&
|
||||
validateQuery(formData.preInstallCondition).valid
|
||||
query === undefined || query === "" || validateQuery(query).valid
|
||||
);
|
||||
},
|
||||
message: (formData) =>
|
||||
validateQuery(formData.preInstallCondition).error,
|
||||
message: (formData) => validateQuery(formData.preInstallQuery).error,
|
||||
},
|
||||
],
|
||||
},
|
||||
postInstallScript: {
|
||||
validations: [
|
||||
{
|
||||
name: "required",
|
||||
message: (formData) => {
|
||||
// we dont want an error message until the user has interacted with
|
||||
// the field. This is why we check for undefined here.
|
||||
if (formData.postInstallScript === undefined) {
|
||||
return "";
|
||||
}
|
||||
return "Post-install script is required when enabled.";
|
||||
},
|
||||
isValid: (formData, _, enabledPostInstallScript) => {
|
||||
if (!enabledPostInstallScript) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
formData.postInstallScript !== undefined &&
|
||||
!validator.isEmpty(formData.postInstallScript)
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
// no validations related to postInstallScript
|
||||
validations: [],
|
||||
},
|
||||
selfService: {
|
||||
// no validations related to self service
|
||||
|
|
@ -110,7 +59,7 @@ const FORM_VALIDATION_CONFIG: Record<
|
|||
};
|
||||
|
||||
const getErrorMessage = (
|
||||
formData: IAddSoftwareFormData,
|
||||
formData: IAddPackageFormData,
|
||||
message?: IValidationMessage
|
||||
) => {
|
||||
if (message === undefined || typeof message === "string") {
|
||||
|
|
@ -119,11 +68,7 @@ const getErrorMessage = (
|
|||
return message(formData);
|
||||
};
|
||||
|
||||
export const generateFormValidation = (
|
||||
formData: IAddSoftwareFormData,
|
||||
showingPreInstallCondition: boolean,
|
||||
showingPostInstallScript: boolean
|
||||
) => {
|
||||
export const generateFormValidation = (formData: IAddPackageFormData) => {
|
||||
const formValidation: IFormValidation = {
|
||||
isValid: true,
|
||||
software: {
|
||||
|
|
@ -134,12 +79,7 @@ export const generateFormValidation = (
|
|||
Object.keys(FORM_VALIDATION_CONFIG).forEach((key) => {
|
||||
const objKey = key as keyof typeof FORM_VALIDATION_CONFIG;
|
||||
const failedValidation = FORM_VALIDATION_CONFIG[objKey].validations.find(
|
||||
(validation) =>
|
||||
!validation.isValid(
|
||||
formData,
|
||||
showingPreInstallCondition,
|
||||
showingPostInstallScript
|
||||
)
|
||||
(validation) => !validation.isValid(formData)
|
||||
);
|
||||
|
||||
if (!failedValidation) {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from "./AddSoftwareForm";
|
||||
export { default } from "./AddPackageForm";
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
import Editor from "components/Editor";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import FleetAce from "components/FleetAce";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
|
||||
const baseClass = "add-software-advanced-options";
|
||||
|
||||
interface IAddSoftwareAdvancedOptionsProps {
|
||||
errors: { preInstallCondition?: string; postInstallScript?: string };
|
||||
showPreInstallCondition: boolean;
|
||||
showPostInstallScript: boolean;
|
||||
preInstallCondition?: string;
|
||||
postInstallScript?: string;
|
||||
onTogglePreInstallCondition: (value: boolean) => void;
|
||||
onTogglePostInstallScript: (value: boolean) => void;
|
||||
onChangePreInstallCondition: (value?: string) => void;
|
||||
onChangePostInstallScript: (value?: string) => void;
|
||||
}
|
||||
|
||||
const AddSoftwareAdvancedOptions = ({
|
||||
errors,
|
||||
showPreInstallCondition,
|
||||
showPostInstallScript,
|
||||
preInstallCondition,
|
||||
postInstallScript,
|
||||
onTogglePreInstallCondition,
|
||||
onTogglePostInstallScript,
|
||||
onChangePreInstallCondition,
|
||||
onChangePostInstallScript,
|
||||
}: IAddSoftwareAdvancedOptionsProps) => {
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const onChangePreInstallCheckbox = () => {
|
||||
onTogglePreInstallCondition(!showPreInstallCondition);
|
||||
};
|
||||
|
||||
const onChangePostInstallCheckbox = () => {
|
||||
onTogglePostInstallScript(!showPostInstallScript);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<RevealButton
|
||||
className={`${baseClass}__accordion-title`}
|
||||
isShowing={showAdvancedOptions}
|
||||
showText="Advanced options"
|
||||
hideText="Advanced options"
|
||||
caretPosition="after"
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<div className={`${baseClass}__input-fields`}>
|
||||
<Checkbox
|
||||
value={showPreInstallCondition}
|
||||
onChange={onChangePreInstallCheckbox}
|
||||
>
|
||||
Pre-install condition
|
||||
</Checkbox>
|
||||
{showPreInstallCondition && (
|
||||
<FleetAce
|
||||
focus
|
||||
error={errors.preInstallCondition}
|
||||
value={preInstallCondition}
|
||||
label="Query"
|
||||
name="preInstallQuery"
|
||||
maxLines={10}
|
||||
onChange={onChangePreInstallCondition}
|
||||
helpText={
|
||||
<>
|
||||
Software will be installed only if the{" "}
|
||||
<CustomLink
|
||||
className={`${baseClass}__table-link`}
|
||||
text="query returns results"
|
||||
url="https://fleetdm.com/tables"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
value={showPostInstallScript}
|
||||
onChange={onChangePostInstallCheckbox}
|
||||
>
|
||||
Post-install script
|
||||
</Checkbox>
|
||||
{showPostInstallScript && (
|
||||
<>
|
||||
<Editor
|
||||
focus
|
||||
error={errors.postInstallScript}
|
||||
wrapEnabled
|
||||
name="post-install-script-editor"
|
||||
maxLines={10}
|
||||
onChange={onChangePostInstallScript}
|
||||
value={postInstallScript}
|
||||
helpText="Shell (macOS and Linux) or PowerShell (Windows)."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddSoftwareAdvancedOptions;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./AddSoftwareAdvancedOptions";
|
||||
|
|
@ -35,14 +35,14 @@ const EnableVppCard = () => {
|
|||
<Card borderRadiusSize="medium">
|
||||
<div className={`${baseClass}__enable-vpp`}>
|
||||
<p className={`${baseClass}__enable-vpp-title`}>
|
||||
<b>Volume Purchasing Program (VPP) isn't enabled</b>
|
||||
<b>No Volume Purchasing Program (VPP) token assigned</b>
|
||||
</p>
|
||||
<p className={`${baseClass}__enable-vpp-description`}>
|
||||
To add App Store apps, first add VPP.
|
||||
To add App Store apps, assign a VPP token to this team.
|
||||
</p>
|
||||
<CustomLink
|
||||
url={PATHS.ADMIN_INTEGRATIONS_VPP}
|
||||
text="Add VPP"
|
||||
text="Edit VPP"
|
||||
className={`${baseClass}__enable-vpp-link`}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -57,9 +57,8 @@ const NoVppAppsCard = () => (
|
|||
You don't have any App Store apps
|
||||
</p>
|
||||
<p className={`${baseClass}__no-software-description`}>
|
||||
Add apps in{" "}
|
||||
<CustomLink url="https://business.apple.com" text="ABM" newTab /> Apps
|
||||
that are already added to this team are not listed.
|
||||
You must purchase apps in ABM. App Store apps that are already added to
|
||||
this team are not listed.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
&__no-software-description {
|
||||
margin: 0;
|
||||
color: $ui-fleet-black-75;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__error {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ interface IMdmSettingsProps {
|
|||
const MdmSettings = ({ router }: IMdmSettingsProps) => {
|
||||
const { isPremiumTier, config } = useContext(AppContext);
|
||||
|
||||
const isMdmEnabled = !!config?.mdm.enabled_and_configured;
|
||||
|
||||
// Currently the status of this API call is what determines various UI states on
|
||||
// this page. Because of this we will not render any of this components UI until this API
|
||||
// call has completed.
|
||||
|
|
@ -48,7 +50,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => {
|
|||
// we're fetching and setting the config, but for now we'll just assume that any 400 response
|
||||
// means that MDM is not enabled and we'll show the "Turn on MDM" button.
|
||||
staleTime: 5000,
|
||||
enabled: !!config?.mdm.enabled_and_configured,
|
||||
enabled: isMdmEnabled,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -63,7 +65,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => {
|
|||
{
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
retry: false,
|
||||
enabled: isPremiumTier && !!config?.mdm.enabled_and_configured,
|
||||
enabled: isPremiumTier && isMdmEnabled,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -80,7 +82,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => {
|
|||
{
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
retry: false,
|
||||
enabled: isPremiumTier && !!config?.mdm.enabled_and_configured,
|
||||
enabled: isPremiumTier && isMdmEnabled,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -104,7 +106,7 @@ const MdmSettings = ({ router }: IMdmSettingsProps) => {
|
|||
|
||||
// we use this to determine if we have all the data we need to render the UI.
|
||||
// Notice that we do not need VPP or EULA data to render this page.
|
||||
const hasAllData = !!APNSInfo;
|
||||
const hasAllData = !isMdmEnabled || !!APNSInfo;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const getErrorMessage = (err: unknown) => {
|
|||
reasonIncludes: "Duplicate entry",
|
||||
});
|
||||
const invalidTokenReason = getErrorReason(err, {
|
||||
reasonIncludes: "invalid",
|
||||
reasonIncludes: "Invalid token",
|
||||
});
|
||||
|
||||
if (duplicateEntryReason) {
|
||||
|
|
@ -28,7 +28,7 @@ export const getErrorMessage = (err: unknown) => {
|
|||
}
|
||||
|
||||
if (invalidTokenReason) {
|
||||
return "Invalid token. Please provide a valid token from Apple Business Manager.";
|
||||
return invalidTokenReason;
|
||||
}
|
||||
|
||||
return DEFAULT_ERROR_MESSAGE;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import ReactTooltip from "react-tooltip";
|
|||
import { uniqueId } from "lodash";
|
||||
|
||||
import { IHostSoftware, SoftwareInstallStatus } from "interfaces/software";
|
||||
import { dateAgo } from "utilities/date_format";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
|
|
@ -14,6 +13,7 @@ const baseClass = "install-status-cell";
|
|||
type IStatusValue = SoftwareInstallStatus | "avaiableForInstall";
|
||||
interface TootipArgs {
|
||||
softwareName?: string | null;
|
||||
// this field is used in My device > Self-service
|
||||
lastInstalledAt?: string;
|
||||
isAppStoreApp?: boolean;
|
||||
}
|
||||
|
|
@ -36,26 +36,23 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record<
|
|||
installed: {
|
||||
iconName: "success",
|
||||
displayText: "Installed",
|
||||
tooltip: ({ lastInstalledAt: lastInstall }) => (
|
||||
<>
|
||||
Fleet installed software on this host ({dateAgo(lastInstall as string)}
|
||||
). Currently, if the software is uninstalled, the "Installed"
|
||||
status won't be updated.
|
||||
</>
|
||||
),
|
||||
tooltip: () =>
|
||||
"Software is installed (install script finished with exit code 0).",
|
||||
},
|
||||
pending: {
|
||||
iconName: "pending-outline",
|
||||
displayText: "Pending",
|
||||
tooltip: () => "Fleet will install software when the host comes online.",
|
||||
tooltip: () =>
|
||||
"Fleet is installing or will install when the host comes online.",
|
||||
},
|
||||
failed: {
|
||||
iconName: "error",
|
||||
displayText: "Failed",
|
||||
tooltip: ({ lastInstalledAt: lastInstall }) => (
|
||||
tooltip: () => (
|
||||
<>
|
||||
Fleet failed to install software ({dateAgo(lastInstall as string)} ago).
|
||||
Select <b>Actions > Software details</b> to see more.
|
||||
The host failed to install software. To view errors, select
|
||||
<br />
|
||||
<b>Actions > Show details</b>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
@ -96,10 +93,6 @@ const InstallStatusCell = ({
|
|||
app_store_app,
|
||||
}: IInstallStatusCellProps) => {
|
||||
// FIXME: Improve the way we handle polymophism of software_package and app_store_app
|
||||
const lastInstalledAt =
|
||||
software_package?.last_install?.installed_at ||
|
||||
app_store_app?.last_install?.installed_at ||
|
||||
"";
|
||||
const hasPackage = !!software_package;
|
||||
const hasAppStoreApp = !!app_store_app;
|
||||
|
||||
|
|
@ -140,7 +133,6 @@ const InstallStatusCell = ({
|
|||
<span className={`${baseClass}__status-tooltip-text`}>
|
||||
{displayConfig.tooltip({
|
||||
softwareName,
|
||||
lastInstalledAt,
|
||||
isAppStoreApp: hasAppStoreApp,
|
||||
})}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ describe("SelfService", () => {
|
|||
).toHaveTextContent("Install");
|
||||
});
|
||||
|
||||
it("renders no action button with 'Install in progress...' status", async () => {
|
||||
it("renders no action button with 'Pending' status", async () => {
|
||||
mockServer.use(
|
||||
customDeviceSoftwareHandler({
|
||||
software: [
|
||||
|
|
@ -186,7 +186,7 @@ describe("SelfService", () => {
|
|||
|
||||
expect(
|
||||
screen.getByTestId("self-service-item__status--test")
|
||||
).toHaveTextContent("Install in progress...");
|
||||
).toHaveTextContent("Pending");
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("self-service-item__item-action-button--test")
|
||||
|
|
|
|||
|
|
@ -25,25 +25,20 @@ const STATUS_CONFIG: Record<SoftwareInstallStatus, IStatusDisplayConfig> = {
|
|||
installed: {
|
||||
iconName: "success",
|
||||
displayText: "Installed",
|
||||
tooltip: ({ lastInstalledAt }) => (
|
||||
<>
|
||||
Software installed successfully ({dateAgo(lastInstalledAt as string)}).
|
||||
Currently, if the software is uninstalled, the "Installed"
|
||||
status won't be updated.
|
||||
</>
|
||||
),
|
||||
tooltip: ({ lastInstalledAt }) =>
|
||||
`Software is installed (${dateAgo(lastInstalledAt as string)}).`,
|
||||
},
|
||||
pending: {
|
||||
iconName: "pending-outline",
|
||||
displayText: "Install in progress...",
|
||||
tooltip: () => "Software installation in progress...",
|
||||
displayText: "Pending",
|
||||
tooltip: () => "Fleet is installing software.",
|
||||
},
|
||||
failed: {
|
||||
iconName: "error",
|
||||
displayText: "Failed",
|
||||
tooltip: ({ lastInstalledAt = "" }) => (
|
||||
<>
|
||||
Software failed to install
|
||||
Software failed to install{" "}
|
||||
{lastInstalledAt ? ` (${dateAgo(lastInstalledAt)})` : ""}. Select{" "}
|
||||
<b>Retry</b> to install again, or contact your IT department.
|
||||
</>
|
||||
|
|
@ -144,7 +139,6 @@ const getInstallButtonText = (status: SoftwareInstallStatus | null) => {
|
|||
case "installed":
|
||||
return "Reinstall";
|
||||
default:
|
||||
// we don't show a button for pending installs
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
|
@ -165,10 +159,7 @@ const InstallerStatusAction = ({
|
|||
SoftwareInstallStatus | undefined
|
||||
>(undefined);
|
||||
|
||||
// displayStatus allows us to display the localStatus (if any) or the status from the list
|
||||
// software reponse
|
||||
const displayStatus = localStatus || status;
|
||||
const installButtonText = getInstallButtonText(displayStatus);
|
||||
const installButtonText = getInstallButtonText(status);
|
||||
|
||||
// if the localStatus is "failed", we don't want our tooltip to include the old installed_at date so we
|
||||
// set this to null, which tells the tooltip to omit the parenthetical date
|
||||
|
|
@ -200,21 +191,16 @@ const InstallerStatusAction = ({
|
|||
return (
|
||||
<div className={`${baseClass}__item-status-action`}>
|
||||
<div className={`${baseClass}__item-status`}>
|
||||
<InstallerStatus
|
||||
id={id}
|
||||
status={displayStatus}
|
||||
last_install={lastInstall}
|
||||
/>
|
||||
<InstallerStatus id={id} status={status} last_install={lastInstall} />
|
||||
</div>
|
||||
<div className={`${baseClass}__item-action`}>
|
||||
{!!installButtonText && (
|
||||
<Button
|
||||
variant="text-icon"
|
||||
type="button"
|
||||
className={`${baseClass}__item-action-button${
|
||||
localStatus === "pending" ? "--installing" : ""
|
||||
}`}
|
||||
className={`${baseClass}__item-action-button`}
|
||||
onClick={onClick}
|
||||
disabled={localStatus === "pending"}
|
||||
>
|
||||
<span data-testid={`${baseClass}__item-action-button--test`}>
|
||||
{installButtonText}
|
||||
|
|
|
|||
|
|
@ -87,9 +87,5 @@
|
|||
|
||||
&__item-action-button {
|
||||
height: auto;
|
||||
|
||||
&--installing {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ import AddPolicyModal from "./components/AddPolicyModal";
|
|||
import DeletePolicyModal from "./components/DeletePolicyModal";
|
||||
import CalendarEventsModal from "./components/CalendarEventsModal";
|
||||
import { ICalendarEventsFormData } from "./components/CalendarEventsModal/CalendarEventsModal";
|
||||
import InstallSoftwareModal from "./components/InstallSoftwareModal";
|
||||
import { IInstallSoftwareFormData } from "./components/InstallSoftwareModal/InstallSoftwareModal";
|
||||
|
||||
interface IManagePoliciesPageProps {
|
||||
router: InjectedRouter;
|
||||
|
|
@ -127,12 +129,19 @@ const ManagePolicyPage = ({
|
|||
const [isUpdatingCalendarEvents, setIsUpdatingCalendarEvents] = useState(
|
||||
false
|
||||
);
|
||||
const [
|
||||
isUpdatingPolicySoftwareInstall,
|
||||
setIsUpdatingPolicySoftwareInstall,
|
||||
] = useState(false);
|
||||
const [isUpdatingOtherWorkflows, setIsUpdatingOtherWorkflows] = useState(
|
||||
false
|
||||
);
|
||||
const [selectedPolicyIds, setSelectedPolicyIds] = useState<number[]>([]);
|
||||
const [showAddPolicyModal, setShowAddPolicyModal] = useState(false);
|
||||
const [showDeletePolicyModal, setShowDeletePolicyModal] = useState(false);
|
||||
const [showInstallSoftwareModal, setShowInstallSoftwareModal] = useState(
|
||||
false
|
||||
);
|
||||
const [showCalendarEventsModal, setShowCalendarEventsModal] = useState(false);
|
||||
const [showOtherWorkflowsModal, setShowOtherWorkflowsModal] = useState(false);
|
||||
const [
|
||||
|
|
@ -438,6 +447,10 @@ const ManagePolicyPage = ({
|
|||
const toggleDeletePolicyModal = () =>
|
||||
setShowDeletePolicyModal(!showDeletePolicyModal);
|
||||
|
||||
const toggleInstallSoftwareModal = () => {
|
||||
setShowInstallSoftwareModal(!showInstallSoftwareModal);
|
||||
};
|
||||
|
||||
const toggleCalendarEventsModal = () => {
|
||||
setShowCalendarEventsModal(!showCalendarEventsModal);
|
||||
};
|
||||
|
|
@ -447,6 +460,9 @@ const ManagePolicyPage = ({
|
|||
case "calendar_events":
|
||||
toggleCalendarEventsModal();
|
||||
break;
|
||||
case "install_software":
|
||||
toggleInstallSoftwareModal();
|
||||
break;
|
||||
case "other_workflows":
|
||||
toggleOtherWorkflowsModal();
|
||||
break;
|
||||
|
|
@ -476,6 +492,63 @@ const ManagePolicyPage = ({
|
|||
}
|
||||
};
|
||||
|
||||
const onUpdatePolicySoftwareInstall = async (
|
||||
formData: IInstallSoftwareFormData
|
||||
) => {
|
||||
try {
|
||||
setIsUpdatingPolicySoftwareInstall(true);
|
||||
const changedPolicies = formData.filter((formPolicy) => {
|
||||
const prevPolicyState = policiesAvailableToAutomate.find(
|
||||
(policy) => policy.id === formPolicy.id
|
||||
);
|
||||
|
||||
const turnedOff =
|
||||
prevPolicyState?.install_software !== undefined &&
|
||||
formPolicy.installSoftwareEnabled === false;
|
||||
|
||||
const turnedOn =
|
||||
prevPolicyState?.install_software === undefined &&
|
||||
formPolicy.installSoftwareEnabled === true;
|
||||
|
||||
const updatedSwId =
|
||||
prevPolicyState?.install_software?.software_title_id !== undefined &&
|
||||
formPolicy.swIdToInstall !==
|
||||
prevPolicyState?.install_software?.software_title_id;
|
||||
|
||||
return turnedOff || turnedOn || updatedSwId;
|
||||
});
|
||||
if (!changedPolicies.length) {
|
||||
renderFlash("success", "No changes detected.");
|
||||
return;
|
||||
}
|
||||
const responses: Promise<
|
||||
ReturnType<typeof teamPoliciesAPI.update>
|
||||
>[] = [];
|
||||
responses.concat(
|
||||
changedPolicies.map((changedPolicy) => {
|
||||
return teamPoliciesAPI.update(changedPolicy.id, {
|
||||
// "software_title_id:" 0 will unset software install for the policy
|
||||
// "software_title_id": X will set the value to the given integer (except 0).
|
||||
software_title_id: changedPolicy.swIdToInstall || 0,
|
||||
team_id: teamIdForApi,
|
||||
});
|
||||
})
|
||||
);
|
||||
await Promise.all(responses);
|
||||
await wait(100); // prevent race
|
||||
refetchTeamPolicies();
|
||||
renderFlash("success", "Successfully updated policy automations.");
|
||||
} catch {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Could not update policy automations. Please try again."
|
||||
);
|
||||
} finally {
|
||||
toggleInstallSoftwareModal();
|
||||
setIsUpdatingPolicySoftwareInstall(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateCalendarEvents = async (formData: ICalendarEventsFormData) => {
|
||||
setIsUpdatingCalendarEvents(true);
|
||||
|
||||
|
|
@ -698,11 +771,20 @@ const ManagePolicyPage = ({
|
|||
|
||||
const getAutomationsDropdownOptions = () => {
|
||||
const isAllTeams = teamIdForApi === undefined || teamIdForApi === -1;
|
||||
let disabledTooltipContent: React.ReactNode;
|
||||
let disabledInstallTooltipContent: React.ReactNode;
|
||||
let disabledCalendarTooltipContent: React.ReactNode;
|
||||
if (!isPremiumTier) {
|
||||
disabledTooltipContent = "Available in Fleet Premium.";
|
||||
disabledInstallTooltipContent = "Available in Fleet Premium.";
|
||||
disabledCalendarTooltipContent = "Available in Fleet Premium.";
|
||||
} else if (isAllTeams) {
|
||||
disabledTooltipContent = (
|
||||
disabledInstallTooltipContent = (
|
||||
<>
|
||||
Select a team to manage
|
||||
<br />
|
||||
install software automation.
|
||||
</>
|
||||
);
|
||||
disabledCalendarTooltipContent = (
|
||||
<>
|
||||
Select a team to manage
|
||||
<br />
|
||||
|
|
@ -715,9 +797,16 @@ const ManagePolicyPage = ({
|
|||
{
|
||||
label: "Calendar events",
|
||||
value: "calendar_events",
|
||||
disabled: !isPremiumTier || isAllTeams,
|
||||
disabled: !!disabledCalendarTooltipContent,
|
||||
helpText: "Automatically reserve time to resolve failing policies.",
|
||||
tooltipContent: disabledTooltipContent,
|
||||
tooltipContent: disabledCalendarTooltipContent,
|
||||
},
|
||||
{
|
||||
label: "Install software",
|
||||
value: "install_software",
|
||||
disabled: !!disabledInstallTooltipContent,
|
||||
helpText: "Install software to resolve failing policies.",
|
||||
tooltipContent: disabledInstallTooltipContent,
|
||||
},
|
||||
{
|
||||
label: "Other workflows",
|
||||
|
|
@ -816,6 +905,16 @@ const ManagePolicyPage = ({
|
|||
onSubmit={onDeletePolicySubmit}
|
||||
/>
|
||||
)}
|
||||
{showInstallSoftwareModal && (
|
||||
<InstallSoftwareModal
|
||||
onExit={toggleInstallSoftwareModal}
|
||||
onSubmit={onUpdatePolicySoftwareInstall}
|
||||
isUpdating={isUpdatingPolicySoftwareInstall}
|
||||
policies={policiesAvailableToAutomate}
|
||||
// currentTeamId will at this point be present
|
||||
teamId={currentTeamId ?? 0}
|
||||
/>
|
||||
)}
|
||||
{showCalendarEventsModal && (
|
||||
<CalendarEventsModal
|
||||
onExit={toggleCalendarEventsModal}
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Used in both CalendarEventsModal and OtherWorkflowsModal
|
||||
// Used in CalendarEventsModal, InstallSoftwareModal, and OtherWorkflowsModal
|
||||
.automated-policies-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -228,8 +228,10 @@
|
|||
align-self: stretch;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
// negate ul padding
|
||||
padding-left: 0;
|
||||
|
||||
.checkbox-row {
|
||||
.policy-row {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
padding: 8px 12px;
|
||||
|
|
@ -270,7 +272,7 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-row__preview-button {
|
||||
.policy-row__preview-button {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,11 +185,11 @@ const CalendarEventsModal = ({
|
|||
return (
|
||||
<div className="form-field">
|
||||
<div className="form-field__label">Policies:</div>
|
||||
<div className="automated-policies-section">
|
||||
<ul className="automated-policies-section">
|
||||
{formData.policies.map((policy) => {
|
||||
const { isChecked, name, id } = policy;
|
||||
return (
|
||||
<div className="checkbox-row" id={`checkbox-row--${id}`} key={id}>
|
||||
<li className="policy-row" id={`policy-row--${id}`} key={id}>
|
||||
<Checkbox
|
||||
value={isChecked}
|
||||
name={name}
|
||||
|
|
@ -208,14 +208,14 @@ const CalendarEventsModal = ({
|
|||
);
|
||||
togglePreviewCalendarEvent();
|
||||
}}
|
||||
className="checkbox-row__preview-button"
|
||||
className="policy-row__preview-button"
|
||||
>
|
||||
<Icon name="eye" /> Preview
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
<span className="form-field__help-text">
|
||||
A calendar event will be created for end users if one of their hosts
|
||||
fail any of these policies.{" "}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,276 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { useQuery } from "react-query";
|
||||
import { omit } from "lodash";
|
||||
|
||||
import { IPolicyStats } from "interfaces/policy";
|
||||
import softwareAPI, {
|
||||
ISoftwareTitlesQueryKey,
|
||||
ISoftwareTitlesResponse,
|
||||
} from "services/entities/software";
|
||||
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
||||
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import Modal from "components/Modal";
|
||||
import DataError from "components/DataError";
|
||||
import Spinner from "components/Spinner";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Button from "components/buttons/Button";
|
||||
import { ISoftwareTitle } from "interfaces/software";
|
||||
|
||||
const getPlatformDisplayFromPackageSuffix = (packageName: string) => {
|
||||
const split = packageName.split(".");
|
||||
const suff = split[split.length - 1];
|
||||
switch (suff) {
|
||||
case "pkg":
|
||||
return "macOS";
|
||||
case "deb":
|
||||
return "Linux";
|
||||
case "exe":
|
||||
return "Windows";
|
||||
case "msi":
|
||||
return "Windows";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const AFI_SOFTWARE_BATCH_SIZE = 1000;
|
||||
|
||||
const baseClass = "install-software-modal";
|
||||
|
||||
interface ISwDropdownField {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
interface IFormPolicy {
|
||||
name: string;
|
||||
id: number;
|
||||
installSoftwareEnabled: boolean;
|
||||
swIdToInstall?: number;
|
||||
}
|
||||
|
||||
export type IInstallSoftwareFormData = IFormPolicy[];
|
||||
|
||||
interface IInstallSoftwareModal {
|
||||
onExit: () => void;
|
||||
onSubmit: (formData: IInstallSoftwareFormData) => void;
|
||||
isUpdating: boolean;
|
||||
policies: IPolicyStats[];
|
||||
teamId: number;
|
||||
}
|
||||
const InstallSoftwareModal = ({
|
||||
onExit,
|
||||
onSubmit,
|
||||
isUpdating,
|
||||
policies,
|
||||
teamId,
|
||||
}: IInstallSoftwareModal) => {
|
||||
const [formData, setFormData] = useState<IInstallSoftwareFormData>(
|
||||
policies.map((policy) => ({
|
||||
name: policy.name,
|
||||
id: policy.id,
|
||||
installSoftwareEnabled: !!policy.install_software,
|
||||
swIdToInstall: policy.install_software?.software_title_id,
|
||||
}))
|
||||
);
|
||||
|
||||
const anyPolicyEnabledWithoutSelectedSoftware = formData.some(
|
||||
(policy) => policy.installSoftwareEnabled && !policy.swIdToInstall
|
||||
);
|
||||
const {
|
||||
data: titlesAFI,
|
||||
isLoading: isTitlesAFILoading,
|
||||
isError: isTitlesAFIError,
|
||||
} = useQuery<
|
||||
ISoftwareTitlesResponse,
|
||||
Error,
|
||||
ISoftwareTitle[],
|
||||
[ISoftwareTitlesQueryKey]
|
||||
>(
|
||||
[
|
||||
{
|
||||
scope: "software-titles",
|
||||
page: 0,
|
||||
perPage: AFI_SOFTWARE_BATCH_SIZE,
|
||||
query: "",
|
||||
orderDirection: "desc",
|
||||
orderKey: "hosts_count",
|
||||
teamId,
|
||||
availableForInstall: true,
|
||||
packagesOnly: true,
|
||||
},
|
||||
],
|
||||
({ queryKey: [queryKey] }) =>
|
||||
softwareAPI.getSoftwareTitles(omit(queryKey, "scope")),
|
||||
{
|
||||
select: (data) => data.software_titles,
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
}
|
||||
);
|
||||
|
||||
const onUpdateInstallSoftware = useCallback(() => {
|
||||
onSubmit(formData);
|
||||
}, [formData, onSubmit]);
|
||||
|
||||
const onChangeEnableInstallSoftware = useCallback(
|
||||
(newVal: { policyName: string; value: boolean }) => {
|
||||
const { policyName, value } = newVal;
|
||||
setFormData(
|
||||
formData.map((policy) => {
|
||||
if (policy.name === policyName) {
|
||||
return {
|
||||
...policy,
|
||||
installSoftwareEnabled: value,
|
||||
swIdToInstall: value ? policy.swIdToInstall : undefined,
|
||||
};
|
||||
}
|
||||
return policy;
|
||||
})
|
||||
);
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
const onSelectPolicySoftware = useCallback(
|
||||
({ name, value }: ISwDropdownField) => {
|
||||
const [policyName, softwareId] = [name, value];
|
||||
setFormData(
|
||||
formData.map((policy) => {
|
||||
if (policy.name === policyName) {
|
||||
return { ...policy, swIdToInstall: softwareId };
|
||||
}
|
||||
return policy;
|
||||
})
|
||||
);
|
||||
},
|
||||
[formData]
|
||||
);
|
||||
|
||||
const availableSoftwareOptions = titlesAFI?.map((title) => {
|
||||
const platformDisplay = getPlatformDisplayFromPackageSuffix(
|
||||
title.software_package?.name ?? ""
|
||||
);
|
||||
const platformString = platformDisplay ? `${platformDisplay} • ` : "";
|
||||
return {
|
||||
label: title.name,
|
||||
value: title.id,
|
||||
helpText: `${platformString}${title.software_package?.version ?? ""}`,
|
||||
};
|
||||
});
|
||||
|
||||
const renderPolicySwInstallOption = (policy: IFormPolicy) => {
|
||||
const {
|
||||
name: policyName,
|
||||
id: policyId,
|
||||
installSoftwareEnabled: enabled,
|
||||
swIdToInstall,
|
||||
} = policy;
|
||||
|
||||
return (
|
||||
<li
|
||||
className={`${baseClass}__policy-row policy-row`}
|
||||
id={`policy-row--${policyId}`}
|
||||
key={policyId}
|
||||
>
|
||||
<Checkbox
|
||||
value={enabled}
|
||||
name={policyName}
|
||||
onChange={() => {
|
||||
onChangeEnableInstallSoftware({
|
||||
policyName,
|
||||
value: !enabled,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TooltipTruncatedText value={policyName} />
|
||||
</Checkbox>
|
||||
{enabled && (
|
||||
<Dropdown
|
||||
options={availableSoftwareOptions}
|
||||
value={swIdToInstall}
|
||||
onChange={onSelectPolicySoftware}
|
||||
placeholder="Select software"
|
||||
className={`${baseClass}__software-dropdown`}
|
||||
name={policyName}
|
||||
parseTarget
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (isTitlesAFIError) {
|
||||
return <DataError />;
|
||||
}
|
||||
if (isTitlesAFILoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
if (!titlesAFI?.length) {
|
||||
return (
|
||||
<div className={`${baseClass}__no-software`}>
|
||||
<b>No software available for install</b>
|
||||
<span>
|
||||
Go to <b>Software</b> to add software to this team.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} form`}>
|
||||
<div className="form-field">
|
||||
<div className="form-field__label">Policies:</div>
|
||||
<ul className="automated-policies-section">
|
||||
{formData.map((policyData) =>
|
||||
renderPolicySwInstallOption(policyData)
|
||||
)}
|
||||
</ul>
|
||||
<span className="form-field__help-text">
|
||||
Selected software will be installed when hosts fail the chosen
|
||||
policy.{" "}
|
||||
<CustomLink
|
||||
url="https://fleetdm.com/learn-more-about/policy-automation-install-software"
|
||||
text="Learn more"
|
||||
newTab
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={onUpdateInstallSoftware}
|
||||
className="save-loading"
|
||||
isLoading={isUpdating}
|
||||
disabled={anyPolicyEnabledWithoutSelectedSoftware}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onExit} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Install software"
|
||||
className={baseClass}
|
||||
onExit={onExit}
|
||||
onEnter={onUpdateInstallSoftware}
|
||||
width="large"
|
||||
isContentDisabled={isUpdating}
|
||||
>
|
||||
{renderContent()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstallSoftwareModal;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
.manage-policies-page {
|
||||
.install-software-modal {
|
||||
.form-field--dropdown {
|
||||
width: 276px;
|
||||
.Select-placeholder {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
.Select-menu {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.Select-menu-outer {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
.policy-row {
|
||||
height: 40px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
&__no-software {
|
||||
display: flex;
|
||||
height: 178px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
justify-content: center;
|
||||
font-size: $small;
|
||||
|
||||
span {
|
||||
color: $ui-fleet-black-75;
|
||||
font-size: $xx-small;
|
||||
}
|
||||
}
|
||||
.data-error {
|
||||
padding: 78px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./InstallSoftwareModal";
|
||||
|
|
@ -416,8 +416,8 @@ const OtherWorkflowsModal = ({
|
|||
const { isChecked, name, id } = policyItem;
|
||||
return (
|
||||
<div
|
||||
className="checkbox-row"
|
||||
id={`checkbox-row--${id}`}
|
||||
className="policy-row"
|
||||
id={`policy-row--${id}`}
|
||||
key={id}
|
||||
>
|
||||
<Checkbox
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default {
|
|||
ADMIN_INTEGRATIONS_APPLE_BUSINESS_MANAGER: `${URL_PREFIX}/settings/integrations/mdm/abm`,
|
||||
ADMIN_INTEGRATIONS_AUTOMATIC_ENROLLMENT_WINDOWS: `${URL_PREFIX}/settings/integrations/automatic-enrollment/windows`,
|
||||
ADMIN_INTEGRATIONS_CALENDARS: `${URL_PREFIX}/settings/integrations/calendars`,
|
||||
ADMIN_INTEGRATIONS_VPP: `${URL_PREFIX}/settings/integrations/vpp`,
|
||||
ADMIN_INTEGRATIONS_VPP: `${URL_PREFIX}/settings/integrations/mdm/vpp`,
|
||||
ADMIN_INTEGRATIONS_VPP_SETUP: `${URL_PREFIX}/settings/integrations/vpp/setup`,
|
||||
|
||||
ADMIN_TEAMS: `${URL_PREFIX}/settings/teams`,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { AxiosResponse } from "axios";
|
||||
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import {
|
||||
|
|
@ -13,7 +11,7 @@ import {
|
|||
buildQueryStringFromParams,
|
||||
convertParamsToSnakeCase,
|
||||
} from "utilities/url";
|
||||
import { IAddSoftwareFormData } from "pages/SoftwarePage/components/AddPackageForm/AddSoftwareForm";
|
||||
import { IAddPackageFormData } from "pages/SoftwarePage/components/AddPackageForm/AddPackageForm";
|
||||
|
||||
export interface ISoftwareApiParams {
|
||||
page?: number;
|
||||
|
|
@ -26,6 +24,7 @@ export interface ISoftwareApiParams {
|
|||
min_cvss_score?: number;
|
||||
exploit?: boolean;
|
||||
availableForInstall?: boolean;
|
||||
packagesOnly?: boolean;
|
||||
selfService?: boolean;
|
||||
teamId?: number;
|
||||
}
|
||||
|
|
@ -206,7 +205,7 @@ export default {
|
|||
},
|
||||
|
||||
addSoftwarePackage: (
|
||||
data: IAddSoftwareFormData,
|
||||
data: IAddPackageFormData,
|
||||
teamId?: number,
|
||||
timeout?: number
|
||||
) => {
|
||||
|
|
@ -220,8 +219,8 @@ export default {
|
|||
formData.append("software", data.software);
|
||||
formData.append("self_service", data.selfService.toString());
|
||||
data.installScript && formData.append("install_script", data.installScript);
|
||||
data.preInstallCondition &&
|
||||
formData.append("pre_install_query", data.preInstallCondition);
|
||||
data.preInstallQuery &&
|
||||
formData.append("pre_install_query", data.preInstallQuery);
|
||||
data.postInstallScript &&
|
||||
formData.append("post_install_script", data.postInstallScript);
|
||||
teamId && formData.append("team_id", teamId.toString());
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export default {
|
|||
platform,
|
||||
critical,
|
||||
calendar_events_enabled,
|
||||
software_title_id,
|
||||
} = data;
|
||||
const { TEAMS } = endpoints;
|
||||
const path = `${TEAMS}/${team_id}/policies/${id}`;
|
||||
|
|
@ -98,6 +99,7 @@ export default {
|
|||
platform,
|
||||
critical,
|
||||
calendar_events_enabled,
|
||||
software_title_id,
|
||||
});
|
||||
},
|
||||
destroy: (teamId: number | undefined, ids: number[]) => {
|
||||
|
|
|
|||
|
|
@ -60,9 +60,13 @@ export const HOST_STATUS_WEBHOOK_WINDOW_DROPDOWN_OPTIONS: IDropdownOption[] = [
|
|||
export const GITHUB_NEW_ISSUE_LINK =
|
||||
"https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md";
|
||||
|
||||
export const SUPPORT_LINK = "https://fleetdm.com/support";
|
||||
export const FLEET_WEBSITE_URL = "https://fleetdm.com";
|
||||
|
||||
export const CONTACT_FLEET_LINK = "https://fleetdm.com/contact";
|
||||
export const SUPPORT_LINK = `${FLEET_WEBSITE_URL}/support`;
|
||||
|
||||
export const CONTACT_FLEET_LINK = `${FLEET_WEBSITE_URL}/contact`;
|
||||
|
||||
export const LEARN_MORE_ABOUT_BASE_LINK = `${FLEET_WEBSITE_URL}/learn-more-about`;
|
||||
|
||||
/** July 28, 2016 is the date of the initial commit to fleet/fleet. */
|
||||
export const INITIAL_FLEET_DATE = "2016-07-28T00:00:00Z";
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
task: "Prioritize for next sprint" # Title that will actually show in rituals table
|
||||
startedOn: "2023-08-09" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday
|
||||
frequency: "Triweekly" # must be supported by https://github.com/fleetdm/fleet/blob/dbbb501358e226fa3fdf48865175efe3334c826c/website/scripts/build-static-content.js
|
||||
description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)"
|
||||
description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)"
|
||||
moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table
|
||||
dri: "jostableford" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title)
|
||||
autoIssue: # Enables automation of GitHub issues
|
||||
|
|
|
|||
|
|
@ -584,6 +584,10 @@ Use the 🧩 ["Design System (current)"](https://www.figma.com/file/8oXlYXpgCV1S
|
|||
|
||||
Use `---`, with color `$ui-fleet-black-50` as the default UI for empty columns.
|
||||
|
||||
**Images**
|
||||
|
||||
Simple icons (aka any images used in the icon [design system component](https://www.figma.com/design/8oXlYXpgCV1Sn4ek7OworP/%F0%9F%A7%A9-Design-system-(current)?node-id=12-2&t=iO2vXbQ9Sc1kFVEJ-1)) are exported as SVGs. All other images are exported as PNGs, following the [Fleet website image](https://github.com/fleetdm/fleet/tree/main/website/assets/images) naming conventions.
|
||||
|
||||
**Form behavior**
|
||||
|
||||
Pressing the return or enter key with an open form will cause the form to be submitted.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
task: "Prioritize for next sprint" # Title that will actually show in rituals table
|
||||
startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday
|
||||
frequency: "Triweekly" # must be supported by
|
||||
description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)"
|
||||
description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)"
|
||||
moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table
|
||||
dri: "zayhanlon" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title)
|
||||
autoIssue: # Enables automation of GitHub issues
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
task: "Prioritize for next sprint" # Title that will actually show in rituals table
|
||||
startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday
|
||||
frequency: "Triweekly"
|
||||
description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)"
|
||||
description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)"
|
||||
moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table
|
||||
dri: "mikermcneil" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title)
|
||||
autoIssue: # « Enable automation of GitHub issues
|
||||
|
|
@ -22,6 +22,13 @@
|
|||
description: "https://fleetdm.com/handbook/demand#settle-event-strategy"
|
||||
moreInfoUrl: "https://fleetdm.com/handbook/demand#settle-event-strategy"
|
||||
dri: "Drew-P-drawers"
|
||||
-
|
||||
task: "🫧 Pipeline sync"
|
||||
startedOn: "2024-08-29"
|
||||
frequency: "Weekly"
|
||||
description: "Allign with CRO and AEs on pipeline processes and incoming leads"
|
||||
moreInfoUrl: ""
|
||||
dri: "Drew-P-drawers"
|
||||
-
|
||||
task: "Optimize ads"
|
||||
startedOn: "2024-02-26"
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
task: "Prioritize for next sprint" # Title that will actually show in rituals table
|
||||
startedOn: "2023-08-09" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday
|
||||
frequency: "Triweekly" # must be supported by https://github.com/fleetdm/fleet/blob/dbbb501358e226fa3fdf48865175efe3334c826c/website/scripts/build-static-content.js
|
||||
description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)"
|
||||
description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/digital-experiencemunication)"
|
||||
moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table
|
||||
dri: "sampfluger88" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title)
|
||||
autoIssue: # Enables automation of GitHub issues
|
||||
|
|
@ -123,8 +123,8 @@
|
|||
repo: "confidential"
|
||||
-
|
||||
task: "Process and backup Sid agenda"
|
||||
startedOn: "2023-09-15"
|
||||
frequency: "Weekly"
|
||||
startedOn: "2023-09-25"
|
||||
frequency: "Monthly"
|
||||
description: "Process and backup Sid agenda"
|
||||
moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#process-and-backup-e-group-agenda"
|
||||
dri: "SFriendLee"
|
||||
|
|
|
|||
|
|
@ -111,11 +111,10 @@ If there is partially merged feature work when the release candidate is created,
|
|||
Before kicking off release QA, confirm that we are using the latest versions of dependencies we want to keep up-to-date with each release. Currently, those dependencies are:
|
||||
|
||||
1. **Go**: Latest minor release
|
||||
- Check the [version included in Fleet](https://github.com/fleetdm/fleet/settings/variables/actions).
|
||||
- Check the [Go version specified in Fleet's go.mod file](https://github.com/fleetdm/fleet/blob/main/go.mod) (`go 1.XX.YY`).
|
||||
- Check the [latest minor version of Go](https://go.dev/dl/). For example, if we are using `go1.19.8`, and there is a new minor version `go1.19.9`, we will upgrade.
|
||||
- If the latest minor version is greater than the version included in Fleet, [file a bug](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&projects=&template=bug-report.md&title=) and assign it to the [release ritual DRI](https://fleetdm.com/handbook/engineering#rituals) and the current oncall engineer. Add the `~release blocker` label. We must upgrade to the latest minor version before publishing the next release.
|
||||
- If the latest major version is greater than the version included in Fleet, [create a story](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=story%2C%3Aproduct&projects=&template=story.md&title=) and assign it to the [release ritual DRI](https://fleetdm.com/handbook/engineering#rituals) and the current oncall engineer. This will be considered for an upcoming sprint. The release can proceed without upgrading the major version.
|
||||
- Note that major version upgrades also require an [update to go.mod](https://github.com/fleetdm/fleet/blob/7b3134498873a31ba748ca27fabb0059cef70db9/go.mod#L3).
|
||||
|
||||
> In Go versioning, the number after the first dot is the "major" version, while the number after the second dot is the "minor" version. For example, in Go 1.19.9, "19" is the major version and "9" is the minor version. Major version upgrades are assessed separately by engineering.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
task: "Prioritize for next sprint" # Title that will actually show in rituals table
|
||||
startedOn: "2023-09-04" # Needs to align with frequency e.g. if frequency is every thrid Thursday startedOn === any third thursday
|
||||
frequency: "Triweekly" # must be supported by
|
||||
description: "Drag next sprint's priorities to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)"
|
||||
description: "Using your departmental kanban board, prioritize and finalize next sprint's goals for your team by draging the appropriate issues to the top of the 'Not yet' column." # example of a longer thing: description: "[Prioritizing next sprint](https://fleetdm.com/handbook/company/communication)"
|
||||
moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table
|
||||
dri: "alexmitchelliii" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title)
|
||||
autoIssue: # Enables automation of GitHub issues
|
||||
|
|
|
|||
|
|
@ -5349,7 +5349,7 @@
|
|||
},
|
||||
{
|
||||
"name": "cryptoinfo",
|
||||
"description": "Get info about the a certificate on the host.",
|
||||
"description": "Get info about a certificate on the host.",
|
||||
"evented": false,
|
||||
"notes": "This table is not a core osquery table. It is included as part of fleetd, the osquery manager from Fleet. Code based on work by [Kolide](https://github.com/kolide/launcher).",
|
||||
"platforms": [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
name: cryptoinfo
|
||||
description: Get info about the a certificate on the host.
|
||||
description: Get info about a certificate on the host.
|
||||
evented: false
|
||||
notes: This table is not a core osquery table. It is included as part of fleetd, the osquery manager from Fleet. Code based on work by [Kolide](https://github.com/kolide/launcher).
|
||||
platforms:
|
||||
|
|
|
|||
26
server/docs/patterns.md
Normal file
26
server/docs/patterns.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Backend patterns
|
||||
|
||||
The backend software patterns that we follow in Fleet.
|
||||
|
||||
> NOTE: There are always exceptions to the rules, but we try to follow these patterns as much as possible unless a specific use case calls
|
||||
> for something else. These should be discussed within the team and documented before merging.
|
||||
|
||||
## MySQL
|
||||
|
||||
Use high precision for all time fields. Precise timestamps make sure that we can accurately track when records were created and updated,
|
||||
keep records in order with a reliable sort, and speed up testing by not having to wait for the time to
|
||||
update. [MySQL reference](https://dev.mysql.com/doc/refman/8.4/en/date-and-time-type-syntax.html). [Backend sync where discussed](https://us-65885.app.gong.io/call?id=8041045095900447703).
|
||||
Example:
|
||||
|
||||
```sql
|
||||
CREATE TABLE `sample` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`created_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
`updated_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
```
|
||||
|
||||
Do not use [goqu](https://github.com/doug-martin/goqu); use MySQL queries directly. Searching for, understanding, and debugging direct MySQL
|
||||
queries is easier. If needing to modify an existing `goqu` query, try to rewrite it in
|
||||
MySQL. [Backend sync where discussed](https://us-65885.app.gong.io/call?id=8041045095900447703).
|
||||
|
|
@ -3,7 +3,6 @@ package fleet
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
|
@ -791,48 +790,6 @@ type TeamTuple struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ExtractToken extracts the metadata from the token as stored in the database,
|
||||
// and returns the raw token that can be used directly with Apple's VPP API. If
|
||||
// while extracting the token it notices that the metadata has changed, it will
|
||||
// update t and return true as second return value, indicating that it changed
|
||||
// and should be saved.
|
||||
func (t *VPPTokenDB) ExtractToken() (rawAppleToken string, didUpdateMetadata bool, err error) {
|
||||
var vppTokenData VPPTokenData
|
||||
if err := json.Unmarshal([]byte(t.Token), &vppTokenData); err != nil {
|
||||
return "", false, fmt.Errorf("unmarshaling VPP token data: %w", err)
|
||||
}
|
||||
|
||||
vppTokenRawBytes, err := base64.StdEncoding.DecodeString(vppTokenData.Token)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("decoding raw vpp token data: %w", err)
|
||||
}
|
||||
|
||||
var vppTokenRaw VPPTokenRaw
|
||||
if err := json.Unmarshal(vppTokenRawBytes, &vppTokenRaw); err != nil {
|
||||
return "", false, fmt.Errorf("unmarshaling raw vpp token data: %w", err)
|
||||
}
|
||||
|
||||
exp, err := time.Parse("2006-01-02T15:04:05Z0700", vppTokenRaw.ExpDate)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("parsing vpp token expiration date: %w", err)
|
||||
}
|
||||
|
||||
if vppTokenData.Location != t.Location {
|
||||
t.Location = vppTokenData.Location
|
||||
didUpdateMetadata = true
|
||||
}
|
||||
if vppTokenRaw.OrgName != t.OrgName {
|
||||
t.OrgName = vppTokenRaw.OrgName
|
||||
didUpdateMetadata = true
|
||||
}
|
||||
if !exp.Equal(t.RenewDate) {
|
||||
t.RenewDate = exp.UTC()
|
||||
didUpdateMetadata = true
|
||||
}
|
||||
|
||||
return vppTokenRaw.Token, didUpdateMetadata, nil
|
||||
}
|
||||
|
||||
type NullTeamType string
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -383,14 +385,15 @@ func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs [
|
|||
|
||||
// Even if we didn't get an error, some of the APNs
|
||||
// responses might have failed, signal that to the caller.
|
||||
var failed []string
|
||||
failed := map[string]error{}
|
||||
for uuid, response := range apnsResponses {
|
||||
if response.Err != nil {
|
||||
failed = append(failed, uuid)
|
||||
failed[uuid] = response.Err
|
||||
}
|
||||
}
|
||||
|
||||
if len(failed) > 0 {
|
||||
return &APNSDeliveryError{FailedUUIDs: failed, Err: err}
|
||||
return &APNSDeliveryError{errorsByUUID: failed}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -399,14 +402,38 @@ func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs [
|
|||
// APNSDeliveryError records an error and the associated host UUIDs in which it
|
||||
// occurred.
|
||||
type APNSDeliveryError struct {
|
||||
FailedUUIDs []string
|
||||
Err error
|
||||
errorsByUUID map[string]error
|
||||
}
|
||||
|
||||
func (e *APNSDeliveryError) Error() string {
|
||||
return fmt.Sprintf("APNS delivery failed with: %s, for UUIDs: %v", e.Err, e.FailedUUIDs)
|
||||
var uuids []string
|
||||
for uuid := range e.errorsByUUID {
|
||||
uuids = append(uuids, uuid)
|
||||
}
|
||||
|
||||
// sort UUIDs alphabetically for deterministic output
|
||||
sort.Strings(uuids)
|
||||
|
||||
var errStrings []string
|
||||
for _, uuid := range uuids {
|
||||
errStrings = append(errStrings, fmt.Sprintf("UUID: %s, Error: %v", uuid, e.errorsByUUID[uuid]))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"APNS delivery failed with the following errors:\n%s",
|
||||
strings.Join(errStrings, "\n"),
|
||||
)
|
||||
}
|
||||
|
||||
func (e *APNSDeliveryError) Unwrap() error { return e.Err }
|
||||
func (e *APNSDeliveryError) FailedUUIDs() []string {
|
||||
var uuids []string
|
||||
for uuid := range e.errorsByUUID {
|
||||
uuids = append(uuids, uuid)
|
||||
}
|
||||
|
||||
// sort UUIDs alphabetically for deterministic output
|
||||
sort.Strings(uuids)
|
||||
return uuids
|
||||
}
|
||||
|
||||
func (e *APNSDeliveryError) StatusCode() int { return http.StatusBadGateway }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package apple_mdm
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
|
|
@ -200,3 +202,50 @@ func mobileconfigForTest(name, identifier string) []byte {
|
|||
</plist>
|
||||
`, name, identifier, uuid.New().String()))
|
||||
}
|
||||
|
||||
func TestAPNSDeliveryError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
errorsByUUID map[string]error
|
||||
expectedError string
|
||||
expectedFailedUUIDs []string
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{
|
||||
name: "single error",
|
||||
errorsByUUID: map[string]error{
|
||||
"uuid1": errors.New("network error"),
|
||||
},
|
||||
expectedError: `APNS delivery failed with the following errors:
|
||||
UUID: uuid1, Error: network error`,
|
||||
expectedFailedUUIDs: []string{"uuid1"},
|
||||
expectedStatusCode: http.StatusBadGateway,
|
||||
},
|
||||
{
|
||||
name: "multiple errors, sorted",
|
||||
errorsByUUID: map[string]error{
|
||||
"uuid3": errors.New("timeout error"),
|
||||
"uuid1": errors.New("network error"),
|
||||
"uuid2": errors.New("certificate error"),
|
||||
},
|
||||
expectedError: `APNS delivery failed with the following errors:
|
||||
UUID: uuid1, Error: network error
|
||||
UUID: uuid2, Error: certificate error
|
||||
UUID: uuid3, Error: timeout error`,
|
||||
expectedFailedUUIDs: []string{"uuid1", "uuid2", "uuid3"},
|
||||
expectedStatusCode: http.StatusBadGateway,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
apnsErr := &APNSDeliveryError{
|
||||
errorsByUUID: tt.errorsByUUID,
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expectedError, apnsErr.Error())
|
||||
require.Equal(t, tt.expectedFailedUUIDs, apnsErr.FailedUUIDs())
|
||||
require.Equal(t, tt.expectedStatusCode, apnsErr.StatusCode())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3149,7 +3149,7 @@ func SendPushesToPendingDevices(
|
|||
if err := commander.SendNotifications(ctx, uuids); err != nil {
|
||||
var apnsErr *apple_mdm.APNSDeliveryError
|
||||
if errors.As(err, &apnsErr) {
|
||||
level.Info(logger).Log("msg", "failed to send APNs notification to some hosts", "host_uuids", apnsErr.FailedUUIDs)
|
||||
level.Info(logger).Log("msg", "failed to send APNs notification to some hosts", "error", apnsErr.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/assets"
|
||||
nanomdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
|
|
@ -567,13 +566,14 @@ func (svc *Service) enqueueAppleMDMCommand(ctx context.Context, rawXMLCmd []byte
|
|||
var apnsErr *apple_mdm.APNSDeliveryError
|
||||
var mysqlErr *mysql.MySQLError
|
||||
if errors.As(err, &apnsErr) {
|
||||
if len(apnsErr.FailedUUIDs) < len(deviceIDs) {
|
||||
failedUUIDs := apnsErr.FailedUUIDs()
|
||||
if len(failedUUIDs) < len(deviceIDs) {
|
||||
// some hosts properly received the command, so return success, with the list
|
||||
// of failed uuids.
|
||||
return &fleet.CommandEnqueueResult{
|
||||
CommandUUID: cmd.CommandUUID,
|
||||
RequestType: cmd.Command.RequestType,
|
||||
FailedUUIDs: apnsErr.FailedUUIDs,
|
||||
FailedUUIDs: failedUUIDs,
|
||||
}, nil
|
||||
}
|
||||
// push failed for all hosts
|
||||
|
|
@ -2542,335 +2542,3 @@ func (svc *Service) DeleteMDMAppleAPNSCert(ctx context.Context) error {
|
|||
|
||||
return svc.ds.SaveAppConfig(ctx, appCfg)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// POST /api/_version_/vpp_tokens
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type uploadVPPTokenRequest struct {
|
||||
File *multipart.FileHeader
|
||||
}
|
||||
|
||||
func (uploadVPPTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
decoded := uploadVPPTokenRequest{}
|
||||
|
||||
err := r.ParseMultipartForm(512 * units.MiB)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "failed to parse multipart form",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "token multipart field is required",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
decoded.File = r.MultipartForm.File["token"][0]
|
||||
|
||||
return &decoded, nil
|
||||
}
|
||||
|
||||
type uploadVPPTokenResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
Token *fleet.VPPTokenDB `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (r uploadVPPTokenResponse) Status() int { return http.StatusAccepted }
|
||||
|
||||
func (r uploadVPPTokenResponse) error() error {
|
||||
return r.Err
|
||||
}
|
||||
|
||||
func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*uploadVPPTokenRequest)
|
||||
file, err := req.File.Open()
|
||||
if err != nil {
|
||||
return uploadVPPTokenResponse{Err: err}, nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tok, err := svc.UploadVPPToken(ctx, file)
|
||||
if err != nil {
|
||||
return uploadVPPTokenResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return uploadVPPTokenResponse{Token: tok}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UploadVPPToken(ctx context.Context, token io.ReadSeeker) (*fleet.VPPTokenDB, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey := svc.config.Server.PrivateKey
|
||||
if testSetEmptyPrivateKey {
|
||||
privateKey = ""
|
||||
}
|
||||
|
||||
if len(privateKey) == 0 {
|
||||
return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager."))
|
||||
}
|
||||
|
||||
tokenBytes, err := io.ReadAll(token)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "reading VPP token")
|
||||
}
|
||||
|
||||
locName, err := vpp.GetConfig(string(tokenBytes))
|
||||
if err != nil {
|
||||
var vppErr *vpp.ErrorResponse
|
||||
if errors.As(err, &vppErr) {
|
||||
// Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes
|
||||
if vppErr.ErrorNumber == 9622 {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager."))
|
||||
}
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple")
|
||||
}
|
||||
|
||||
data := fleet.VPPTokenData{
|
||||
Token: string(tokenBytes),
|
||||
Location: locName,
|
||||
}
|
||||
|
||||
tok, err := svc.ds.InsertVPPToken(ctx, &data)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "writing VPP token to db")
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityEnabledVPP{
|
||||
Location: locName,
|
||||
}); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "create activity for upload VPP token")
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// PATCH /api/_version_/fleet/vpp_tokens/%d/renew //
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
type patchVPPTokenRenewRequest struct {
|
||||
ID uint `url:"id"`
|
||||
File *multipart.FileHeader
|
||||
}
|
||||
|
||||
func (patchVPPTokenRenewRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
decoded := patchVPPTokenRenewRequest{}
|
||||
|
||||
err := r.ParseMultipartForm(512 * units.MiB)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "failed to parse multipart form",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "token multipart field is required",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
decoded.File = r.MultipartForm.File["token"][0]
|
||||
|
||||
return &decoded, nil
|
||||
}
|
||||
|
||||
type patchVPPTokenRenewResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
Token *fleet.VPPTokenDB `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (r patchVPPTokenRenewResponse) Status() int { return http.StatusAccepted }
|
||||
|
||||
func (r patchVPPTokenRenewResponse) error() error {
|
||||
return r.Err
|
||||
}
|
||||
|
||||
func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*patchVPPTokenRenewRequest)
|
||||
file, err := req.File.Open()
|
||||
if err != nil {
|
||||
return patchVPPTokenRenewResponse{Err: err}, nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tok, err := svc.UpdateVPPToken(ctx, req.ID, file)
|
||||
if err != nil {
|
||||
return patchVPPTokenRenewResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return patchVPPTokenRenewResponse{Token: tok}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKey := svc.config.Server.PrivateKey
|
||||
if testSetEmptyPrivateKey {
|
||||
privateKey = ""
|
||||
}
|
||||
|
||||
if len(privateKey) == 0 {
|
||||
return nil, ctxerr.New(ctx, "Couldn't upload content token. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager."))
|
||||
}
|
||||
|
||||
tokenBytes, err := io.ReadAll(token)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "reading VPP token")
|
||||
}
|
||||
|
||||
locName, err := vpp.GetConfig(string(tokenBytes))
|
||||
if err != nil {
|
||||
var vppErr *vpp.ErrorResponse
|
||||
if errors.As(err, &vppErr) {
|
||||
// Per https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes
|
||||
if vppErr.ErrorNumber == 9622 {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Invalid token. Please provide a valid content token from Apple Business Manager."))
|
||||
}
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating VPP token with Apple")
|
||||
}
|
||||
|
||||
data := fleet.VPPTokenData{
|
||||
Token: string(tokenBytes),
|
||||
Location: locName,
|
||||
}
|
||||
|
||||
tok, err := svc.ds.UpdateVPPToken(ctx, tokenID, &data)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "updating vpp token")
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// PATCH /api/_version_/fleet/vpp_tokens/%d/teams //
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
type patchVPPTokensTeamsRequest struct {
|
||||
ID uint `url:"id"`
|
||||
TeamIDs []uint `json:"teams"`
|
||||
}
|
||||
|
||||
type patchVPPTokensTeamsResponse struct {
|
||||
Token *fleet.VPPTokenDB `json:"token,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r patchVPPTokensTeamsResponse) error() error { return r.Err }
|
||||
|
||||
func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*patchVPPTokensTeamsRequest)
|
||||
|
||||
tok, err := svc.UpdateVPPTokenTeams(ctx, req.ID, req.TeamIDs)
|
||||
if err != nil {
|
||||
return patchVPPTokensTeamsResponse{Err: err}, nil
|
||||
}
|
||||
return patchVPPTokensTeamsResponse{Token: tok}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tok, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, teamIDs)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "updating vpp token team")
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// DELETE /api/_version_/fleet/vpp_tokens/%d //
|
||||
///////////////////////////////////////////////
|
||||
|
||||
type getVPPTokensRequest struct{}
|
||||
|
||||
type getVPPTokensResponse struct {
|
||||
Tokens []*fleet.VPPTokenDB `json:"vpp_tokens"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r getVPPTokensResponse) error() error { return r.Err }
|
||||
|
||||
func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (errorer, error) {
|
||||
tokens, err := svc.GetVPPTokens(ctx)
|
||||
if err != nil {
|
||||
return getVPPTokensResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
if tokens == nil {
|
||||
tokens = []*fleet.VPPTokenDB{}
|
||||
}
|
||||
|
||||
return getVPPTokensResponse{Tokens: tokens}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svc.ds.ListVPPTokens(ctx)
|
||||
}
|
||||
|
||||
type deleteVPPTokenRequest struct {
|
||||
ID uint `url:"id"`
|
||||
}
|
||||
|
||||
type deleteVPPTokenResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r deleteVPPTokenResponse) error() error { return r.Err }
|
||||
|
||||
func (r deleteVPPTokenResponse) Status() int { return http.StatusNoContent }
|
||||
|
||||
func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*deleteVPPTokenRequest)
|
||||
|
||||
err := svc.DeleteVPPToken(ctx, req.ID)
|
||||
if err != nil {
|
||||
return deleteVPPTokenResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return deleteVPPTokenResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
tok, err := svc.ds.GetVPPToken(ctx, tokenID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting vpp token")
|
||||
}
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityDisabledVPP{
|
||||
Location: tok.Location,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for delete VPP token")
|
||||
}
|
||||
|
||||
return svc.ds.DeleteVPPToken(ctx, tokenID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,13 @@ func (s *Schedule) Start() {
|
|||
level.Error(s.logger).Log("err", "start schedule", "details", err)
|
||||
ctxerr.Handle(s.ctx, err)
|
||||
}
|
||||
s.setIntervalStartedAt(prevScheduledRun.CreatedAt)
|
||||
|
||||
// if there is no previous run, set the start time to the current time.
|
||||
startedAt := prevScheduledRun.CreatedAt
|
||||
if startedAt.IsZero() {
|
||||
startedAt = time.Now()
|
||||
}
|
||||
s.setIntervalStartedAt(startedAt)
|
||||
|
||||
initialWait := 10 * time.Second
|
||||
if schedInterval := s.getSchedInterval(); schedInterval < initialWait {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
|
|
@ -73,3 +77,239 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppT
|
|||
|
||||
return fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// POST /api/_version_/vpp_tokens
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type uploadVPPTokenRequest struct {
|
||||
File *multipart.FileHeader
|
||||
}
|
||||
|
||||
func (uploadVPPTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
decoded := uploadVPPTokenRequest{}
|
||||
|
||||
err := r.ParseMultipartForm(512 * units.MiB)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "failed to parse multipart form",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "token multipart field is required",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
decoded.File = r.MultipartForm.File["token"][0]
|
||||
|
||||
return &decoded, nil
|
||||
}
|
||||
|
||||
type uploadVPPTokenResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
Token *fleet.VPPTokenDB `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (r uploadVPPTokenResponse) Status() int { return http.StatusAccepted }
|
||||
|
||||
func (r uploadVPPTokenResponse) error() error {
|
||||
return r.Err
|
||||
}
|
||||
|
||||
func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*uploadVPPTokenRequest)
|
||||
file, err := req.File.Open()
|
||||
if err != nil {
|
||||
return uploadVPPTokenResponse{Err: err}, nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tok, err := svc.UploadVPPToken(ctx, file)
|
||||
if err != nil {
|
||||
return uploadVPPTokenResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return uploadVPPTokenResponse{Token: tok}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UploadVPPToken(ctx context.Context, file io.ReadSeeker) (*fleet.VPPTokenDB, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// PATCH /api/_version_/fleet/vpp_tokens/%d/renew //
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
type patchVPPTokenRenewRequest struct {
|
||||
ID uint `url:"id"`
|
||||
File *multipart.FileHeader
|
||||
}
|
||||
|
||||
func (patchVPPTokenRenewRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
decoded := patchVPPTokenRenewRequest{}
|
||||
|
||||
err := r.ParseMultipartForm(512 * units.MiB)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "failed to parse multipart form",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
if r.MultipartForm.File["token"] == nil || len(r.MultipartForm.File["token"]) == 0 {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: "token multipart field is required",
|
||||
InternalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
decoded.File = r.MultipartForm.File["token"][0]
|
||||
|
||||
return &decoded, nil
|
||||
}
|
||||
|
||||
type patchVPPTokenRenewResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
Token *fleet.VPPTokenDB `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (r patchVPPTokenRenewResponse) Status() int { return http.StatusAccepted }
|
||||
|
||||
func (r patchVPPTokenRenewResponse) error() error {
|
||||
return r.Err
|
||||
}
|
||||
|
||||
func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*patchVPPTokenRenewRequest)
|
||||
file, err := req.File.Open()
|
||||
if err != nil {
|
||||
return patchVPPTokenRenewResponse{Err: err}, nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tok, err := svc.UpdateVPPToken(ctx, req.ID, file)
|
||||
if err != nil {
|
||||
return patchVPPTokenRenewResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return patchVPPTokenRenewResponse{Token: tok}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateVPPToken(ctx context.Context, tokenID uint, token io.ReadSeeker) (*fleet.VPPTokenDB, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// PATCH /api/_version_/fleet/vpp_tokens/%d/teams //
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
type patchVPPTokensTeamsRequest struct {
|
||||
ID uint `url:"id"`
|
||||
TeamIDs []uint `json:"teams"`
|
||||
}
|
||||
|
||||
type patchVPPTokensTeamsResponse struct {
|
||||
Token *fleet.VPPTokenDB `json:"token,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r patchVPPTokensTeamsResponse) error() error { return r.Err }
|
||||
|
||||
func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*patchVPPTokensTeamsRequest)
|
||||
|
||||
tok, err := svc.UpdateVPPTokenTeams(ctx, req.ID, req.TeamIDs)
|
||||
if err != nil {
|
||||
return patchVPPTokensTeamsResponse{Err: err}, nil
|
||||
}
|
||||
return patchVPPTokensTeamsResponse{Token: tok}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UpdateVPPTokenTeams(ctx context.Context, tokenID uint, teamIDs []uint) (*fleet.VPPTokenDB, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
// GET /api/_version_/fleet/vpp_tokens //
|
||||
/////////////////////////////////////////
|
||||
|
||||
type getVPPTokensRequest struct{}
|
||||
|
||||
type getVPPTokensResponse struct {
|
||||
Tokens []*fleet.VPPTokenDB `json:"vpp_tokens"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r getVPPTokensResponse) error() error { return r.Err }
|
||||
|
||||
func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (errorer, error) {
|
||||
tokens, err := svc.GetVPPTokens(ctx)
|
||||
if err != nil {
|
||||
return getVPPTokensResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
if tokens == nil {
|
||||
tokens = []*fleet.VPPTokenDB{}
|
||||
}
|
||||
|
||||
return getVPPTokensResponse{Tokens: tokens}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetVPPTokens(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// DELETE /api/_version_/fleet/vpp_tokens/%d //
|
||||
///////////////////////////////////////////////
|
||||
|
||||
type deleteVPPTokenRequest struct {
|
||||
ID uint `url:"id"`
|
||||
}
|
||||
|
||||
type deleteVPPTokenResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r deleteVPPTokenResponse) error() error { return r.Err }
|
||||
|
||||
func (r deleteVPPTokenResponse) Status() int { return http.StatusNoContent }
|
||||
|
||||
func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*deleteVPPTokenRequest)
|
||||
|
||||
err := svc.DeleteVPPToken(ctx, req.ID)
|
||||
if err != nil {
|
||||
return deleteVPPTokenResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return deleteVPPTokenResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteVPPToken(ctx context.Context, tokenID uint) error {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return fleet.ErrMissingLicense
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ func CreateVPPTokenEncoded(expiration time.Time, orgName, location string) ([]by
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(dataToken.Token), nil
|
||||
}
|
||||
|
||||
func CreateVPPTokenEncodedAfterMigration(expiration time.Time, orgName, location string) ([]byte, error) {
|
||||
dataToken, err := CreateVPPTokenData(expiration, orgName, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataTokenJson, err := json.Marshal(dataToken)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package worker
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
|
|
@ -70,7 +73,7 @@ func (m *DBMigration) migrateVPPToken(ctx context.Context) error {
|
|||
return ctxerr.Wrap(ctx, err, "get VPP token to migrate")
|
||||
}
|
||||
|
||||
rawToken, didUpdate, err := tok.ExtractToken()
|
||||
tokenData, didUpdate, err := extractVPPTokenFromMigration(tok)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "extract VPP token metadata")
|
||||
}
|
||||
|
|
@ -81,7 +84,47 @@ func (m *DBMigration) migrateVPPToken(ctx context.Context) error {
|
|||
m.Log.Log("info", "VPP token metadata was not updated")
|
||||
}
|
||||
|
||||
tokenData := fleet.VPPTokenData{Token: rawToken, Location: tok.Location}
|
||||
_, err = m.Datastore.UpdateVPPToken(ctx, tok.ID, &tokenData)
|
||||
return ctxerr.Wrap(ctx, err, "update VPP token")
|
||||
if _, err := m.Datastore.UpdateVPPToken(ctx, tok.ID, tokenData); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update VPP token")
|
||||
}
|
||||
// the migated token should target "All teams"
|
||||
_, err = m.Datastore.UpdateVPPTokenTeams(ctx, tok.ID, []uint{})
|
||||
return ctxerr.Wrap(ctx, err, "update VPP token teams")
|
||||
}
|
||||
|
||||
func extractVPPTokenFromMigration(migratedToken *fleet.VPPTokenDB) (tokData *fleet.VPPTokenData, didUpdateMetadata bool, err error) {
|
||||
var vppTokenData fleet.VPPTokenData
|
||||
if err := json.Unmarshal([]byte(migratedToken.Token), &vppTokenData); err != nil {
|
||||
return nil, false, fmt.Errorf("unmarshaling VPP token data: %w", err)
|
||||
}
|
||||
|
||||
vppTokenRawBytes, err := base64.StdEncoding.DecodeString(vppTokenData.Token)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("decoding raw vpp token data: %w", err)
|
||||
}
|
||||
|
||||
var vppTokenRaw fleet.VPPTokenRaw
|
||||
if err := json.Unmarshal(vppTokenRawBytes, &vppTokenRaw); err != nil {
|
||||
return nil, false, fmt.Errorf("unmarshaling raw vpp token data: %w", err)
|
||||
}
|
||||
|
||||
exp, err := time.Parse("2006-01-02T15:04:05Z0700", vppTokenRaw.ExpDate)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("parsing vpp token expiration date: %w", err)
|
||||
}
|
||||
|
||||
if vppTokenData.Location != migratedToken.Location {
|
||||
migratedToken.Location = vppTokenData.Location
|
||||
didUpdateMetadata = true
|
||||
}
|
||||
if vppTokenRaw.OrgName != migratedToken.OrgName {
|
||||
migratedToken.OrgName = vppTokenRaw.OrgName
|
||||
didUpdateMetadata = true
|
||||
}
|
||||
if !exp.Equal(migratedToken.RenewDate) {
|
||||
migratedToken.RenewDate = exp.UTC()
|
||||
didUpdateMetadata = true
|
||||
}
|
||||
|
||||
return &vppTokenData, didUpdateMetadata, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,11 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDBMigrationsVPPToken(t *testing.T) {
|
||||
// FIXME
|
||||
t.Skip()
|
||||
ctx := context.Background()
|
||||
|
||||
ds := mysql.CreateMySQLDS(t)
|
||||
|
|
@ -38,7 +37,7 @@ func TestDBMigrationsVPPToken(t *testing.T) {
|
|||
|
||||
// create the migrated token and enqueue the job
|
||||
expDate := time.Date(2024, 8, 27, 0, 0, 0, 0, time.UTC)
|
||||
tok, err := test.CreateVPPTokenEncoded(expDate, "test-org", "test-loc")
|
||||
tok, err := test.CreateVPPTokenEncodedAfterMigration(expDate, "test-org", "test-loc")
|
||||
require.NoError(t, err)
|
||||
encTok, err := mysql.EncryptWithPrivateKey(t, ds, tok)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -49,12 +48,10 @@ INSERT INTO vpp_tokens
|
|||
organization_name,
|
||||
location,
|
||||
renew_at,
|
||||
token,
|
||||
team_id,
|
||||
null_team_type
|
||||
token
|
||||
)
|
||||
VALUES
|
||||
('', '', DATE('2000-01-01'), ?, NULL, 'allteams')
|
||||
('', '', DATE('2000-01-01'), ?)
|
||||
`
|
||||
|
||||
const insJob = `
|
||||
|
|
@ -93,7 +90,9 @@ VALUES (?, ?, ?, '', ?, ?, ?)
|
|||
// nothing more to run
|
||||
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, jobs)
|
||||
if !assert.Empty(t, jobs) {
|
||||
t.Logf(">>> %#+v", jobs[0])
|
||||
}
|
||||
|
||||
// token should've been updated
|
||||
vppTok, err := ds.GetVPPTokenByLocation(ctx, "test-loc")
|
||||
|
|
@ -101,7 +100,7 @@ VALUES (?, ?, ?, '', ?, ?, ?)
|
|||
require.Equal(t, "test-org", vppTok.OrgName)
|
||||
require.Equal(t, "test-loc", vppTok.Location)
|
||||
require.Equal(t, expDate, vppTok.RenewDate)
|
||||
require.Equal(t, string(tok), vppTok.Token)
|
||||
require.Contains(t, string(tok), `"token":"`+vppTok.Token+`"`) // the DB-stored token is the "token" JSON field in the raw tok
|
||||
require.NotNil(t, vppTok.Teams)
|
||||
require.Len(t, vppTok.Teams, 0)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.22.5-alpine3.20@sha256:8c9183f715b0b4eca05b8b3dbf59766aaedb41ec07477b132ee2891ac0110a07
|
||||
FROM golang:1.22.6-alpine3.20@sha256:1a478681b671001b7f029f94b5016aed984a23ad99c707f6a0ab6563860ae2f3
|
||||
ARG TAG
|
||||
RUN apk update && apk add --no-cache git
|
||||
RUN git clone -b $TAG --depth=1 --no-tags --progress --no-recurse-submodules https://github.com/fleetdm/fleet.git && cd /go/fleet/tools/mdm/migration/mdmproxy && go build .
|
||||
|
|
|
|||
|
|
@ -84,14 +84,6 @@ func (m *mdmProxy) handleProxy(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(r.URL.Path, "/mdm") {
|
||||
if m.logSkipped {
|
||||
log.Printf("Forbidden non-mdm request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Send all micromdm repo requests to the existing server
|
||||
if strings.HasPrefix(r.URL.Path, "/repo") {
|
||||
log.Printf("%s %s -> Existing (Repo)", r.Method, r.URL.String())
|
||||
|
|
@ -100,6 +92,14 @@ func (m *mdmProxy) handleProxy(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(r.URL.Path, "/mdm") {
|
||||
if m.logSkipped {
|
||||
log.Printf("Forbidden non-mdm request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the body of the request
|
||||
body, err := io.ReadAll(r.Body)
|
||||
_ = r.Body.Close()
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@ module.exports = {
|
|||
'SFriendLee',
|
||||
'ddribeiro',
|
||||
'rebeccaui',
|
||||
'allenhouchins',
|
||||
'harrisonravazzolo',
|
||||
];
|
||||
|
||||
let GREEN_LABEL_COLOR = 'C2E0C6';// « Used in multiple places below. (FUTURE: Use the "+" prefix for this instead of color. 2022-05-05)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue