mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Merge branch 'main' into feat-mdm-migration-updates
This commit is contained in:
commit
051ba6f780
249 changed files with 3372 additions and 1848 deletions
2
.github/workflows/fleet-and-orbit.yml
vendored
2
.github/workflows/fleet-and-orbit.yml
vendored
|
|
@ -111,7 +111,7 @@ jobs:
|
|||
done
|
||||
|
||||
- name: Start Infra Dependencies
|
||||
run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker-compose up -d mysql redis &
|
||||
run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker compose up -d mysql redis &
|
||||
|
||||
- name: Install JS Dependencies
|
||||
run: make deps-js
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ defaults:
|
|||
shell: bash
|
||||
|
||||
env:
|
||||
FLEET_DESKTOP_VERSION: 1.29.0
|
||||
FLEET_DESKTOP_VERSION: 1.30.0
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ defaults:
|
|||
shell: bash
|
||||
|
||||
env:
|
||||
OSQUERY_VERSION: 5.12.2
|
||||
OSQUERY_VERSION: 5.13.0
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
2
.github/workflows/test-db-changes.yml
vendored
2
.github/workflows/test-db-changes.yml
vendored
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
|
||||
- name: Start Infra Dependencies
|
||||
# Use & to background this
|
||||
run: docker-compose up -d mysql_test &
|
||||
run: docker compose up -d mysql_test &
|
||||
|
||||
- name: Verify test schema changes
|
||||
run: |
|
||||
|
|
|
|||
6
.github/workflows/test-go.yaml
vendored
6
.github/workflows/test-go.yaml
vendored
|
|
@ -70,7 +70,7 @@ jobs:
|
|||
# Pre-starting dependencies here means they are ready to go when we need them.
|
||||
- name: Start Infra Dependencies
|
||||
# Use & to background this
|
||||
run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker-compose -f docker-compose.yml -f docker-compose-redis-cluster.yml up -d mysql_test mysql_replica_test redis redis-cluster-1 redis-cluster-2 redis-cluster-3 redis-cluster-4 redis-cluster-5 redis-cluster-6 redis-cluster-setup minio saml_idp mailhog mailpit smtp4dev_test &
|
||||
run: FLEET_MYSQL_IMAGE=${{ matrix.mysql }} docker compose -f docker-compose.yml -f docker-compose-redis-cluster.yml up -d mysql_test mysql_replica_test redis redis-cluster-1 redis-cluster-2 redis-cluster-3 redis-cluster-4 redis-cluster-5 redis-cluster-6 redis-cluster-setup minio saml_idp mailhog mailpit smtp4dev_test &
|
||||
|
||||
- name: Add TLS certificate for SMTP Tests
|
||||
run: |
|
||||
|
|
@ -98,13 +98,13 @@ jobs:
|
|||
- name: Wait for mysql
|
||||
run: |
|
||||
echo "waiting for mysql..."
|
||||
until docker-compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
|
||||
until docker compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
|
||||
echo "."
|
||||
sleep 1
|
||||
done
|
||||
echo "mysql is ready"
|
||||
echo "waiting for mysql replica..."
|
||||
until docker-compose exec -T mysql_replica_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
|
||||
until docker compose exec -T mysql_replica_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
|
||||
echo "."
|
||||
sleep 1
|
||||
done
|
||||
|
|
|
|||
14
Makefile
14
Makefile
|
|
@ -281,7 +281,7 @@ binary-arch: .pre-binary-arch .pre-binary-bundle .pre-fleet
|
|||
|
||||
# Drop, create, and migrate the e2e test database
|
||||
e2e-reset-db:
|
||||
docker-compose exec -T mysql_test bash -c 'echo "drop database if exists e2e; create database e2e;" | MYSQL_PWD=toor mysql -uroot'
|
||||
docker compose exec -T mysql_test bash -c 'echo "drop database if exists e2e; create database e2e;" | MYSQL_PWD=toor mysql -uroot'
|
||||
./build/fleet prepare db --mysql_address=localhost:3307 --mysql_username=root --mysql_password=toor --mysql_database=e2e
|
||||
|
||||
e2e-setup:
|
||||
|
|
@ -312,7 +312,7 @@ e2e-serve-premium: e2e-reset-db
|
|||
# Usage:
|
||||
# make e2e-set-desktop-token host_id=1 token=foo
|
||||
e2e-set-desktop-token:
|
||||
docker-compose exec -T mysql_test bash -c 'echo "INSERT INTO e2e.host_device_auth (host_id, token) VALUES ($(host_id), \"$(token)\") ON DUPLICATE KEY UPDATE token=VALUES(token)" | MYSQL_PWD=toor mysql -uroot'
|
||||
docker compose exec -T mysql_test bash -c 'echo "INSERT INTO e2e.host_device_auth (host_id, token) VALUES ($(host_id), \"$(token)\") ON DUPLICATE KEY UPDATE token=VALUES(token)" | MYSQL_PWD=toor mysql -uroot'
|
||||
|
||||
changelog:
|
||||
sh -c "find changes -type f | grep -v .keep | xargs -I {} sh -c 'grep \"\S\" {}; echo' > new-CHANGELOG.md"
|
||||
|
|
@ -347,7 +347,7 @@ fleetd-tuf:
|
|||
|
||||
# Reset the development DB
|
||||
db-reset:
|
||||
docker-compose exec -T mysql bash -c 'echo "drop database if exists fleet; create database fleet;" | MYSQL_PWD=toor mysql -uroot'
|
||||
docker compose exec -T mysql bash -c 'echo "drop database if exists fleet; create database fleet;" | MYSQL_PWD=toor mysql -uroot'
|
||||
./build/fleet prepare db --dev
|
||||
|
||||
# Back up the development DB to file
|
||||
|
|
@ -424,6 +424,14 @@ endif
|
|||
tar czf $(out-path)/swiftDialog.app.tar.gz -C $(TMP_DIR)/swiftDialog_pkg_payload_expanded/Library/Application\ Support/Dialog/ Dialog.app
|
||||
rm -rf $(TMP_DIR)
|
||||
|
||||
# Generate escrowBuddy.pkg bundle from the Escrow Buddy repo.
|
||||
#
|
||||
# Usage:
|
||||
# make escrow-buddy-pkg version=1.0.0 out-path=.
|
||||
escrow-buddy-pkg:
|
||||
curl -L https://github.com/macadmins/escrow-buddy/releases/download/v$(version)/Escrow.Buddy-$(version).pkg --output $(out-path)/escrowBuddy.pkg
|
||||
|
||||
|
||||
# Build and generate desktop.app.tar.gz bundle.
|
||||
#
|
||||
# Usage:
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ Fleet has no ambition to replace all of your other tools. (Though it might repl
|
|||
|
||||
Fleet plays well with Munki, Chef, Puppet, and Ansible, as well as with security tools like Crowdstrike and SentinelOne. For example, you can use the free version of Fleet to quickly report on what hosts are _actually_ running your EDR agent.
|
||||
|
||||
While most folks prefer to use one or the other, Fleet can also coexist peacefully with Rapid7 and other agent-based vulnerability scanners. This can be useful during migrations.
|
||||
|
||||
#### Free as in free
|
||||
The free version of Fleet will [always be free](https://fleetdm.com/pricing). Fleet is [independently backed](https://linkedin.com/company/fleetdm) and actively maintained with the help of many amazing [contributors](https://github.com/fleetdm/fleet/graphs/contributors).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Deploy Fleet on AWS ECS
|
||||
|
||||
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
|
||||
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Deploy Fleet on AWS with Terraform
|
||||
|
||||
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
|
||||
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Deploy Fleet on CentOS
|
||||
|
||||
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
|
||||
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Deploy Fleet on Cloud.gov (Cloud Foundry)
|
||||
|
||||
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
|
||||
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Deploy Fleet on Hetzner Cloud
|
||||
|
||||
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
|
||||
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Deploy Fleet on Kubernetes
|
||||
|
||||
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
|
||||
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Deploy Fleet on Render
|
||||
|
||||
> **This article was archived on May 16, 2024,** and may be outdated. Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for our recommended deployment method.
|
||||
> **This article was archived on May 16, 2024.** Check out [Deploy Fleet](https://fleetdm.com/docs/deploy/deploy-fleet) for the most up to date deployment method.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Fleet in your calendar: introducing maintenance windows
|
||||
|
||||
> Unlike other Fleet features which take advantage of declarative device management (DDM), the approach described in this article still uses traditional MDM commands. (More to come.)
|
||||
|
||||

|
||||
|
||||
Fleet is excited to announce the release of "maintenance windows", a new feature in Fleet v4.48 that helps make sure OS updates occur during times that disrupt your users the least. Now, just like any good colleague, when Fleet needs some of your time, it puts it on your calendar. This approach avoids interrupting your key activities or important meetings, whether in the office, on the road, or working remotely.
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ On macOS, there are two utilities that enable osquery process auditing: [OpenBSM
|
|||
To use the `es_process_events` tables, use the flag `--disable_endpointsecurity=false`. See the [EndpointSecurity instructions](https://osquery.readthedocs.io/en/latest/deployment/process-auditing/#auditing-processes-with-endpointsecurity) for more information. To use `process_events` and `socket_events` with OpenBSM, see the [OpenBSM instructions](https://osquery.readthedocs.io/en/latest/deployment/process-auditing/#auditing-processes-with-openbsm).
|
||||
|
||||
#### Windows
|
||||
Currently, osquery does not support process auditing for Windows. To learn more about process auditing on Windows, visit [Microsoft's security auditing overview](https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/security-auditing-overview). Fleet is tracking work to build process auditing for Windows in osquery. [Stay up to date on GitHub](https://github.com/fleetdm/fleet/issues/7732).
|
||||
Fleet supports auditing process events on Windows via the `process_etw_events` table. To learn more about process auditing on Windows, visit [Microsoft's security auditing overview](https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/security-auditing-overview). Fleet is tracking work to add file auditing for Windows in osquery. [Stay up to date on GitHub](https://github.com/fleetdm/fleet/issues/20946).
|
||||
|
||||
### YARA scanning
|
||||
[YARA](https://virustotal.github.io/yara/) is a malware research and detection tool available on Linux and macOS that allows users to create descriptions of malware families based on patterns of text or binary code. Each potential piece of malware is matched against a YARA rule and triggers if the specified conditions are met.
|
||||
|
|
|
|||
1
changes/13157-fv-escrow
Normal file
1
changes/13157-fv-escrow
Normal file
|
|
@ -0,0 +1 @@
|
|||
* `fleetd` now uses Escrow Buddy to rotate FileVault keys. Internal API endpoints documented in the API for contributors have been modified and/or removed.
|
||||
|
|
@ -1 +0,0 @@
|
|||
- Updated MDM features to enforce minimum OS version settings during Apple Automated Device Enrollment (ADE).
|
||||
1
changes/20194-sort-label-names-in-ui
Normal file
1
changes/20194-sort-label-names-in-ui
Normal file
|
|
@ -0,0 +1 @@
|
|||
- display the label names case-insensitive alphabetical order in the fleet UI
|
||||
1
changes/20271-deleted-host-software-installs
Normal file
1
changes/20271-deleted-host-software-installs
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fig bug where software install results could not be retrieved for deleted hosts in the activity feed
|
||||
1
changes/20395-DE-table-style-fix
Normal file
1
changes/20395-DE-table-style-fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fix a styling issue in the Controls > OS Settings > disk encryption table
|
||||
1
changes/20440-Notion-exe-installer-name
Normal file
1
changes/20440-Notion-exe-installer-name
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added a special-case to properly name the Notion .exe Windows installer the same as how it will be reported by osquery post-install.
|
||||
1
changes/20604-hosts-page-pagination
Normal file
1
changes/20604-hosts-page-pagination
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fix a bug where hosts page would sometimes allow excess pagination
|
||||
1
changes/20747-gitops-software-query
Normal file
1
changes/20747-gitops-software-query
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Use new gitops format for software pre install query
|
||||
1
changes/20751-detect-held-linux-packages-as-installed
Normal file
1
changes/20751-detect-held-linux-packages-as-installed
Normal file
|
|
@ -0,0 +1 @@
|
|||
Linux .deb packages 'on hold' are now included in the installed software list.
|
||||
1
changes/20882-ui-update-turn-on-mdm-banner
Normal file
1
changes/20882-ui-update-turn-on-mdm-banner
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Updated text for "Turn on MDM" banners in UI.
|
||||
1
changes/20933-disable-overlay-other-workflows-modal
Normal file
1
changes/20933-disable-overlay-other-workflows-modal
Normal file
|
|
@ -0,0 +1 @@
|
|||
- add a disabled overlay to the Other Workflows modal on the policy page.
|
||||
1
changes/21006-fleetctl-preview
Normal file
1
changes/21006-fleetctl-preview
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fixed a bug in `fleetctl preview` that was causing it to fail if Docker was installed without support for the deprecated `docker-compose` CLI
|
||||
2
changes/fix-software-array-migration
Normal file
2
changes/fix-software-array-migration
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Adds a migration to migrate older team configurations to the new version that includes both
|
||||
installers and App Store apps.
|
||||
|
|
@ -90,7 +90,7 @@ func applyCommand() *cli.Command {
|
|||
opts.TeamForPolicies = policiesTeamName
|
||||
}
|
||||
baseDir := filepath.Dir(flFilename)
|
||||
_, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, opts)
|
||||
_, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, nil, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ func gitopsCommand() *cli.Command {
|
|||
secrets := make(map[string]struct{})
|
||||
for _, flFilename := range flFilenames.Value() {
|
||||
baseDir := filepath.Dir(flFilename)
|
||||
config, err := spec.GitOpsFromFile(flFilename, baseDir)
|
||||
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ contexts:
|
|||
fmt.Sprintf(
|
||||
`
|
||||
controls:
|
||||
software:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
|
|
@ -230,5 +231,4 @@ team_settings:
|
|||
for _, fileName := range teamFileNames {
|
||||
_ = runAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", fileName})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const (
|
|||
orgName = "GitOps Test"
|
||||
)
|
||||
|
||||
func TestFilenameValidation(t *testing.T) {
|
||||
func TestFilenameGitOpsValidation(t *testing.T) {
|
||||
filename := strings.Repeat("a", filenameMaxLength+1)
|
||||
_, err := runAppNoChecks([]string{"gitops", "-f", filename})
|
||||
assert.ErrorContains(t, err, "file name must be less than")
|
||||
|
|
@ -207,6 +207,9 @@ func TestBasicGlobalPremiumGitOps(t *testing.T) {
|
|||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return &fleet.Job{}, nil
|
||||
}
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -238,6 +241,7 @@ org_settings:
|
|||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets:
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -381,6 +385,7 @@ agent_options:
|
|||
name: ${TEST_TEAM_NAME}
|
||||
team_settings:
|
||||
secrets: ${TEST_SECRET}
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -538,6 +543,7 @@ func TestFullGlobalGitOps(t *testing.T) {
|
|||
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
|
||||
t.Setenv("ORG_NAME", orgName)
|
||||
t.Setenv("APPLE_BM_DEFAULT_TEAM", teamName)
|
||||
t.Setenv("SOFTWARE_INSTALLER_URL", fleetServerURL)
|
||||
file := "./testdata/gitops/global_config_no_paths.yml"
|
||||
|
||||
// Dry run should fail because Apple BM Default Team does not exist and premium license is not set
|
||||
|
|
@ -834,6 +840,7 @@ agent_options:
|
|||
name: ${TEST_TEAM_NAME}
|
||||
team_settings:
|
||||
secrets: [{"secret":"${TEST_SECRET}"}]
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1011,6 +1018,7 @@ org_settings:
|
|||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1030,6 +1038,7 @@ agent_options:
|
|||
name: ${TEST_TEAM_NAME}
|
||||
team_settings:
|
||||
secrets: [{"secret":"${TEST_SECRET}"}]
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1045,6 +1054,7 @@ agent_options:
|
|||
name: ${TEST_TEAM_NAME}
|
||||
team_settings:
|
||||
secrets: [{"secret":"${TEST_SECRET}"},{"secret":"globalSecret"}]
|
||||
software:
|
||||
`,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1215,24 +1225,69 @@ func TestTeamSofwareInstallersGitOps(t *testing.T) {
|
|||
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
|
||||
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MB"},
|
||||
{"testdata/gitops/team_software_installer_valid.yml", ""},
|
||||
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
|
||||
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
||||
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries_apply.yml", "should have only one query."},
|
||||
{"testdata/gitops/team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/team_software_installer_install_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/team_software_installer_post_install_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/team_software_installer_no_url.yml", "software URL is required"},
|
||||
{"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field TeamSpecSoftware.packages of type bool"},
|
||||
{"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field SoftwareSpec.packages of type bool"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
||||
ds, _, _ := setupFullGitOpsPremiumServer(t)
|
||||
setupFullGitOpsPremiumServer(t)
|
||||
|
||||
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppID) error {
|
||||
return nil
|
||||
}
|
||||
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
|
||||
return nil
|
||||
_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
|
||||
if c.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamSoftwareInstallersGitopsQueryEnv(t *testing.T) {
|
||||
startSoftwareInstallerServer(t)
|
||||
ds, _, _ := setupFullGitOpsPremiumServer(t)
|
||||
|
||||
t.Setenv("QUERY_VAR", "IT_WORKS")
|
||||
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
if installers[0].PreInstallQuery != "select IT_WORKS" {
|
||||
return fmt.Errorf("Missing env var, got %s", installers[0].PreInstallQuery)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := runAppNoChecks([]string{"gitops", "-f", "testdata/gitops/team_software_installer_valid_env_query.yml"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNoTeamSoftwareInstallersGitOps(t *testing.T) {
|
||||
startSoftwareInstallerServer(t)
|
||||
|
||||
cases := []struct {
|
||||
file string
|
||||
wantErr string
|
||||
}{
|
||||
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are publicy accessible to the internet."},
|
||||
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
|
||||
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MB"},
|
||||
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
|
||||
{"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
||||
{"testdata/gitops/no_team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/no_team_software_installer_install_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/no_team_software_installer_post_install_not_found.yml", "no such file or directory"},
|
||||
{"testdata/gitops/no_team_software_installer_no_url.yml", "software URL is required"},
|
||||
{"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "cannot unmarshal string into Go struct field SoftwareSpec.packages of type bool"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(filepath.Base(c.file), func(t *testing.T) {
|
||||
setupFullGitOpsPremiumServer(t)
|
||||
|
||||
t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
|
||||
_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
|
||||
if c.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ func (d dockerCompose) Command(arg ...string) *exec.Cmd {
|
|||
|
||||
func newDockerCompose() (dockerCompose, error) {
|
||||
// first, check if `docker compose` is available
|
||||
if err := exec.Command("docker compose").Run(); err == nil {
|
||||
if err := exec.Command("docker", "compose").Run(); err == nil {
|
||||
return dockerCompose{dockerComposeV2}, nil
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +387,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
|
|||
}
|
||||
// this only applies standard queries, the base directory is not used,
|
||||
// so pass in the current working directory.
|
||||
_, err = client.ApplyGroup(c.Context, specs, ".", logf, fleet.ApplyClientSpecOptions{})
|
||||
_, err = client.ApplyGroup(c.Context, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,3 +187,4 @@ org_settings:
|
|||
secrets: # These secrets are used to enroll hosts to the "All teams" team
|
||||
- secret: SampleSecret123
|
||||
- secret: ABC
|
||||
software:
|
||||
|
|
|
|||
|
|
@ -91,3 +91,4 @@ org_settings:
|
|||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
|
|
@ -97,3 +97,4 @@ org_settings:
|
|||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
|
|
|
|||
|
|
@ -93,3 +93,4 @@ org_settings:
|
|||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
|
|
@ -91,3 +91,4 @@ org_settings:
|
|||
databases_path: ""
|
||||
secrets:
|
||||
- secret: ABC
|
||||
software:
|
||||
|
|
@ -1,11 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
name: query_ruby
|
||||
- name: query_ruby
|
||||
query: select 1
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
name: query_ruby2
|
||||
- name: query_ruby2
|
||||
query: select 2
|
||||
|
|
|
|||
11
cmd/fleetctl/testdata/gitops/lib/query_multiple_apply.yml
vendored
Normal file
11
cmd/fleetctl/testdata/gitops/lib/query_multiple_apply.yml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
name: query_ruby
|
||||
query: select 1
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
name: query_ruby2
|
||||
query: select 2
|
||||
|
|
@ -1,5 +1,2 @@
|
|||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
name: query_ruby
|
||||
- name: query_ruby
|
||||
query: select 1
|
||||
|
|
|
|||
5
cmd/fleetctl/testdata/gitops/lib/query_ruby_apply.yml
vendored
Normal file
5
cmd/fleetctl/testdata/gitops/lib/query_ruby_apply.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v1
|
||||
kind: query
|
||||
spec:
|
||||
name: query_ruby
|
||||
query: select 1
|
||||
2
cmd/fleetctl/testdata/gitops/lib/query_ruby_env.yml
vendored
Normal file
2
cmd/fleetctl/testdata/gitops/lib/query_ruby_env.yml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- name: query_ruby
|
||||
query: select ${QUERY_VAR}
|
||||
19
cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml
vendored
Normal file
19
cmd/fleetctl/testdata/gitops/no_team_software_installer_install_not_found.yml
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/notfound.sh
|
||||
18
cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml
vendored
Normal file
18
cmd/fleetctl/testdata/gitops/no_team_software_installer_invalid_self_service_value.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt
|
||||
self_service: "not a boolean"
|
||||
22
cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml
vendored
Normal file
22
cmd/fleetctl/testdata/gitops/no_team_software_installer_no_url.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_ruby.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
17
cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml
vendored
Normal file
17
cmd/fleetctl/testdata/gitops/no_team_software_installer_not_found.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/notfound.deb
|
||||
21
cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml
vendored
Normal file
21
cmd/fleetctl/testdata/gitops/no_team_software_installer_post_install_not_found.yml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
post_install_script:
|
||||
path: lib/notfound.sh
|
||||
23
cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml
vendored
Normal file
23
cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_multiple.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
21
cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml
vendored
Normal file
21
cmd/fleetctl/testdata/gitops/no_team_software_installer_pre_condition_not_found.yml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/notfound.yml
|
||||
17
cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml
vendored
Normal file
17
cmd/fleetctl/testdata/gitops/no_team_software_installer_too_large.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/toolarge.deb
|
||||
17
cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml
vendored
Normal file
17
cmd/fleetctl/testdata/gitops/no_team_software_installer_unsupported.yml
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/invalidtype.txt
|
||||
25
cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml
vendored
Normal file
25
cmd/fleetctl/testdata/gitops/no_team_software_installer_valid.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Test config
|
||||
controls:
|
||||
queries:
|
||||
policies:
|
||||
agent_options:
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_SERVER_URL
|
||||
org_info:
|
||||
contact_url: https://example.com/contact
|
||||
org_logo_url: ""
|
||||
org_logo_url_light_background: ""
|
||||
org_name: ${ORG_NAME}
|
||||
secrets: [{"secret":"globalSecret"}]
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_ruby.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
|
||||
self_service: true
|
||||
23
cmd/fleetctl/testdata/gitops/team_software_installer_pre_condition_multiple_queries_apply.yml
vendored
Normal file
23
cmd/fleetctl/testdata/gitops/team_software_installer_pre_condition_multiple_queries_apply.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
name: "${TEST_TEAM_NAME}"
|
||||
team_settings:
|
||||
secrets:
|
||||
- secret: "ABC"
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
host_expiry_settings:
|
||||
host_expiry_enabled: true
|
||||
host_expiry_window: 30
|
||||
agent_options:
|
||||
controls:
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_multiple_apply.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
25
cmd/fleetctl/testdata/gitops/team_software_installer_valid_apply.yml
vendored
Normal file
25
cmd/fleetctl/testdata/gitops/team_software_installer_valid_apply.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
name: "${TEST_TEAM_NAME}"
|
||||
team_settings:
|
||||
secrets:
|
||||
- secret: "ABC"
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
host_expiry_settings:
|
||||
host_expiry_enabled: true
|
||||
host_expiry_window: 30
|
||||
agent_options:
|
||||
controls:
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_ruby_apply.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
|
||||
self_service: true
|
||||
25
cmd/fleetctl/testdata/gitops/team_software_installer_valid_env_query.yml
vendored
Normal file
25
cmd/fleetctl/testdata/gitops/team_software_installer_valid_env_query.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
name: "${TEST_TEAM_NAME}"
|
||||
team_settings:
|
||||
secrets:
|
||||
- secret: "ABC"
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
host_expiry_settings:
|
||||
host_expiry_enabled: true
|
||||
host_expiry_window: 30
|
||||
agent_options:
|
||||
controls:
|
||||
policies:
|
||||
queries:
|
||||
software:
|
||||
packages:
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/ruby.deb
|
||||
install_script:
|
||||
path: lib/install_ruby.sh
|
||||
pre_install_query:
|
||||
path: lib/query_ruby_env.yml
|
||||
post_install_script:
|
||||
path: lib/post_install_ruby.sh
|
||||
- url: ${SOFTWARE_INSTALLER_URL}/other.deb
|
||||
self_service: true
|
||||
|
|
@ -2481,6 +2481,7 @@ Gets all information required by Fleet Desktop, this includes things like the nu
|
|||
```json
|
||||
{
|
||||
"failing_policies_count": 3,
|
||||
"self_service": true,
|
||||
"notifications": {
|
||||
"needs_mdm_migration": true,
|
||||
"renew_enrollment_profile": false,
|
||||
|
|
@ -2755,27 +2756,6 @@ Signals the Fleet server to send a webbook request with the device UUID and seri
|
|||
|
||||
---
|
||||
|
||||
#### Trigger FileVault key escrow
|
||||
|
||||
Sends a signal to Fleet Desktop to initiate a FileVault key escrow. This is useful for setting the escrow key initially as well as in scenarios where a token rotation is required. **Requires Fleet Premium license**
|
||||
|
||||
`POST /api/v1/fleet/device/{token}/rotate_encryption_key`
|
||||
|
||||
##### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ----- | ------ | ---- | ---------------------------------- |
|
||||
| token | string | path | The device's authentication token. |
|
||||
|
||||
##### Example
|
||||
|
||||
`POST /api/v1/fleet/device/abcdef012456789/rotate_encryption_key`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 204`
|
||||
|
||||
|
||||
### Report an agent error
|
||||
|
||||
Notifies the server about an agent error, resulting in two outcomes:
|
||||
|
|
@ -2977,8 +2957,8 @@ _Available in Fleet Premium._
|
|||
|
||||
| Name | Type | In | Description |
|
||||
| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_name`. |
|
||||
| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_id`. |
|
||||
| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_name`. Ommitting these parameters will add software to 'No Team'. |
|
||||
| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request, omit this parameter if using `team_id`. Ommitting these parameters will add software to 'No Team'. |
|
||||
| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. |
|
||||
| software | list | body | An array of software objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, and `post_install_script` - script that runs after software install. |
|
||||
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ In some cases adding a read replica can increase database performance for specif
|
|||
|
||||
#### Traffic load balancing
|
||||
Load balancing enables distributing request traffic over many instances of the backend application. Using AWS Application
|
||||
Load Balancer can also [offload SSL termination](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html), freeing Fleet to spend the majority of it's allocated compute dedicated
|
||||
Load Balancer can also [offload SSL termination](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html), freeing Fleet to spend the majority of its allocated compute dedicated
|
||||
to its core functionality. More details about ALB can be found [here](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html).
|
||||
|
||||
_**Note if using [terraform reference architecture](https://github.com/fleetdm/fleet/tree/main/infrastructure/dogfood/terraform/aws#terraform) all configurations can dynamically scale based on load(cpu/memory) and all configurations
|
||||
|
|
|
|||
|
|
@ -67,5 +67,7 @@ A collection of guides to help you get up and running with Fleet.
|
|||
- [Generate process trees with osquery](https://fleetdm.com/guides/generate-process-trees-with-osquery)
|
||||
|
||||
|
||||
<a style="text-decoration: none;" href="https://fleetdm.com/guides"><animated-arrow-button>See all guides</animated-arrow-button></a>
|
||||
|
||||
<meta name="description" value="Links to deployment tutorials and guides for using Fleet.">
|
||||
<meta name="pageOrderInSection" value="300">
|
||||
|
|
|
|||
|
|
@ -3102,18 +3102,17 @@ Returns the information of the specified host.
|
|||
|
||||
### Get host by identifier
|
||||
|
||||
Returns the information of the host specified using the `uuid`, `hardware_serial`, `osquery_host_id`, `hostname`, or
|
||||
`node_key` as an identifier.
|
||||
Returns the information of the host specified using the `hostname`, `uuid`, or `hardware_serial` as an identifier.
|
||||
|
||||
If `hostname` is specified when there is more than one host with the same hostname, the endpoint returns the first matching host. In Fleet, hostnames are fully qualified domain names (FQDNs).
|
||||
If `hostname` is specified when there is more than one host with the same hostname, the endpoint returns the first matching host. In Fleet, hostnames are fully qualified domain names (FQDNs). `hostname` (e.g. johns-macbook-air.local) is not the same as `display_name` (e.g. John's MacBook Air).
|
||||
|
||||
`GET /api/v1/fleet/hosts/identifier/:identifier`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---------- | ----------------- | ---- | ----------------------------------------------------------------------------- |
|
||||
| identifier | integer or string | path | **Required**. The host's `hardware_serial`, `uuid`, `osquery_host_id`, `hostname`, or `node_key` |
|
||||
| Name | Type | In | Description |
|
||||
| ---------- | ----------------- | ---- | ------------------------------------------------------------------ |
|
||||
| identifier | string | path | **Required**. The host's `hostname`, `uuid`, or `hardware_serial`. |
|
||||
| exclude_software | boolean | query | If `true`, the response will not include a list of installed software for the host. |
|
||||
|
||||
#### Example
|
||||
|
|
@ -3505,6 +3504,7 @@ This is the API route used by the **My device** page in Fleet desktop to display
|
|||
]
|
||||
}
|
||||
},
|
||||
"self_service": true,
|
||||
"org_logo_url": "https://example.com/logo.jpg",
|
||||
"license": {
|
||||
"tier": "free",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Fleet's agent (fleetd) generated for MacOS by `fleetctl package` does not includ
|
|||
> Ubuntu 24.04 comes with Wayland enabled by default. To use X11 instead of Wayland you can set
|
||||
> `WaylandEnable=false` in `/etc/gdm3/custom.conf` and reboot.
|
||||
|
||||
> Fedora, CentOS 8 and 9 require a [gnome extension](https://extensions.gnome.org/extension/615/appindicator-support/) and Google Chrome set to the default browser for Fleet Desktop.
|
||||
> Fedora, CentOS 8 and 9 require a [gnome extension](https://extensions.gnome.org/extension/615/appindicator-support/) and Google Chrome for Fleet Desktop.
|
||||
|
||||
> The `fleetctl package` command is not supported on DISA-STIG distribution.
|
||||
|
||||
|
|
|
|||
|
|
@ -504,7 +504,7 @@ SELECT
|
|||
'' AS arch,
|
||||
'' AS installed_path
|
||||
FROM deb_packages
|
||||
WHERE status = 'install ok installed'
|
||||
WHERE status LIKE '% ok installed'
|
||||
UNION
|
||||
SELECT
|
||||
package AS name,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Fleet supports the [latest version of osquery](https://github.com/osquery/osquer
|
|||
|
||||
## CLI
|
||||
|
||||
> You must have `fleetctl` installed. [Learn how to install `fleetctl`](https://fleetdm.com/fleetctl-preview).
|
||||
> You must have `fleetctl` installed. [Learn how to install `fleetctl`](https://fleetdm.com/docs/using-fleet/fleetctl-cli#installing-fleetctl).
|
||||
|
||||
The `fleetctl package` command is used to generate Fleet's agent (fleetd).
|
||||
|
||||
|
|
|
|||
|
|
@ -270,6 +270,8 @@ func updatesAddFunc(c *cli.Context) error {
|
|||
dstPath += ".exe"
|
||||
case strings.HasSuffix(target, ".app.tar.gz"):
|
||||
dstPath += ".app.tar.gz"
|
||||
case strings.HasSuffix(target, ".pkg"):
|
||||
dstPath += ".pkg"
|
||||
// osquery extensions require the .ext suffix
|
||||
case strings.HasSuffix(target, ".ext"):
|
||||
dstPath += ".ext"
|
||||
|
|
|
|||
|
|
@ -144,6 +144,9 @@ func adjustEmail(email string) string {
|
|||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetSetting(name string) (*calendar.Setting, error) {
|
||||
result, err := lowLevelAPI.withRetry(
|
||||
func() (any, error) {
|
||||
if lowLevelAPI.service == nil || lowLevelAPI.service.Settings == nil {
|
||||
return nil, errors.New("calendar service or settings not initialized")
|
||||
}
|
||||
return lowLevelAPI.service.Settings.Get(name).Do()
|
||||
},
|
||||
)
|
||||
|
|
@ -153,6 +156,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetSetting(name string) (*calendar
|
|||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) CreateEvent(event *calendar.Event) (*calendar.Event, error) {
|
||||
result, err := lowLevelAPI.withRetry(
|
||||
func() (any, error) {
|
||||
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
|
||||
return nil, errors.New("calendar service or events not initialized (CreateEvent)")
|
||||
}
|
||||
return lowLevelAPI.service.Events.Insert(calendarID, event).Do()
|
||||
},
|
||||
)
|
||||
|
|
@ -162,6 +168,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) CreateEvent(event *calendar.Event)
|
|||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) UpdateEvent(event *calendar.Event) (*calendar.Event, error) {
|
||||
result, err := lowLevelAPI.withRetry(
|
||||
func() (any, error) {
|
||||
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
|
||||
return nil, errors.New("calendar service or events not initialized (UpdateEvent)")
|
||||
}
|
||||
return lowLevelAPI.service.Events.Update(calendarID, event.Id, event).Do()
|
||||
},
|
||||
)
|
||||
|
|
@ -171,6 +180,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) UpdateEvent(event *calendar.Event)
|
|||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetEvent(id, eTag string) (*calendar.Event, error) {
|
||||
result, err := lowLevelAPI.withRetry(
|
||||
func() (any, error) {
|
||||
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
|
||||
return nil, errors.New("calendar service or events not initialized (GetEvent)")
|
||||
}
|
||||
return lowLevelAPI.service.Events.Get(calendarID, id).IfNoneMatch(eTag).Do()
|
||||
},
|
||||
)
|
||||
|
|
@ -180,6 +192,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) GetEvent(id, eTag string) (*calend
|
|||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) ListEvents(timeMin, timeMax string) (*calendar.Events, error) {
|
||||
result, err := lowLevelAPI.withRetry(
|
||||
func() (any, error) {
|
||||
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
|
||||
return nil, errors.New("calendar service or events not initialized (ListEvents)")
|
||||
}
|
||||
// Default maximum number of events returned is 250, which should be sufficient for most calendars.
|
||||
return lowLevelAPI.service.Events.List(calendarID).
|
||||
EventTypes("default").
|
||||
|
|
@ -197,6 +212,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) ListEvents(timeMin, timeMax string
|
|||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) DeleteEvent(id string) error {
|
||||
_, err := lowLevelAPI.withRetry(
|
||||
func() (any, error) {
|
||||
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
|
||||
return nil, errors.New("calendar service or events not initialized (DeleteEvent)")
|
||||
}
|
||||
return nil, lowLevelAPI.service.Events.Delete(calendarID, id).Do()
|
||||
},
|
||||
)
|
||||
|
|
@ -206,6 +224,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) DeleteEvent(id string) error {
|
|||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) Watch(eventUUID string, channelID string, ttl uint64) (resourceID string, err error) {
|
||||
resp, err := lowLevelAPI.withRetry(
|
||||
func() (any, error) {
|
||||
if lowLevelAPI.service == nil || lowLevelAPI.service.Events == nil {
|
||||
return nil, errors.New("calendar service or events not initialized (Watch)")
|
||||
}
|
||||
return lowLevelAPI.service.Events.Watch(calendarID, &calendar.Channel{
|
||||
Id: channelID, // channelID is also used for authentication -- it should be a random value
|
||||
Type: "web_hook",
|
||||
|
|
@ -226,6 +247,9 @@ func (lowLevelAPI *GoogleCalendarLowLevelAPI) Watch(eventUUID string, channelID
|
|||
func (lowLevelAPI *GoogleCalendarLowLevelAPI) Stop(channelID string, resourceID string) error {
|
||||
_, err := lowLevelAPI.withRetry(
|
||||
func() (any, error) {
|
||||
if lowLevelAPI.service == nil || lowLevelAPI.service.Channels == nil {
|
||||
return nil, errors.New("calendar service or channels not initialized (Stop)")
|
||||
}
|
||||
return nil, lowLevelAPI.service.Channels.Stop(&calendar.Channel{
|
||||
Id: channelID,
|
||||
ResourceId: resourceID,
|
||||
|
|
@ -274,30 +298,30 @@ func (c *GoogleCalendar) Configure(userEmail string) error {
|
|||
}
|
||||
|
||||
func (c *GoogleCalendar) UpdateEventBody(event *fleet.CalendarEvent,
|
||||
genBodyFn fleet.CalendarGenBodyFn) error {
|
||||
genBodyFn fleet.CalendarGenBodyFn) (string, error) {
|
||||
details, err := c.unmarshalDetails(event)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
gEvent, err := c.config.API.GetEvent(details.ID, "")
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(c.config.Context, err, "retrieving Google calendar event")
|
||||
return "", ctxerr.Wrap(c.config.Context, err, "retrieving Google calendar event")
|
||||
}
|
||||
// Check if the current description contains the conflict text
|
||||
conflict := strings.Contains(gEvent.Description, fleet.CalendarEventConflictText)
|
||||
var ok bool
|
||||
gEvent.Description, ok, err = genBodyFn(conflict)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(c.config.Context, err, "generating calendar event body")
|
||||
return "", ctxerr.Wrap(c.config.Context, err, "generating calendar event body")
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
_, err = c.config.API.UpdateEvent(gEvent)
|
||||
updatedEvent, err := c.config.API.UpdateEvent(gEvent)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(c.config.Context, err, "updating Google calendar event")
|
||||
return "", ctxerr.Wrap(c.config.Context, err, "updating Google calendar event")
|
||||
}
|
||||
return nil
|
||||
return updatedEvent.Etag, nil
|
||||
}
|
||||
|
||||
func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn fleet.CalendarGenBodyFn,
|
||||
|
|
@ -417,7 +441,8 @@ func (c *GoogleCalendar) GetAndUpdateEvent(event *fleet.CalendarEvent, genBodyFn
|
|||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
fleetEvent, err := c.googleEventToFleetEvent(*startTime, *endTime, gEvent, event.UUID, details.ChannelID, details.ResourceID)
|
||||
fleetEvent, err := c.googleEventToFleetEvent(*startTime, *endTime, gEvent, event.UUID, details.ChannelID, details.ResourceID,
|
||||
details.BodyTag)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
@ -650,8 +675,8 @@ func (c *GoogleCalendar) createEvent(
|
|||
resourceID = opts.ResourceID
|
||||
}
|
||||
|
||||
// Convert Google event to Fleet event
|
||||
fleetEvent, err := c.googleEventToFleetEvent(eventStart, eventEnd, event, eventUUID, channelID, resourceID)
|
||||
// Convert Google event to Fleet event. Body tag will be updated by the calling function.
|
||||
fleetEvent, err := c.googleEventToFleetEvent(eventStart, eventEnd, event, eventUUID, channelID, resourceID, "body_tag")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -702,8 +727,7 @@ func getLocation(tz string, config *GoogleCalendarConfig) *time.Location {
|
|||
}
|
||||
|
||||
func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime time.Time, event *calendar.Event, eventUUID string,
|
||||
channelID string,
|
||||
resourceID string) (
|
||||
channelID string, resourceID string, bodyTag string) (
|
||||
*fleet.CalendarEvent, error,
|
||||
) {
|
||||
tzName := c.location.String()
|
||||
|
|
@ -718,6 +742,7 @@ func (c *GoogleCalendar) googleEventToFleetEvent(startTime time.Time, endTime ti
|
|||
ETag: event.Etag,
|
||||
ChannelID: channelID,
|
||||
ResourceID: resourceID,
|
||||
BodyTag: bodyTag,
|
||||
}
|
||||
detailsJson, err := json.Marshal(details)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -235,7 +235,9 @@ func (svc *Service) processCalendarEvent(ctx context.Context, eventDetails *flee
|
|||
return ctxerr.Wrap(ctx, err, "set recent update flag")
|
||||
}
|
||||
// Event was updated, so we need to save it
|
||||
err = event.SaveBodyTag(generatedTag)
|
||||
if generatedTag != "" {
|
||||
err = event.SaveDataItems("body_tag", generatedTag)
|
||||
}
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "save calendar event body tag")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([
|
|||
return svc.ds.ListPoliciesForHost(ctx, host)
|
||||
}
|
||||
|
||||
func (svc *Service) RequestEncryptionKeyRotation(ctx context.Context, hostID uint) error {
|
||||
return svc.ds.SetDiskEncryptionResetStatus(ctx, hostID, true)
|
||||
}
|
||||
|
||||
const refetchMDMUnenrollCriticalQueryDuration = 3 * time.Minute
|
||||
|
||||
// TriggerMigrateMDMDevice triggers the webhook associated with the MDM
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ for user in $logged_in; do
|
|||
done
|
||||
|
||||
# Create the pam_nologin file
|
||||
touch /etc/nologin
|
||||
echo "Locked by administrator" > /etc/nologin
|
||||
|
||||
# Disable systemd-user-sessions, a service that deletes /etc/nologin
|
||||
if [ -f /usr/lib/systemd/system/systemd-user-sessions.service ]; then
|
||||
systemctl mask systemd-user-sessions
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
echo "All non-root users have been logged out and their accounts locked."
|
||||
|
|
|
|||
|
|
@ -21,4 +21,11 @@ do
|
|||
done
|
||||
|
||||
# Remove the pam_nologin file
|
||||
rm /etc/nologin
|
||||
[ -f /etc/nologin ] && rm /etc/nologin
|
||||
|
||||
# Enable systemd-user-sessions, a service that deletes /etc/nologin
|
||||
if [ -f /usr/lib/systemd/system/systemd-user-sessions.service ]; then
|
||||
systemctl unmask systemd-user-sessions
|
||||
systemctl daemon-reload
|
||||
/usr/lib/systemd/systemd-user-sessions start
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
|
@ -121,7 +122,7 @@ func (svc *Service) deleteVPPApp(ctx context.Context, teamID *uint, meta *fleet.
|
|||
}
|
||||
|
||||
var teamName *string
|
||||
if teamID != nil {
|
||||
if teamID != nil && *teamID != 0 {
|
||||
t, err := svc.ds.Team(ctx, *teamID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting team name for deleted VPP app")
|
||||
|
|
@ -161,11 +162,19 @@ func (svc *Service) deleteSoftwareInstaller(ctx context.Context, meta *fleet.Sof
|
|||
teamName = &t.Name
|
||||
}
|
||||
|
||||
var teamID *uint
|
||||
switch {
|
||||
case meta.TeamID == nil:
|
||||
teamID = ptr.Uint(0)
|
||||
case meta.TeamID != nil:
|
||||
teamID = meta.TeamID
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeDeletedSoftware{
|
||||
SoftwareTitle: meta.SoftwareTitle,
|
||||
SoftwarePackage: meta.Name,
|
||||
TeamName: teamName,
|
||||
TeamID: meta.TeamID,
|
||||
TeamID: teamID,
|
||||
SelfService: meta.SelfService,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "creating activity for deleted software")
|
||||
|
|
@ -425,7 +434,7 @@ func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host
|
|||
return ctxerr.Wrapf(ctx, err, "sending command to install VPP %s application to host with serial %s", vppApp.AdamID, host.HardwareSerial)
|
||||
}
|
||||
|
||||
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, user.ID, vppApp.AdamID, cmdUUID, eventID)
|
||||
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, user.ID, vppApp.VPPAppID, cmdUUID, eventID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "inserting host vpp software install for host with serial %s and app with adamID %s", host.HardwareSerial, vppApp.AdamID)
|
||||
}
|
||||
|
|
@ -463,12 +472,38 @@ func (svc *Service) GetSoftwareInstallResults(ctx context.Context, resultUUID st
|
|||
|
||||
res, err := svc.ds.GetSoftwareInstallResults(ctx, resultUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if fleet.IsNotFound(err) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return nil, ctxerr.Wrap(ctx, err, "get software install result")
|
||||
}
|
||||
|
||||
// Team specific auth check
|
||||
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{HostTeamID: res.HostTeamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
if res.HostDeletedAt == nil {
|
||||
// host is not deleted, get it and authorize for the host's team
|
||||
host, err := svc.ds.HostLite(ctx, res.HostID)
|
||||
// if error is because the host does not exist, check first if the user
|
||||
// had access to run a script (to prevent leaking valid host ids).
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return nil, ctxerr.Wrap(ctx, err, "get host lite")
|
||||
}
|
||||
// Team specific auth check
|
||||
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{HostTeamID: host.TeamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// host was deleted, authorize for no-team as a fallback
|
||||
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
res.EnhanceOutputDetails()
|
||||
|
|
@ -545,25 +580,24 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
|||
const maxInstallerSizeBytes int64 = 1024 * 1024 * 500
|
||||
|
||||
func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName string, payloads []fleet.SoftwareInstallerPayload, dryRun bool) error {
|
||||
if tmName == "" {
|
||||
svc.authz.SkipAuthorization(ctx) // so that the error message is not replaced by "forbidden"
|
||||
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("team_name", "must not be empty"))
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tm, err := svc.ds.TeamByName(ctx, tmName)
|
||||
if err != nil {
|
||||
// If this is a dry run, the team may not have been created yet
|
||||
if dryRun && fleet.IsNotFound(err) {
|
||||
return nil
|
||||
var teamID *uint
|
||||
if tmName != "" {
|
||||
tm, err := svc.ds.TeamByName(ctx, tmName)
|
||||
if err != nil {
|
||||
// If this is a dry run, the team may not have been created yet
|
||||
if dryRun && fleet.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
teamID = &tm.ID
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: &tm.ID}, fleet.ActionWrite); err != nil {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "validating authorization")
|
||||
}
|
||||
|
||||
|
|
@ -637,7 +671,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin
|
|||
}
|
||||
|
||||
installer := &fleet.UploadSoftwareInstallerPayload{
|
||||
TeamID: &tm.ID,
|
||||
TeamID: teamID,
|
||||
InstallScript: p.InstallScript,
|
||||
PreInstallQuery: p.PreInstallQuery,
|
||||
PostInstallScript: p.PostInstallScript,
|
||||
|
|
@ -697,7 +731,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin
|
|||
}
|
||||
}
|
||||
|
||||
if err := svc.ds.BatchSetSoftwareInstallers(ctx, &tm.ID, installers); err != nil {
|
||||
if err := svc.ds.BatchSetSoftwareInstallers(ctx, teamID, installers); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "batch set software installers")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1246,7 +1246,7 @@ func (svc *Service) editTeamFromSpec(
|
|||
|
||||
if spec.Software != nil {
|
||||
if team.Config.Software == nil {
|
||||
team.Config.Software = &fleet.TeamSpecSoftware{}
|
||||
team.Config.Software = &fleet.SoftwareSpec{}
|
||||
}
|
||||
|
||||
if spec.Software.Packages.Set {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import Spinner from "components/Spinner/Spinner";
|
|||
import { IMdmCommandResult } from "interfaces/mdm";
|
||||
import { IActivityDetails } from "interfaces/activity";
|
||||
|
||||
import { IconNames } from "components/icons";
|
||||
import {
|
||||
getInstallDetailsStatusPredicate,
|
||||
INSTALL_DETAILS_STATUS_ICONS,
|
||||
|
|
@ -46,7 +45,10 @@ export const AppInstallDetails = ({
|
|||
return mdmApi.getCommandResults(command_uuid).then((response) => {
|
||||
const results = response.results?.[0];
|
||||
if (!results) {
|
||||
return Promise.reject(new Error("No data returned"));
|
||||
// FIXME: It's currently possible that the command results API response is empty for pending
|
||||
// commands. As a temporary workaround to handle this case, we'll ignore the empty response and
|
||||
// display some minimal pending UI. This should be removed once the API response is fixed.
|
||||
return {} as IMdmCommandResult;
|
||||
}
|
||||
return {
|
||||
...results,
|
||||
|
|
@ -66,31 +68,40 @@ export const AppInstallDetails = ({
|
|||
} else if (isError) {
|
||||
return <DataError description="Close this modal and try again." />;
|
||||
} else if (!result) {
|
||||
// FIXME: Find a better solution for this.
|
||||
return <DataError description="No data returned." />;
|
||||
// FIXME: It's currently possible that the command results API response is empty for pending
|
||||
// commands. As a temporary workaround to handle this case, we'll ignore the empty response and
|
||||
// display some minimal pending UI. This should be updated once the API response is fixed.
|
||||
}
|
||||
|
||||
const displayStatus = (status as SoftwareInstallStatus) || "pending";
|
||||
const iconName = INSTALL_DETAILS_STATUS_ICONS[displayStatus];
|
||||
|
||||
// Note: We need to reconcile status values from two different sources. From props, we
|
||||
// get the status from the activity item details (which can be "failed", "pending", or
|
||||
// "installed"). From the command results API response, we also receive the raw status
|
||||
// from the MDM protocol, e.g., "NotNow" or "Acknowledged". We need to display some special
|
||||
// messaging for the "NotNow" status, which otherwise would be treated as "pending".
|
||||
const isStatusNotNow = result.status === "NotNow";
|
||||
let iconName: IconNames;
|
||||
const isStatusNotNow = result?.status === "NotNow";
|
||||
let predicate: string;
|
||||
let subordinate: string;
|
||||
if (isStatusNotNow) {
|
||||
iconName = INSTALL_DETAILS_STATUS_ICONS.pending;
|
||||
predicate = "tried to install";
|
||||
subordinate =
|
||||
" but couldn’t because the host was locked or was running on battery power while in Power Nap. Fleet will try again";
|
||||
} else {
|
||||
iconName = INSTALL_DETAILS_STATUS_ICONS[status as SoftwareInstallStatus];
|
||||
predicate = getInstallDetailsStatusPredicate(status);
|
||||
predicate = getInstallDetailsStatusPredicate(displayStatus);
|
||||
subordinate = status === "pending" ? " when it comes online" : "";
|
||||
}
|
||||
|
||||
const showCommandResponse = isStatusNotNow || status !== "pending";
|
||||
const formattedHost = host_display_name ? (
|
||||
<b>{host_display_name}</b>
|
||||
) : (
|
||||
"the host"
|
||||
);
|
||||
|
||||
const showCommandPayload = !!result?.payload;
|
||||
const showCommandResponse =
|
||||
!!result?.result && (isStatusNotNow || status !== "pending");
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -98,20 +109,21 @@ export const AppInstallDetails = ({
|
|||
<div className={`${baseClass}__status-message`}>
|
||||
{!!iconName && <Icon name={iconName} />}
|
||||
<span>
|
||||
Fleet {predicate} <b>{software_title}</b> on{" "}
|
||||
<b>{host_display_name}</b>
|
||||
Fleet {predicate} <b>{software_title}</b> on {formattedHost}
|
||||
{subordinate}.
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${baseClass}__script-output`}>
|
||||
Request payload:
|
||||
<Textarea className={`${baseClass}__output-textarea`}>
|
||||
{result.payload}
|
||||
</Textarea>
|
||||
</div>
|
||||
{showCommandPayload && (
|
||||
<div className={`${baseClass}__script-output`}>
|
||||
Request payload:
|
||||
<Textarea className={`${baseClass}__output-textarea`}>
|
||||
{result.payload}
|
||||
</Textarea>
|
||||
</div>
|
||||
)}
|
||||
{showCommandResponse && (
|
||||
<div className={`${baseClass}__script-output`}>
|
||||
The response from <b>{host_display_name}</b>:
|
||||
The response from {formattedHost}:
|
||||
<Textarea className={`${baseClass}__output-textarea`}>
|
||||
{result.result}
|
||||
</Textarea>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { IActivityDetails } from "interfaces/activity";
|
||||
import {
|
||||
ISoftwareInstallResult,
|
||||
ISoftwareInstallResults,
|
||||
|
|
@ -21,17 +22,28 @@ import {
|
|||
|
||||
const baseClass = "software-install-details";
|
||||
|
||||
// TODO: Expand to include more details as needed
|
||||
export type IPackageInstallDetails = Pick<
|
||||
IActivityDetails,
|
||||
"install_uuid" | "host_display_name"
|
||||
>;
|
||||
|
||||
const StatusMessage = ({
|
||||
result: { host_display_name, software_package, software_title, status },
|
||||
}: {
|
||||
result: ISoftwareInstallResult;
|
||||
}) => {
|
||||
const formattedHost = host_display_name ? (
|
||||
<b>{host_display_name}</b>
|
||||
) : (
|
||||
"the host"
|
||||
);
|
||||
return (
|
||||
<div className={`${baseClass}__status-message`}>
|
||||
<Icon name={INSTALL_DETAILS_STATUS_ICONS[status]} />
|
||||
<span>
|
||||
Fleet {getInstallDetailsStatusPredicate(status)} <b>{software_title}</b>{" "}
|
||||
({software_package}) on <b>{host_display_name}</b>
|
||||
({software_package}) on {formattedHost}
|
||||
{status === "pending" ? " when it comes online" : ""}.
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -56,18 +68,17 @@ const Output = ({
|
|||
};
|
||||
|
||||
export const SoftwareInstallDetails = ({
|
||||
installUuid,
|
||||
}: {
|
||||
installUuid: string;
|
||||
}) => {
|
||||
host_display_name = "",
|
||||
install_uuid = "",
|
||||
}: IPackageInstallDetails) => {
|
||||
const { data: result, isLoading, isError } = useQuery<
|
||||
ISoftwareInstallResults,
|
||||
Error,
|
||||
ISoftwareInstallResult
|
||||
>(
|
||||
["softwareInstallResults", installUuid],
|
||||
["softwareInstallResults", install_uuid],
|
||||
() => {
|
||||
return softwareAPI.getSoftwareInstallResult(installUuid);
|
||||
return softwareAPI.getSoftwareInstallResult(install_uuid);
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
|
|
@ -88,7 +99,11 @@ export const SoftwareInstallDetails = ({
|
|||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__software-install-details`}>
|
||||
<StatusMessage result={result} />
|
||||
<StatusMessage
|
||||
result={
|
||||
result.host_display_name ? result : { ...result, host_display_name } // prefer result.host_display_name (it may be empty if the host was deleted) otherwise default to whatever we received via props
|
||||
}
|
||||
/>
|
||||
{result.status !== "pending" && (
|
||||
<>
|
||||
{result.pre_install_query_output && (
|
||||
|
|
@ -106,10 +121,10 @@ export const SoftwareInstallDetails = ({
|
|||
};
|
||||
|
||||
export const SoftwareInstallDetailsModal = ({
|
||||
installUuid,
|
||||
details,
|
||||
onCancel,
|
||||
}: {
|
||||
installUuid: string;
|
||||
details: IPackageInstallDetails;
|
||||
onCancel: () => void;
|
||||
}) => {
|
||||
return (
|
||||
|
|
@ -121,7 +136,7 @@ export const SoftwareInstallDetailsModal = ({
|
|||
>
|
||||
<>
|
||||
<div className={`${baseClass}__modal-content`}>
|
||||
<SoftwareInstallDetails installUuid={installUuid} />
|
||||
<SoftwareInstallDetails {...details} />
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="brand">
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const INSTALL_DETAILS_STATUS_PREDICATES: Record<
|
|||
SoftwareInstallStatus,
|
||||
string
|
||||
> = {
|
||||
pending: "will install",
|
||||
pending: "is installing or will install",
|
||||
installed: "installed",
|
||||
failed: "failed to install",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ interface IDataErrorProps {
|
|||
children?: React.ReactNode;
|
||||
card?: boolean;
|
||||
className?: string;
|
||||
// flag to use the updated DataError design
|
||||
/** Flag to use the updated DataError design */
|
||||
useNew?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const Editor = ({
|
|||
<TooltipWrapper
|
||||
className={labelClassName}
|
||||
tipContent={labelTooltip}
|
||||
position="top"
|
||||
position="top-start"
|
||||
>
|
||||
{labelText}
|
||||
</TooltipWrapper>
|
||||
|
|
|
|||
|
|
@ -37,9 +37,12 @@ const MainContent = ({
|
|||
config,
|
||||
isPremiumTier,
|
||||
noSandboxHosts,
|
||||
apnsExpiry = "",
|
||||
abmExpiry = "",
|
||||
vppExpiry = "",
|
||||
isApplePnsExpired,
|
||||
isAppleBmExpired,
|
||||
isVppExpired,
|
||||
willAppleBmExpire,
|
||||
willApplePnsExpire,
|
||||
willVppExpire,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const sandboxExpiryTime =
|
||||
|
|
@ -49,29 +52,22 @@ const MainContent = ({
|
|||
|
||||
const renderAppWideBanner = () => {
|
||||
const isAppleBmTermsExpired = config?.mdm?.apple_bm_terms_expired;
|
||||
const isApplePnsExpired = hasLicenseExpired(apnsExpiry);
|
||||
const willApplePnsExpireIn30Days = willExpireWithinXDays(apnsExpiry, 30);
|
||||
const isAppleBmExpired = hasLicenseExpired(abmExpiry); // NOTE: See Rachel's related FIXME added to App.tsx in https://github.com/fleetdm/fleet/pull/19571
|
||||
const willAppleBmExpireIn30Days = willExpireWithinXDays(abmExpiry, 30);
|
||||
const isFleetLicenseExpired = hasLicenseExpired(
|
||||
config?.license.expiration || ""
|
||||
);
|
||||
|
||||
const isVppExpired = hasLicenseExpired(vppExpiry);
|
||||
const willVppExpireIn30Days = willExpireWithinXDays(vppExpiry, 30);
|
||||
|
||||
let banner: JSX.Element | null = null;
|
||||
|
||||
if (isPremiumTier) {
|
||||
if (isApplePnsExpired || willApplePnsExpireIn30Days) {
|
||||
if (isApplePnsExpired || willApplePnsExpire) {
|
||||
banner = <ApplePNCertRenewalMessage expired={isApplePnsExpired} />;
|
||||
} else if (isAppleBmExpired || willAppleBmExpireIn30Days) {
|
||||
} else if (isAppleBmExpired || willAppleBmExpire) {
|
||||
banner = <AppleBMRenewalMessage expired={isAppleBmExpired} />;
|
||||
} else if (isAppleBmTermsExpired) {
|
||||
banner = <AppleBMTermsMessage />;
|
||||
} else if (isFleetLicenseExpired) {
|
||||
banner = <LicenseExpirationBanner />;
|
||||
} else if (isVppExpired || willVppExpireIn30Days) {
|
||||
} else if (isVppExpired || willVppExpire) {
|
||||
banner = <VppRenewalMessage expired={isVppExpired} />;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ export interface IModalProps {
|
|||
isHidden?: boolean;
|
||||
/** isLoading can be set true to enable targeting elements by loading state */
|
||||
isLoading?: boolean;
|
||||
/** isContentDisabled can be set to true to display the modal content as disabled.
|
||||
* At the moment this will place an overlay over the modal content and make it
|
||||
* unclickable.
|
||||
*/
|
||||
isContentDisabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +34,7 @@ const Modal = ({
|
|||
width = "medium",
|
||||
isHidden = false,
|
||||
isLoading = false,
|
||||
isContentDisabled = false,
|
||||
className,
|
||||
}: IModalProps): JSX.Element => {
|
||||
useEffect(() => {
|
||||
|
|
@ -61,26 +67,30 @@ const Modal = ({
|
|||
}
|
||||
}, [onEnter]);
|
||||
|
||||
const modalContainerClassName = classnames(
|
||||
`${baseClass}__modal_container`,
|
||||
const backgroundClasses = classnames(`${baseClass}__background`, {
|
||||
[`${baseClass}__hidden`]: isHidden,
|
||||
});
|
||||
|
||||
const modalContainerClasses = classnames(
|
||||
className,
|
||||
{ [`${baseClass}__modal_container__medium`]: width === "medium" },
|
||||
{ [`${baseClass}__modal_container__large`]: width === "large" },
|
||||
{ [`${baseClass}__modal_container__xlarge`]: width === "xlarge" },
|
||||
{ [`${baseClass}__modal_container__auto`]: width === "auto" }
|
||||
`${baseClass}__modal_container`,
|
||||
`${baseClass}__modal_container__${width}`,
|
||||
{
|
||||
[`${className}__loading`]: isLoading,
|
||||
}
|
||||
);
|
||||
|
||||
const contentWrapperClasses = classnames(`${baseClass}__content-wrapper`, {
|
||||
[`${baseClass}__content-wrapper-disabled`]: isContentDisabled,
|
||||
});
|
||||
|
||||
const contentClasses = classnames(`${baseClass}__content`, {
|
||||
[`${baseClass}__content-disabled`]: isContentDisabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${baseClass}__background ${
|
||||
isHidden ? `${baseClass}__hidden` : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`${modalContainerClassName} ${
|
||||
isLoading ? `${className}__loading` : ""
|
||||
}`}
|
||||
>
|
||||
<div className={backgroundClasses}>
|
||||
<div className={modalContainerClasses}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<span>{title}</span>
|
||||
<div className={`${baseClass}__ex`}>
|
||||
|
|
@ -89,7 +99,13 @@ const Modal = ({
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__content`}>{children}</div>
|
||||
|
||||
<div className={contentWrapperClasses}>
|
||||
{isContentDisabled && (
|
||||
<div className={`${baseClass}__disabled-overlay`} />
|
||||
)}
|
||||
<div className={contentClasses}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
&__content-wrapper {
|
||||
margin-top: $pad-large;
|
||||
font-size: $x-small;
|
||||
|
||||
|
|
@ -100,4 +100,21 @@
|
|||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// these styles are for the modal content when it is disabled
|
||||
&__content-wrapper-disabled {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__content-disabled {
|
||||
transition: opacity 150ms ease-in-out;
|
||||
opacity: 0.5; // this adds a disabled effect to the modal content
|
||||
}
|
||||
|
||||
&__disabled-overlay {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ const REGEX_GLOBAL_PAGES = {
|
|||
|
||||
const REGEX_EXCLUDE_NO_TEAM_PAGES = {
|
||||
MANAGE_POLICIES: /\/policies\/manage/i,
|
||||
MANAGE_QUERIES: /\/queries\/manage/i,
|
||||
};
|
||||
|
||||
const testDetailPage = (path: string, re: RegExp) => {
|
||||
|
|
@ -96,7 +97,6 @@ const SiteTopNav = ({
|
|||
isGlobalMaintainer,
|
||||
isAnyTeamMaintainer,
|
||||
isNoAccess,
|
||||
isSandboxMode,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const isActiveDetailPage = isDetailPage(currentPath);
|
||||
|
|
@ -187,7 +187,7 @@ const SiteTopNav = ({
|
|||
<LinkWithContext
|
||||
className={`${navItemBaseClass}__link`}
|
||||
withParams={withParams}
|
||||
currentQueryParams={currentQueryParams}
|
||||
currentQueryParams={{ team_id: currentQueryParams.team_id }}
|
||||
to={navItem.location.pathname}
|
||||
>
|
||||
<span
|
||||
|
|
@ -220,8 +220,7 @@ const SiteTopNav = ({
|
|||
isAnyTeamAdmin,
|
||||
isAnyTeamMaintainer,
|
||||
isGlobalMaintainer,
|
||||
isNoAccess,
|
||||
isSandboxMode
|
||||
isNoAccess
|
||||
);
|
||||
|
||||
const renderNavItems = () => {
|
||||
|
|
@ -238,7 +237,6 @@ const SiteTopNav = ({
|
|||
currentUser={currentUser}
|
||||
isAnyTeamAdmin={isAnyTeamAdmin}
|
||||
isGlobalAdmin={isGlobalAdmin}
|
||||
isSandboxMode={isSandboxMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ export default (
|
|||
isAnyTeamAdmin = false,
|
||||
isAnyTeamMaintainer = false,
|
||||
isGlobalMaintainer = false,
|
||||
isNoAccess = false,
|
||||
isSandboxMode = false
|
||||
isNoAccess = false
|
||||
): INavItem[] => {
|
||||
if (!user) {
|
||||
return [];
|
||||
|
|
@ -67,7 +66,7 @@ export default (
|
|||
regex: new RegExp(`^${URL_PREFIX}/controls/`),
|
||||
pathname: PATHS.CONTROLS,
|
||||
},
|
||||
exclude: isSandboxMode || !isMaintainerOrAdmin,
|
||||
exclude: !isMaintainerOrAdmin,
|
||||
withParams: { type: "query", names: ["team_id"] },
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ interface IUserMenuProps {
|
|||
isAnyTeamAdmin: boolean | undefined;
|
||||
isGlobalAdmin: boolean | undefined;
|
||||
currentUser: IUser;
|
||||
isSandboxMode?: boolean;
|
||||
}
|
||||
|
||||
const UserMenu = ({
|
||||
|
|
@ -26,7 +25,6 @@ const UserMenu = ({
|
|||
isAnyTeamAdmin,
|
||||
isGlobalAdmin,
|
||||
currentUser,
|
||||
isSandboxMode = false,
|
||||
}: IUserMenuProps): JSX.Element => {
|
||||
const accountNavigate = onNavItemClick(PATHS.ACCOUNT);
|
||||
const dropdownItems = [
|
||||
|
|
@ -44,7 +42,7 @@ const UserMenu = ({
|
|||
},
|
||||
];
|
||||
|
||||
if (isGlobalAdmin && !isSandboxMode) {
|
||||
if (isGlobalAdmin) {
|
||||
const manageUsersNavigate = onNavItemClick(PATHS.ADMIN_USERS);
|
||||
|
||||
const manageUserNavItem = {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { IUser } from "interfaces/user";
|
||||
import permissions from "utilities/permissions";
|
||||
import sort from "utilities/sort";
|
||||
import { hasLicenseExpired, willExpireWithinXDays } from "utilities/helpers";
|
||||
|
||||
enum ACTIONS {
|
||||
SET_AVAILABLE_TEAMS = "SET_AVAILABLE_TEAMS",
|
||||
|
|
@ -144,6 +145,12 @@ type InitialStateType = {
|
|||
isOnlyObserver?: boolean;
|
||||
isObserverPlus?: boolean;
|
||||
isNoAccess?: boolean;
|
||||
isAppleBmExpired: boolean;
|
||||
isApplePnsExpired: boolean;
|
||||
isVppExpired: boolean;
|
||||
willAppleBmExpire: boolean;
|
||||
willApplePnsExpire: boolean;
|
||||
willVppExpire: boolean;
|
||||
abmExpiry?: string;
|
||||
apnsExpiry?: string;
|
||||
vppExpiry?: string;
|
||||
|
|
@ -206,6 +213,12 @@ export const initialState = {
|
|||
filteredSoftwarePath: undefined,
|
||||
filteredQueriesPath: undefined,
|
||||
filteredPoliciesPath: undefined,
|
||||
isAppleBmExpired: false,
|
||||
isApplePnsExpired: false,
|
||||
isVppExpired: false,
|
||||
willAppleBmExpire: false,
|
||||
willApplePnsExpire: false,
|
||||
willVppExpire: false,
|
||||
setAvailableTeams: () => null,
|
||||
setCurrentUser: () => null,
|
||||
setCurrentTeam: () => null,
|
||||
|
|
@ -339,6 +352,8 @@ const reducer = (state: InitialStateType, action: IAction) => {
|
|||
return {
|
||||
...state,
|
||||
abmExpiry,
|
||||
isAppleBmExpired: hasLicenseExpired(abmExpiry),
|
||||
willAppleBmExpire: willExpireWithinXDays(abmExpiry, 30),
|
||||
};
|
||||
}
|
||||
case ACTIONS.SET_APNS_EXPIRY: {
|
||||
|
|
@ -346,6 +361,8 @@ const reducer = (state: InitialStateType, action: IAction) => {
|
|||
return {
|
||||
...state,
|
||||
apnsExpiry,
|
||||
isApplePnsExpired: hasLicenseExpired(apnsExpiry),
|
||||
willApplePnsExpire: willExpireWithinXDays(apnsExpiry, 30),
|
||||
};
|
||||
}
|
||||
case ACTIONS.SET_VPP_EXPIRY: {
|
||||
|
|
@ -353,6 +370,8 @@ const reducer = (state: InitialStateType, action: IAction) => {
|
|||
return {
|
||||
...state,
|
||||
vppExpiry,
|
||||
isVppExpired: hasLicenseExpired(vppExpiry),
|
||||
willVppExpire: willExpireWithinXDays(vppExpiry, 30),
|
||||
};
|
||||
}
|
||||
case ACTIONS.SET_SANDBOX_EXPIRY: {
|
||||
|
|
@ -418,6 +437,12 @@ const AppProvider = ({ children }: Props): JSX.Element => {
|
|||
abmExpiry: state.abmExpiry,
|
||||
apnsExpiry: state.apnsExpiry,
|
||||
vppExpiry: state.vppExpiry,
|
||||
isAppleBmExpired: state.isAppleBmExpired,
|
||||
isApplePnsExpired: state.isApplePnsExpired,
|
||||
isVppExpired: state.isVppExpired,
|
||||
willAppleBmExpire: state.willAppleBmExpire,
|
||||
willApplePnsExpire: state.willApplePnsExpire,
|
||||
willVppExpire: state.willVppExpire,
|
||||
noSandboxHosts: state.noSandboxHosts,
|
||||
filteredHostsPath: state.filteredHostsPath,
|
||||
filteredSoftwarePath: state.filteredSoftwarePath,
|
||||
|
|
|
|||
|
|
@ -395,6 +395,8 @@ export const useTeamIdParam = ({
|
|||
currentTeamName: currentTeam?.name,
|
||||
currentTeamSummary: currentTeam,
|
||||
isAnyTeamSelected: isAnyTeamSelected(currentTeam?.id),
|
||||
isAllTeamsSelected:
|
||||
!isAnyTeamSelected(currentTeam?.id) && currentTeam?.id !== 0,
|
||||
isRouteOk,
|
||||
isTeamAdmin:
|
||||
!!currentTeam?.id && permissions.isTeamAdmin(currentUser, currentTeam.id),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@ const ActivityFeed = ({
|
|||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [showShowQueryModal, setShowShowQueryModal] = useState(false);
|
||||
const [showScriptDetailsModal, setShowScriptDetailsModal] = useState(false);
|
||||
const [installedSoftwareUuid, setInstalledSoftwareUuid] = useState("");
|
||||
const [
|
||||
packageInstallDetails,
|
||||
setPackageInstallDetails,
|
||||
] = useState<IActivityDetails | null>(null);
|
||||
const [
|
||||
appInstallDetails,
|
||||
setAppInstallDetails,
|
||||
|
|
@ -88,7 +91,6 @@ const ActivityFeed = ({
|
|||
activityType: ActivityType,
|
||||
details: IActivityDetails
|
||||
) => {
|
||||
console.log("activityType", activityType);
|
||||
switch (activityType) {
|
||||
case ActivityType.LiveQuery:
|
||||
queryShown.current = details.query_sql ?? "";
|
||||
|
|
@ -102,10 +104,10 @@ const ActivityFeed = ({
|
|||
setShowScriptDetailsModal(true);
|
||||
break;
|
||||
case ActivityType.InstalledSoftware:
|
||||
setInstalledSoftwareUuid(details.install_uuid ?? "");
|
||||
setPackageInstallDetails({ ...details });
|
||||
break;
|
||||
case ActivityType.InstalledAppStoreApp:
|
||||
setAppInstallDetails(details);
|
||||
setAppInstallDetails({ ...details });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -197,10 +199,10 @@ const ActivityFeed = ({
|
|||
onCancel={() => setShowScriptDetailsModal(false)}
|
||||
/>
|
||||
)}
|
||||
{installedSoftwareUuid && (
|
||||
{packageInstallDetails && (
|
||||
<SoftwareInstallDetailsModal
|
||||
installUuid={installedSoftwareUuid}
|
||||
onCancel={() => setInstalledSoftwareUuid("")}
|
||||
details={packageInstallDetails}
|
||||
onCancel={() => setPackageInstallDetails(null)}
|
||||
/>
|
||||
)}
|
||||
{appInstallDetails && (
|
||||
|
|
|
|||
|
|
@ -18,4 +18,9 @@
|
|||
.side-nav__card-container > .custom-settings {
|
||||
max-width: none;
|
||||
}
|
||||
@media (max-width: 1120px) {
|
||||
.side-nav__nav-list {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
|
||||
&--message {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { NotificationContext } from "context/notification";
|
|||
import { IApiError } from "interfaces/errors";
|
||||
import { ILabelSummary } from "interfaces/label";
|
||||
|
||||
import labelsAPI from "services/entities/labels";
|
||||
import labelsAPI, { getCustomLabels } from "services/entities/labels";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
// @ts-ignore
|
||||
|
|
@ -250,10 +250,7 @@ const AddProfileModal = ({
|
|||
isError: isErrorLabels,
|
||||
} = useQuery<ILabelSummary[], Error>(
|
||||
["custom_labels"],
|
||||
() =>
|
||||
labelsAPI
|
||||
.summary()
|
||||
.then((res) => res.labels.filter((l) => l.label_type !== "builtin")),
|
||||
() => labelsAPI.summary().then((res) => getCustomLabels(res.labels)),
|
||||
|
||||
{
|
||||
enabled: isPremiumTier,
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__profile-graphic--message {
|
||||
text-align: center;
|
||||
&--message {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,29 @@
|
|||
border-right: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.view-hosts-link {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
.linkToFilteredHosts__header {
|
||||
width: auto;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.view-hosts-link {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.linkToFilteredHosts {
|
||||
&__header {
|
||||
width: 0;
|
||||
.column-header {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
&__cell {
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -151,10 +151,13 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
|
|||
const [showPreviewTicketModal, setShowPreviewTicketModal] = useState(false);
|
||||
const [showAddSoftwareModal, setShowAddSoftwareModal] = useState(false);
|
||||
const [resetPageIndex, setResetPageIndex] = useState<boolean>(false);
|
||||
const [addedSoftwareToken, setAddedSoftwareToken] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const {
|
||||
currentTeamId,
|
||||
isAnyTeamSelected,
|
||||
isAllTeamsSelected,
|
||||
isRouteOk,
|
||||
teamIdForApi,
|
||||
userTeams,
|
||||
|
|
@ -315,7 +318,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
|
|||
|
||||
const renderPageActions = () => {
|
||||
const canManageAutomations =
|
||||
isGlobalAdmin && (!isPremiumTier || !isAnyTeamSelected);
|
||||
isGlobalAdmin && (!isPremiumTier || isAllTeamsSelected);
|
||||
|
||||
const canAddSoftware =
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
|
||||
|
|
@ -345,8 +348,8 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
|
|||
const renderHeaderDescription = () => {
|
||||
return (
|
||||
<p>
|
||||
Manage software and search for installed software, OS and
|
||||
vulnerabilities {isAnyTeamSelected ? "on this team" : "for all hosts"}.
|
||||
Manage software and search for installed software, OS, and
|
||||
vulnerabilities {isAllTeamsSelected ? "for all hosts" : "on this team"}.
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
|
@ -385,6 +388,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
|
|||
showExploitedVulnerabilitiesOnly,
|
||||
softwareFilter,
|
||||
resetPageIndex,
|
||||
addedSoftwareToken,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -424,6 +428,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
|
|||
teamId={currentTeamId ?? 0}
|
||||
router={router}
|
||||
onExit={toggleAddSoftwareModal}
|
||||
setAddedSoftwareToken={setAddedSoftwareToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -76,11 +76,13 @@ const getSoftwareNameCellData = (
|
|||
iconUrl = app_store_app.icon_url;
|
||||
}
|
||||
|
||||
const isAllTeams = teamId === undefined;
|
||||
|
||||
return {
|
||||
name: softwareTitle.name,
|
||||
source: softwareTitle.source,
|
||||
path: softwareTitleDetailsPath,
|
||||
hasPackage: hasPackage && !!teamId,
|
||||
hasPackage: hasPackage && !isAllTeams,
|
||||
isSelfService,
|
||||
iconUrl,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ interface ISoftwareTitlesProps {
|
|||
currentPage: number;
|
||||
teamId?: number;
|
||||
resetPageIndex: boolean;
|
||||
addedSoftwareToken: string | null;
|
||||
}
|
||||
|
||||
const SoftwareTitles = ({
|
||||
|
|
@ -56,6 +57,7 @@ const SoftwareTitles = ({
|
|||
currentPage,
|
||||
teamId,
|
||||
resetPageIndex,
|
||||
addedSoftwareToken,
|
||||
}: ISoftwareTitlesProps) => {
|
||||
const showVersions = location.pathname === PATHS.SOFTWARE_VERSIONS;
|
||||
|
||||
|
|
@ -80,6 +82,7 @@ const SoftwareTitles = ({
|
|||
orderDirection,
|
||||
orderKey,
|
||||
teamId,
|
||||
addedSoftwareToken,
|
||||
...getSoftwareFilterForQueryKey(softwareFilter),
|
||||
},
|
||||
],
|
||||
|
|
@ -113,6 +116,7 @@ const SoftwareTitles = ({
|
|||
orderKey,
|
||||
teamId,
|
||||
vulnerable: softwareFilter === "vulnerableSoftware",
|
||||
addedSoftwareToken,
|
||||
},
|
||||
],
|
||||
({ queryKey: [queryKey] }) =>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,15 @@ interface IAddPackageProps {
|
|||
teamId: number;
|
||||
router: InjectedRouter;
|
||||
onExit: () => void;
|
||||
setAddedSoftwareToken: (token: string) => void;
|
||||
}
|
||||
|
||||
const AddPackage = ({ teamId, router, onExit }: IAddPackageProps) => {
|
||||
const AddPackage = ({
|
||||
teamId,
|
||||
router,
|
||||
onExit,
|
||||
setAddedSoftwareToken,
|
||||
}: IAddPackageProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
|
|
@ -86,7 +92,8 @@ const AddPackage = ({ teamId, router, onExit }: IAddPackageProps) => {
|
|||
} else {
|
||||
newQueryParams.available_for_install = true;
|
||||
}
|
||||
|
||||
// any unique string - triggers SW refetch
|
||||
setAddedSoftwareToken(`${Date.now()}`);
|
||||
router.push(
|
||||
`${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}`
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,12 +37,14 @@ interface IAddSoftwareModalProps {
|
|||
teamId: number;
|
||||
router: InjectedRouter;
|
||||
onExit: () => void;
|
||||
setAddedSoftwareToken: (token: string) => void;
|
||||
}
|
||||
|
||||
const AddSoftwareModal = ({
|
||||
teamId,
|
||||
router,
|
||||
onExit,
|
||||
setAddedSoftwareToken,
|
||||
}: IAddSoftwareModalProps) => {
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -62,10 +64,20 @@ const AddSoftwareModal = ({
|
|||
<Tab>App Store (VPP)</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<AddPackage teamId={teamId} router={router} onExit={onExit} />
|
||||
<AddPackage
|
||||
teamId={teamId}
|
||||
router={router}
|
||||
onExit={onExit}
|
||||
setAddedSoftwareToken={setAddedSoftwareToken}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<AppStoreVpp teamId={teamId} router={router} onExit={onExit} />
|
||||
<AppStoreVpp
|
||||
teamId={teamId}
|
||||
router={router}
|
||||
onExit={onExit}
|
||||
setAddedSoftwareToken={setAddedSoftwareToken}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</TabsWrapper>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { NotificationContext } from "context/notification";
|
|||
import { getErrorReason } from "interfaces/errors";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
import SoftwareIcon from "../icons/SoftwareIcon";
|
||||
import { getErrorMessage } from "./helpers";
|
||||
import { getErrorMessage, getUniqueAppId } from "./helpers";
|
||||
|
||||
const baseClass = "app-store-vpp";
|
||||
|
||||
|
|
@ -64,10 +64,16 @@ const NoVppAppsCard = () => (
|
|||
interface IVppAppListItemProps {
|
||||
app: IVppApp;
|
||||
selected: boolean;
|
||||
uniqueAppId: string;
|
||||
onSelect: (software: IVppApp) => void;
|
||||
}
|
||||
|
||||
const VppAppListItem = ({ app, selected, onSelect }: IVppAppListItemProps) => {
|
||||
const VppAppListItem = ({
|
||||
app,
|
||||
selected,
|
||||
uniqueAppId,
|
||||
onSelect,
|
||||
}: IVppAppListItemProps) => {
|
||||
return (
|
||||
<li className={`${baseClass}__list-item`}>
|
||||
<Radio
|
||||
|
|
@ -77,9 +83,9 @@ const VppAppListItem = ({ app, selected, onSelect }: IVppAppListItemProps) => {
|
|||
<span>{app.name}</span>
|
||||
</div>
|
||||
}
|
||||
id={`vppApp-${app.app_store_id}`}
|
||||
id={`vppApp-${uniqueAppId}`}
|
||||
checked={selected}
|
||||
value={app.app_store_id.toString()}
|
||||
value={uniqueAppId}
|
||||
name="vppApp"
|
||||
onChange={() => onSelect(app)}
|
||||
/>
|
||||
|
|
@ -98,28 +104,41 @@ interface IVppAppListProps {
|
|||
onSelect: (app: IVppApp) => void;
|
||||
}
|
||||
|
||||
const VppAppList = ({ apps, selectedApp, onSelect }: IVppAppListProps) => (
|
||||
<div className={`${baseClass}__list-container`}>
|
||||
<ul className={`${baseClass}__list`}>
|
||||
{apps.map((app) => (
|
||||
<VppAppListItem
|
||||
key={app.app_store_id}
|
||||
app={app}
|
||||
selected={selectedApp?.app_store_id === app.app_store_id}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
const VppAppList = ({ apps, selectedApp, onSelect }: IVppAppListProps) => {
|
||||
const uniqueSelectedAppId = selectedApp ? getUniqueAppId(selectedApp) : null;
|
||||
return (
|
||||
<div className={`${baseClass}__list-container`}>
|
||||
<ul className={`${baseClass}__list`}>
|
||||
{apps.map((app) => {
|
||||
const uniqueAppId = getUniqueAppId(app);
|
||||
return (
|
||||
<VppAppListItem
|
||||
key={uniqueAppId}
|
||||
app={app}
|
||||
selected={uniqueSelectedAppId === uniqueAppId}
|
||||
uniqueAppId={uniqueAppId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IAppStoreVppProps {
|
||||
teamId: number;
|
||||
router: InjectedRouter;
|
||||
onExit: () => void;
|
||||
setAddedSoftwareToken: (token: string) => void;
|
||||
}
|
||||
|
||||
const AppStoreVpp = ({ teamId, router, onExit }: IAppStoreVppProps) => {
|
||||
const AppStoreVpp = ({
|
||||
teamId,
|
||||
router,
|
||||
onExit,
|
||||
setAddedSoftwareToken,
|
||||
}: IAppStoreVppProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true);
|
||||
const [selectedApp, setSelectedApp] = useState<IVppApp | null>(null);
|
||||
|
|
@ -160,7 +179,11 @@ const AppStoreVpp = ({ teamId, router, onExit }: IAppStoreVppProps) => {
|
|||
}
|
||||
|
||||
try {
|
||||
await mdmAppleAPI.addVppApp(teamId, selectedApp.app_store_id);
|
||||
await mdmAppleAPI.addVppApp(
|
||||
teamId,
|
||||
selectedApp.app_store_id,
|
||||
selectedApp.platform
|
||||
);
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
|
|
@ -172,6 +195,8 @@ const AppStoreVpp = ({ teamId, router, onExit }: IAppStoreVppProps) => {
|
|||
team_id: teamId,
|
||||
available_for_install: true,
|
||||
});
|
||||
// any unique string - triggers SW refetch
|
||||
setAddedSoftwareToken(`${Date.now()}`);
|
||||
router.push(`${PATHS.SOFTWARE}?${queryParams}`);
|
||||
} catch (e) {
|
||||
renderFlash("error", getErrorMessage(e));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
import { IVppApp } from "services/entities/mdm_apple";
|
||||
|
||||
const ADD_SOFTWARE_ERROR_PREFIX = "Couldn’t add software.";
|
||||
const DEFAULT_ERROR_MESSAGE = `${ADD_SOFTWARE_ERROR_PREFIX} Please try again.`;
|
||||
|
|
@ -40,3 +41,6 @@ export const getErrorMessage = (e: unknown) => {
|
|||
}
|
||||
return DEFAULT_ERROR_MESSAGE;
|
||||
};
|
||||
|
||||
export const getUniqueAppId = (app: IVppApp) =>
|
||||
`${app.app_store_id}_${app.platform}`;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useQuery } from "react-query";
|
|||
import { AxiosError } from "axios";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
import mdmAppleAPI, { IGetVppInfoResponse } from "services/entities/mdm_apple";
|
||||
|
|
@ -121,6 +122,7 @@ interface IVppSetupPageProps {
|
|||
const VppSetupPage = ({ router }: IVppSetupPageProps) => {
|
||||
const [showDisableModal, setShowDisableModal] = useState(false);
|
||||
const [showRenewModal, setShowRenewModal] = useState(false);
|
||||
const { setVppExpiry } = useContext(AppContext);
|
||||
|
||||
const {
|
||||
data: vppData,
|
||||
|
|
@ -134,6 +136,9 @@ const VppSetupPage = ({ router }: IVppSetupPageProps) => {
|
|||
{
|
||||
...DEFAULT_USE_QUERY_OPTIONS,
|
||||
retry: false,
|
||||
onSuccess: (data) => {
|
||||
setVppExpiry(data.renew_date);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export const LABEL_SLUG_PREFIX = "labels/";
|
|||
|
||||
export const DEFAULT_SORT_HEADER = "display_name";
|
||||
export const DEFAULT_SORT_DIRECTION = "asc";
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const DEFAULT_PAGE_SIZE = 50;
|
||||
export const DEFAULT_PAGE_INDEX = 0;
|
||||
|
||||
export const getHostSelectStatuses = (isSandboxMode = false) => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue