diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
index 7955082fc7e8..4819eabc22f9 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -2,7 +2,7 @@
name: 💡 Feature request
about: Propose a new feature or enhancement in Fleet.
title: ''
-labels: '~feature fest,:product'
+labels: ':product'
assignees: ''
---
diff --git a/.github/workflows/check-tuf-timestamps.yml b/.github/workflows/check-tuf-timestamps.yml
index c55de6e2b960..cdd98c7d91cc 100644
--- a/.github/workflows/check-tuf-timestamps.yml
+++ b/.github/workflows/check-tuf-timestamps.yml
@@ -6,7 +6,7 @@ on:
- '.github/workflows/check-tuf-timestamps.yml'
workflow_dispatch: # Manual
schedule:
- - cron: '0 10 * * *'
+ - cron: '0 10,22 * * *'
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
@@ -38,7 +38,7 @@ jobs:
run: |
expires=$(curl -s http://tuf.fleetctl.com/timestamp.json | jq -r '.signed.expires' | cut -c 1-10)
today=$(date "+%Y-%m-%d")
- warning_at=$(date -d "$today + 2 day" "+%Y-%m-%d")
+ warning_at=$(date -d "$today + 4 day" "+%Y-%m-%d")
expires_sec=$(date -d "$expires" "+%s")
warning_at_sec=$(date -d "$warning_at" "+%s")
diff --git a/.github/workflows/render-deploy.yml b/.github/workflows/render-deploy.yml
new file mode 100644
index 000000000000..1a927ed3bd8e
--- /dev/null
+++ b/.github/workflows/render-deploy.yml
@@ -0,0 +1,48 @@
+name: Render deploy
+
+# Re-deploy Fleet servers on Render to update to the latest Fleet release.
+#
+# Render (https://render.com/) is hosting 2 Fleet servers that are used by our gitops repos:
+# - https://github.com/fleetdm/fleet-gitops
+# - https://gitlab.com/fleetdm/fleet-gitops
+#
+# The premium server (fleet-gitops-ci-premium) is used by GitHub CI and the free server (fleet-gitops-ci-free) is used by GitLab CI.
+# Both servers share a MySQL service (fleet-gitops-ci-mysql).
+# - fleet-gitops-ci-premium uses fleet database
+# - fleet-gitops-ci-free uses fleet_free database
+#
+# Both servers share a Redis service (fleet-gitops-ci-redis).
+# - fleet-gitops-ci-premium uses database 0 (the default)
+# - fleet-gitops-ci-free uses database 1
+
+on:
+ workflow_dispatch: # Manual
+ schedule:
+ - cron: '0 2 * * *' # Nightly 2AM UTC
+
+# This allows a subsequently queued workflow run to interrupt previous runs
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ # fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
+ shell: bash
+
+permissions:
+ contents: read
+
+jobs:
+ render-deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
+ with:
+ egress-policy: audit
+
+ - name: Trigger deploy
+ run: |
+ curl "${{ secrets.RENDER_GITOPS_FREE_DEPLOY_HOOK }}"
+ curl "${{ secrets.RENDER_GITOPS_PREMIUM_DEPLOY_HOOK }}"
diff --git a/.github/workflows/test-go.yaml b/.github/workflows/test-go.yaml
index b9c013bd4101..084b1ea56b53 100644
--- a/.github/workflows/test-go.yaml
+++ b/.github/workflows/test-go.yaml
@@ -44,7 +44,7 @@ jobs:
matrix:
suite: ["integration", "core"]
os: [ubuntu-latest]
- mysql: ["mysql:8.0.36", "mysql:8.4.2"] # make sure to update supported versions docs when this changes
+ mysql: ["mysql:8.0.36", "mysql:8.4.3"] # make sure to update supported versions docs when this changes
continue-on-error: ${{ matrix.suite == 'integration' }} # Since integration tests have a higher chance of failing, often for unrelated reasons, we don't want to fail the whole job if they fail
runs-on: ${{ matrix.os }}
diff --git a/articles/puppet-module.md b/articles/puppet-module.md
index bf6a442bc14e..78dc4b0c7490 100644
--- a/articles/puppet-module.md
+++ b/articles/puppet-module.md
@@ -20,7 +20,7 @@ Install [Fleet's Puppet module](https://forge.puppet.com/modules/fleetdm/fleetdm
### Step 2: configure Puppet to talk to Fleet using Heira
-1. In Fleet, create an API-only user with the global admin role. Instructions for creating an API-only user are [here](./fleetctl-CLI.md#create-an-api-only-user).
+1. In Fleet, create an API-only user with the GitOps role. Instructions for creating an API-only user are [here](./fleetctl-CLI.md#create-an-api-only-user).
2. Get the API token for your new API-only user. Learn how [here](./fleetctl-CLI.md#get-the-api-token-of-an-api-only-user).
diff --git a/articles/seamless-mdm-migration.md b/articles/seamless-mdm-migration.md
index 9abf3c516c4f..369c6c17145d 100644
--- a/articles/seamless-mdm-migration.md
+++ b/articles/seamless-mdm-migration.md
@@ -1,20 +1,20 @@
-# Seamless MDM migrations to Fleet
+# Seamless macOS MDM migration
-![Seamless MDM migrations to Fleet](../website/assets/images/articles/seamless-mdm-migration-1600x900@2x.png)
+![Seamless macOS MDM migrations to Fleet](../website/assets/images/articles/seamless-mdm-migration-1600x900@2x.png)
Migrating macOS devices between Mobile Device Management (MDM) solutions is often fraught with challenges, including potential gaps in device management, user disruption, and compliance issues. Traditional MDM migrations typically require end-user interaction and leave devices unmanaged for a period, leading to problems like Wi-Fi disconnections due to certificate profile removal and incomplete migrations. These challenges can force organizations to stay with outdated MDM solutions that no longer meet their needs. But there’s a better way.
Seamless MDM migrations are now possible, allowing organizations to transition their macOS devices to Fleet without any downtime or end-user involvement. By leveraging Fleet, you can ensure that your devices remain fully managed and compliant throughout the migration process. This means no more gaps in management, no user disruptions, and a smoother path to a more modern and effective MDM solution.
-This guide will walk you through the entire process of migrating your MDM deployment to Fleet. You’ll start by understanding the specific requirements for a seamless migration, followed by configuring Fleet with the necessary certificates and database records. The guide will then take you through the process of installing Fleet’s agent (`fleetd`) on your devices, updating DNS records to redirect devices to the Fleet server, and finally, decommissioning your old MDM server.
+This guide will walk you through the entire process of migrating your MDM deployment to Fleet. You’ll start by understanding the specific requirements for a seamless migration, followed by configuring Fleet with the necessary certificates and database records. The guide will then take you through the process of installing Fleet’s agent (`fleetd`) on your devices, updating domain (DNS) records to redirect devices to the Fleet server, and finally, decommissioning your old MDM server.
Throughout the guide, you’ll find practical advice and best practices to ensure a smooth transition with minimal risk. By the end, you’ll be equipped with the knowledge and tools to execute a seamless MDM migration to Fleet, ensuring that your organization’s devices are securely managed without the typical headaches associated with a traditional MDM switch.
## Requirements
-Note: Deployments that do not meet these seamless migration requirements can still migrate with the [standard MDM migration process](https://fleetdm.com/docs/using-fleet/mdm-migration-guide).
+> Deployments that do not meet these seamless migration requirements can still migrate with the [standard MDM migration process](https://fleetdm.com/docs/using-fleet/mdm-migration-guide).
-* Customer controls the DNS used in the MDM server enrollment (eg. devices are enrolled to `*.customerowneddomain.com`, not `*.mdmvendor.com`).
+* Customer owns the domain (DNS) used in the MDM enrollment profile (e.g. devices are enrolled to `*.customerowneddomain.com`, not `*.mdmvendor.com`).
* Customer has access to the Apple Push Notification Service (APNS) certificate/key and SCEP certificate/key, or access to the MDM server database to extract these values.
These requirements are easily met in self-hosted open-source MDM solutions and may be met with commercial solutions when the customer is self-hosting or otherwise controls the DNS.
@@ -31,7 +31,7 @@ Apple allows changing most values in profiles delivered by MDM, but the `ServerU
2. Import database records letting Fleet know about the devices to be migrated.
3. Configure controls (profiles, updates, etc.) in Fleet.
4. Install `fleetd` on the devices (through the existing MDM).
-5. Update DNS records to point devices to the Fleet server.
+5. Update domain (DNS) records to point devices to the Fleet server.
6. Decommission the old server.
It is recommended to follow the entire process on a staging/test MDM instance and devices, then repeat for the production instance and devices.
diff --git a/articles/windows-mdm-setup.md b/articles/windows-mdm-setup.md
index 87188e11ee11..18273f1526c2 100644
--- a/articles/windows-mdm-setup.md
+++ b/articles/windows-mdm-setup.md
@@ -4,7 +4,7 @@
To control OS settings, updates, and more on Windows hosts follow the manual enrollment instructions.
-To use automatic enrollment (aka zero-touch) features on Windows, follow instructions to connect Fleet to Microsoft Azure Active Directory (aka Microsoft Entra). You can further customize zero-touch with Windows Autopilot.
+To use automatic enrollment (aka zero-touch) features on Windows, follow instructions to connect Fleet to Microsoft Entra ID. You can further customize zero-touch with Windows Autopilot.
## Manual enrollment
@@ -34,7 +34,7 @@ Restart the Fleet server.
### Step 3: Turn on Windows MDM
-1. Head to the **Settings > Integrations > Mobile device management (MDM) enrollment** page.
+1. Head to the **Settings > Integrations > Mobile device management (MDM)** page.
2. Next to **Turn on Windows MDM** select **Turn on** to navigate to the **Turn on Windows MDM** page.
@@ -48,13 +48,13 @@ With Windows MDM turned on, enroll a Windows host to Fleet by installing [Fleet'
> Available in Fleet Premium
-To automatically enroll Windows workstations when they’re first unboxed and set up by your end users, we will connect Fleet to Microsoft Azure Active Directory (Azure AD).
+To automatically enroll Windows workstations when they’re first unboxed and set up by your end users, we will connect Fleet to Microsoft Entra ID.
-After you connect Fleet to Azure AD, you can customize the Windows setup experience with [Windows Autopilot](https://learn.microsoft.com/en-us/autopilot/windows-autopilot).
+After you connect Fleet to Microsoft Entra ID, you can customize the Windows setup experience with [Windows Autopilot](https://learn.microsoft.com/en-us/autopilot/windows-autopilot).
-In order to connect Fleet to Azure AD, the IT admin (you) needs a Microsoft Enterprise Mobility + Security E3 license.
+In order to connect Fleet to Microsoft Entra ID, the IT admin (you) needs a Microsoft Enterprise Mobility + Security E3 license.
-Each end user who automatically enrolls needs a Microsoft Intune license.
+Each end user who automatically enrolls needs a [Microsoft license](https://learn.microsoft.com/en-us/mem/intune/fundamentals/licenses.)
### Step 1: Buy Microsoft licenses
@@ -68,9 +68,9 @@ Each end user who automatically enrolls needs a Microsoft Intune license.
5. On the **Enterprise Mobility + Security E3** page, select **Buy** and follow instructions to purchase the license.
-6. Find and buy an Intune license.
+6. Find and buy a license.
-7. Sign in to [Azure portal](https://portal.azure.com).
+7. Sign in to [Microsoft Entra ID portal](https://portal.azure.com).
8. At the top of the page search "Users" and select **Users**.
@@ -78,15 +78,15 @@ Each end user who automatically enrolls needs a Microsoft Intune license.
10. Select **+ Assignments** and assign yourself the **Enterprise Mobility + Security E3**. Assign the test user the Intune licnese.
-### Step 2: Connect Fleet to Azure AD
+### Step 2: Connect Fleet to Microsoft Entra ID
-For instructions on how to connect Fleet to Azure AD, in the Fleet UI, select the avatar on the right side of the top navigation and select **Settings > Integrations > Automatic enrollment**. Then, next to **Windows automatic enrollment** select **Details**.
+For instructions on how to connect Fleet to Microsoft Entra ID, in the Fleet UI, select the avatar on the right side of the top navigation and select **Settings > Integrations > Mobile device management (MDM)**. Then, next to **Windows automatic enrollment** select **Details**.
### Step 3: Test automatic enrollment
-Testing automatic enrollment requires creating a test user in Azure AD and a freshly wiped or new Windows workstation.
+Testing automatic enrollment requires creating a test user in Microsoft Entra ID and a freshly wiped or new Windows workstation.
-1. Sign in to [Azure portal](https://portal.azure.com).
+1. Sign in to [Microsoft Entra ID portal](https://portal.azure.com).
2. At the top of the page search "Users" and select **Users**.
@@ -124,7 +124,7 @@ Testing automatic enrollment requires creating a test user in Azure AD and a fre
### Step 3: Upload your organization's logo
-1. Navigate to [Azure portal](https://portal.azure.com).
+1. Navigate to [Microsoft Entra ID portal](https://portal.azure.com).
2. At the top of the page, search for "Microsoft Entra ID", select **Microsoft Entra ID**, and then select **Company branding**.
diff --git a/changes/22331-remove-pending-devices b/changes/22331-remove-pending-devices
new file mode 100644
index 000000000000..ddd32ef2b0ae
--- /dev/null
+++ b/changes/22331-remove-pending-devices
@@ -0,0 +1 @@
+Remove a pending MDM device if it was deleted from current ABM
diff --git a/changes/23174-fix-patch-config-vpp-associations b/changes/23174-fix-patch-config-vpp-associations
new file mode 100644
index 000000000000..f8edd86b47ec
--- /dev/null
+++ b/changes/23174-fix-patch-config-vpp-associations
@@ -0,0 +1 @@
+* Fixed bug where `PATCH /api/latest/fleet/config` was incorrectly clearing VPP token<->team associations.
diff --git a/changes/23183-opentelemetry b/changes/23183-opentelemetry
new file mode 100644
index 000000000000..5dc48a73d24e
--- /dev/null
+++ b/changes/23183-opentelemetry
@@ -0,0 +1,3 @@
+Updated OpenTelemetry libraries to latest versions. This includes the following changes when OpenTelemetry is enabled:
+- MySQL spans outside of HTTPS transactions are now logged.
+- Renamed MySQL spans to include the query, for easier tracking/debugging.
diff --git a/changes/23215-message-spacing b/changes/23215-message-spacing
new file mode 100644
index 000000000000..3ad12430b283
--- /dev/null
+++ b/changes/23215-message-spacing
@@ -0,0 +1 @@
+* Explicitly set line heights on "add profile" messages so they are consistent cross-browser
\ No newline at end of file
diff --git a/changes/23219 b/changes/23219
new file mode 100644
index 000000000000..ea0982ba0e60
--- /dev/null
+++ b/changes/23219
@@ -0,0 +1 @@
+* Make entire rows of the Disk encryption table clickable
\ No newline at end of file
diff --git a/changes/urf-8 b/changes/urf-8
new file mode 100644
index 000000000000..23095d90726a
--- /dev/null
+++ b/changes/urf-8
@@ -0,0 +1 @@
+* Fixed incorrect character set header on manual Mac enrollment config download
diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go
index 5294e302e88d..a3c310adf3d0 100644
--- a/cmd/fleet/serve_test.go
+++ b/cmd/fleet/serve_test.go
@@ -307,6 +307,7 @@ func TestAutomationsSchedule(t *testing.T) {
}
func TestCronVulnerabilitiesCreatesDatabasesPath(t *testing.T) {
+ t.Skip() // https://github.com/fleetdm/fleet/issues/23258
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
@@ -352,10 +353,15 @@ func TestCronVulnerabilitiesCreatesDatabasesPath(t *testing.T) {
}
// Use schedule to test that the schedule does indeed call cronVulnerabilities.
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
+ ctx, cancel := context.WithCancel(ctx)
lg := kitlog.NewJSONLogger(os.Stdout)
s, err := newVulnerabilitiesSchedule(ctx, "test_instance", ds, lg, &config)
require.NoError(t, err)
s.Start()
+ t.Cleanup(func() {
+ cancel()
+ <-s.Done()
+ })
assert.Eventually(t, func() bool {
info, err := os.Lstat(vulnPath)
@@ -660,9 +666,14 @@ func TestCronVulnerabilitiesSkipMkdirIfDisabled(t *testing.T) {
// Use schedule to test that the schedule does indeed call cronVulnerabilities.
ctx = license.NewContext(ctx, &fleet.LicenseInfo{Tier: fleet.TierPremium})
+ ctx, cancel := context.WithCancel(ctx)
s, err := newVulnerabilitiesSchedule(ctx, "test_instance", ds, kitlog.NewNopLogger(), &config)
require.NoError(t, err)
s.Start()
+ t.Cleanup(func() {
+ cancel()
+ <-s.Done()
+ })
// Every cron tick is 10 seconds ... here we just wait for a loop interation and assert the vuln
// dir. was not created.
@@ -1117,7 +1128,8 @@ func TestVerifyDiskEncryptionKeysJob(t *testing.T) {
fleet.MDMAssetCAKey: {Value: testKeyPEM},
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
- _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
+ _ sqlx.QueryerContext,
+ ) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return assets, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
diff --git a/cmd/fleetctl/generate.go b/cmd/fleetctl/generate.go
index 612740df0d16..255b5a33df07 100644
--- a/cmd/fleetctl/generate.go
+++ b/cmd/fleetctl/generate.go
@@ -49,24 +49,24 @@ func generateMDMAppleCommand() *cli.Command {
// before printing the CSR output message.
client, err := clientFromCLI(c)
if err != nil {
- fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s", err)
+ fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s\n", err)
return ErrGeneric
}
csr, err := client.RequestAppleCSR()
if err != nil {
- fmt.Fprintf(c.App.ErrWriter, "requesting APNs CSR: %s", err)
+ fmt.Fprintf(c.App.ErrWriter, "requesting APNs CSR: %s\n", err)
return ErrGeneric
}
if err := os.WriteFile(csrPath, csr, defaultFileMode); err != nil {
- fmt.Fprintf(c.App.ErrWriter, "write CSR: %s", err)
+ fmt.Fprintf(c.App.ErrWriter, "write CSR: %s\n", err)
return ErrGeneric
}
appCfg, err := client.GetAppConfig()
if err != nil {
- fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s", err)
+ fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s\n", err)
return ErrGeneric
}
diff --git a/cmd/fleetctl/gitops.go b/cmd/fleetctl/gitops.go
index 2382840f67fa..04ca1469c0bc 100644
--- a/cmd/fleetctl/gitops.go
+++ b/cmd/fleetctl/gitops.go
@@ -86,8 +86,13 @@ func gitopsCommand() *cli.Command {
noTeamControls spec.Controls
noTeamPresent bool
)
+ isPremium := appConfig.License.IsPremium()
for _, flFilename := range flFilenames.Value() {
if filepath.Base(flFilename) == "no-team.yml" {
+ if !isPremium {
+ // Message is printed in the next flFilenames loop to avoid printing it multiple times
+ break
+ }
baseDir := filepath.Dir(flFilename)
config, err := spec.GitOpsFromFile(flFilename, baseDir, appConfig, func(format string, a ...interface{}) {})
if err != nil {
@@ -148,13 +153,16 @@ func gitopsCommand() *cli.Command {
if !config.Controls.Set() {
config.Controls = noTeamControls
}
+ } else if !isPremium {
+ logf("[!] skipping team config %s since teams are only supported for premium Fleet users\n", flFilename)
+ continue
}
// Special handling for tokens is required because they link to teams (by
// name.) Because teams can be created/deleted during the same gitops run, we
// grab some information to help us determine allowed/restricted actions and
// when to perform the associations.
- if isGlobalConfig && totalFilenames > 1 && !(totalFilenames == 2 && noTeamPresent) {
+ if isGlobalConfig && totalFilenames > 1 && !(totalFilenames == 2 && noTeamPresent) && isPremium {
abmTeams, hasMissingABMTeam, usesLegacyABMConfig, err = checkABMTeamAssignments(config, fleetClient)
if err != nil {
return err
diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go
index fefe4c3153d2..3dc253804192 100644
--- a/cmd/fleetctl/gitops_test.go
+++ b/cmd/fleetctl/gitops_test.go
@@ -1020,6 +1020,50 @@ software:
assert.Equal(t, filepath.Base(tmpFile.Name()), *savedTeam.Filename)
}
+func createFakeITunesAndVPPServices(t *testing.T) {
+ config := &appleVPPConfigSrvConf{
+ Assets: []vpp.Asset{
+ {
+ AdamID: "1",
+ PricingParam: "STDQ",
+ AvailableCount: 12,
+ },
+ {
+ AdamID: "2",
+ PricingParam: "STDQ",
+ AvailableCount: 3,
+ },
+ },
+ SerialNumbers: []string{"123", "456"},
+ }
+ startVPPApplyServer(t, config)
+
+ appleITunesSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // a map of apps we can respond with
+ db := map[string]string{
+ // macos app
+ "1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
+ // macos, ios, ipados app
+ "2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2,
+ "supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
+ // ipados app
+ "3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3,
+ "supportedDevices": ["iPadAir-iPadAir"] }`,
+ }
+
+ adamIDString := r.URL.Query().Get("id")
+ adamIDs := strings.Split(adamIDString, ",")
+
+ var objs []string
+ for _, a := range adamIDs {
+ objs = append(objs, db[a])
+ }
+
+ _, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ","))))
+ }))
+ t.Setenv("FLEET_DEV_ITUNES_URL", appleITunesSrv.URL)
+}
+
func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
@@ -1033,7 +1077,8 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
// Mock appConfig
savedAppConfig := &fleet.AppConfig{}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
- return &fleet.AppConfig{}, nil
+ appConfig := savedAppConfig.Copy()
+ return appConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
savedAppConfig = config
@@ -1104,6 +1149,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
return nil, nil, nil
}
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
+ if savedTeam != nil {
+ return []*fleet.Team{savedTeam}, nil
+ }
return nil, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, error) { return nil, nil }
@@ -1158,8 +1206,12 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
return nil
}
+ vppToken := &fleet.VPPTokenDB{
+ Location: "Foobar",
+ RenewDate: time.Now().Add(24 * 365 * time.Hour),
+ }
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
- return []*fleet.VPPTokenDB{}, nil
+ return []*fleet.VPPTokenDB{vppToken}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
@@ -1169,11 +1221,38 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
return nil
}
+ ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
+ var teamsSummary []*fleet.TeamSummary
+ if savedTeam != nil {
+ teamsSummary = append(teamsSummary, &fleet.TeamSummary{
+ ID: savedTeam.ID,
+ Name: savedTeam.Name,
+ Description: savedTeam.Description,
+ })
+ }
+ return teamsSummary, nil
+ }
+
+ ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
+ if teamID != nil && *teamID == savedTeam.ID {
+ return vppToken, nil
+ }
+ return nil, ¬FoundError{}
+ }
+
+ ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
+ return vppToken, nil
+ }
+
+ createFakeITunesAndVPPServices(t)
+
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
+ t.Setenv("TEST_TEAM_NAME", teamName)
+ t.Setenv("TEST_SECRET", secret)
_, err = globalFile.WriteString(
`
@@ -1189,6 +1268,11 @@ org_settings:
org_logo_url: ""
org_logo_url_light_background: ""
org_name: ${ORG_NAME}
+ mdm:
+ volume_purchasing_program:
+ - location: Foobar
+ teams:
+ - "${TEST_TEAM_NAME}"
secrets: [{"secret":"globalSecret"}]
software:
`,
@@ -1198,9 +1282,6 @@ software:
teamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
- t.Setenv("TEST_TEAM_NAME", teamName)
- t.Setenv("TEST_SECRET", secret)
-
_, err = teamFile.WriteString(
`
controls:
@@ -1211,6 +1292,8 @@ name: ${TEST_TEAM_NAME}
team_settings:
secrets: [{"secret":"${TEST_SECRET}"}]
software:
+ app_store_apps:
+ - app_store_id: '1'
`,
)
require.NoError(t, err)
@@ -1246,12 +1329,18 @@ software:
require.Error(t, err)
assert.ErrorContains(t, err, "duplicate enroll secret found")
+ ds.GetVPPTokenByTeamIDFuncInvoked = false
+
// Dry run
_ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
+ // Dry run should not attempt to get the VPP token when applying VPP apps (it may not exist).
+ require.False(t, ds.GetVPPTokenByTeamIDFuncInvoked)
+ ds.ListTeamsFuncInvoked = false
+
// Dry run, deleting other teams
- assert.False(t, ds.ListTeamsFuncInvoked)
+ savedAppConfig = &fleet.AppConfig{}
_ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run", "--delete-other-teams"})
assert.Equal(t, fleet.AppConfig{}, *savedAppConfig, "AppConfig should be empty")
assert.True(t, ds.ListTeamsFuncInvoked)
@@ -1266,6 +1355,12 @@ software:
require.Len(t, enrolledTeamSecrets, 1)
assert.Equal(t, secret, enrolledTeamSecrets[0].Secret)
+ // Dry run again (after team was created by real run)
+ ds.GetVPPTokenByTeamIDFuncInvoked = false
+ _ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
+ // Dry run should not attempt to get the VPP token when applying VPP apps (it may not exist).
+ require.False(t, ds.GetVPPTokenByTeamIDFuncInvoked)
+
// Now, set up a team to delete
teamToDeleteID := uint(999)
teamToDelete := &fleet.Team{
diff --git a/docs/Configuration/fleet-server-configuration.md b/docs/Configuration/fleet-server-configuration.md
index 89fa9072ee40..c64dc6f97d10 100644
--- a/docs/Configuration/fleet-server-configuration.md
+++ b/docs/Configuration/fleet-server-configuration.md
@@ -757,26 +757,6 @@ The license key provided to Fleet customers which provides access to Fleet Premi
key: foobar
```
-##### license_enforce_host_limit
-
-Whether Fleet should enforce the host limit of the license, if true, attempting to enroll new hosts when the limit is reached will fail.
-
-- Default value: `false`
-- Environment variable: `FLEET_LICENSE_ENFORCE_HOST_LIMIT`
-- Config file format:
- ```yaml
- license:
- enforce_host_limit: true
- ```
-
-##### Example YAML
-
-```yaml
-license:
- key: foobar
- enforce_host_limit: false
-```
-
#### Session
##### session_key_size
diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md
index 15271d1cc719..25b80cdc7463 100644
--- a/docs/Configuration/yaml-files.md
+++ b/docs/Configuration/yaml-files.md
@@ -4,20 +4,6 @@ Use Fleet's best practice GitOps workflow to manage your computers as code.
To learn how to set up a GitOps workflow see the [Fleet GitOps repo](https://github.com/fleetdm/fleet-gitops).
-## File structure
-
-- `default.yml` - File where you define the queries, policies and agent options for all hosts. If you're using Fleet Premium, this file updates queries and policies that run on all hosts ("All teams").
-- `teams/no-team.yml` - File where you define the policies, controls, and software for hosts on "No team". Available in Fleet Premium.
-- `teams/` - Folder where you define your teams in Fleet. These `teams/team-name.yml` files define the controls, queries, policies, software, and agent options for hosts assigned to the specified team. Available in Fleet Premium.
-- `lib/` - Folder where you define policies, queries, configuration profiles, scripts, and agent options. These files can be referenced in top level keys in the `default.yml` file and the files in the `teams/` folder.
-- `.github/workflows/workflow.yml` - The GitHub workflow file where you can add [environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow).
-
-The following files are responsible for running the GitHub action or GitLab CI/CD. Most users don't need to edit these files.
-- `gitops.sh` - The bash script that applies the latest configuration to Fleet. This script is used in the GitHub action file.
-- `.github/gitops-action/action.yml` - The GitHub action that runs `gitops.sh`. This action is used in the GitHub workflow file. It can also be used in other workflows.
-- `.gitlab-ci.yml` - The GitLab CI/CD file that applies the latest configuration to Fleet.
-
-## Configuration options
The following are the required keys in the `default.yml` and any `teams/team-name.yml` files:
@@ -31,14 +17,7 @@ org_settings: # Only default.yml
team_settings: # Only teams/team-name.yml
```
-- [policies](#policies)
-- [queries](#queries)
-- [agent_options](#agent-options)
-- [controls](#controls)
-- [software](#software)
-- [org_settings and team_settings](#org-settings-and-team-settings)
-
-### policies
+## policies
Policies can be specified inline in your `default.yml`, `teams/team-name.yml`, or `teams/no-team.yml` files. They can also be specified in separate files in your `lib/` folder.
@@ -48,13 +27,13 @@ Policies defined in `teams/no-team.yml` run on hosts that belong to "No team".
> Policies that run automations to install software or run scripts must be defined in `teams/no-team.yml` to run on hosts that belong to "No team".
-#### Options
+### Options
For possible options, see the parameters for the [Add policy API endpoint](https://fleetdm.com/docs/rest-api/rest-api#add-policy).
-#### Example
+### Example
-##### Inline
+#### Inline
`default.yml`, `teams/team-name.yml`, or `teams/no-team.yml`
@@ -69,7 +48,7 @@ policies:
calendar_event_enabled: false
```
-##### Separate file
+#### Separate file
`lib/policies-name.policies.yml`
@@ -107,19 +86,19 @@ policies:
# path is relative to default.yml or teams/team-name.yml
```
-### queries
+## queries
Queries can be specified inline in your `default.yml` file or `teams/team-name.yml` files. They can also be specified in separate files in your `lib/` folder.
Note that the `team_id` option isn't supported in GitOps.
-#### Options
+### Options
For possible options, see the parameters for the [Create query API endpoint](https://fleetdm.com/docs/rest-api/rest-api#create-query).
-#### Example
+### Example
-##### Inline
+#### Inline
`default.yml` or `teams/team-name.yml`
@@ -134,7 +113,7 @@ queries:
automations_enabled: false
```
-##### Separate file
+#### Separate file
`lib/queries-name.queries.yml`
@@ -163,15 +142,15 @@ queries:
# path is relative to default.yml or teams/team-name.yml
```
-### agent_options
+## agent_options
Agent options can be specified inline in your `default.yml` file or `teams/team-name.yml` files. They can also be specified in separate files in your `lib/` folder.
See "[Agent configuration](https://fleetdm.com/docs/configuration/agent-configuration)" to find all possible options.
-#### Example
+### Example
-##### Inline
+#### Inline
`default.yml` or `teams/team-name.yml`
@@ -192,7 +171,7 @@ agent_options:
pack_delimiter: /
```
-##### Separate file
+#### Separate file
`lib/agent-options.yml`
@@ -222,7 +201,7 @@ queries:
# path is relative to default.yml or teams/team-name.yml
```
-### controls
+## controls
The `controls` section allows you to configure scripts and device management (MDM) features in Fleet.
@@ -232,7 +211,7 @@ Controls for hosts that are in "No team" can be defined in `default.yml` or in `
- `windows_enabled_and_configured` specifies whether or not to turn on Windows MDM features (default: `false`). Can only be configured for all teams (`default.yml`).
- `enable_disk_encryption` specifies whether or not to enforce disk encryption on macOS and Windows hosts (default: `false`).
-##### Example
+#### Example
```yaml
controls:
@@ -270,27 +249,27 @@ controls:
# paths are relative to default.yml or teams/team-name.yml
```
-#### macos_updates
+### macos_updates
- `deadline` specifies the deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8) (default: `""`).
- `minimum_version` specifies the minimum required macOS version (default: `""`).
-#### windows_updates
+### windows_updates
- `deadline_days` (default: null)
- `grace_period_days` (default: null)
-#### ios_updates
+### ios_updates
- `deadline` specifies the deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8) (default: `""`).
- `minimum_version` specifies the minimum required iOS version (default: `""`).
-#### ipados_updates
+### ipados_updates
- `deadline` specifies the deadline in the form of `YYYY-MM-DD`. The exact deadline time is at 04:00:00 (UTC-8) (default: `""`).
- `minimum_version` specifies the minimum required iPadOS version (default: `""`).
-#### macos_settings and windows_settings
+### macos_settings and windows_settings
- `macos_settings.custom_settings` is a list of paths to macOS configuration profiles (.mobileconfig) or declaration profiles (.json).
- `windows_settings.custom_settings` is a list of paths to Windows configuration profiles (.xml).
@@ -299,7 +278,7 @@ Fleet supports adding [GitHub environment variables](https://docs.github.com/en/
Use `labels_include_all` to only apply (scope) profiles to hosts that have all those labels or `labels_exclude_any` to apply profiles to hosts that don't have any of those labels.
-#### macos_setup
+### macos_setup
The `macos_setup` section lets you control the out-of-the-box macOS [setup experience](https://fleetdm.com/guides/macos-setup-experience) for hosts that use Automated Device Enrollment (ADE).
@@ -307,7 +286,7 @@ The `macos_setup` section lets you control the out-of-the-box macOS [setup exper
- `enable_end_user_authentication` specifies whether or not to require end user authentication when the user first sets up their macOS host.
- `macos_setup_assistant` is a path to a custom automatic enrollment (ADE) profile (.json).
-#### macos_migration
+### macos_migration
The `macos_migration` section lets you control the [end user migration workflow](https://fleetdm.com/docs/using-fleet/mdm-migration-guide#end-user-workflow) for macOS hosts that enrolled to your old MDM solution.
@@ -317,7 +296,7 @@ The `macos_migration` section lets you control the [end user migration workflow]
Can only be configure for all teams (`default.yml`).
-### software
+## software
> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
@@ -328,9 +307,9 @@ Software can also be specified in separate files in your `lib/` folder.
- `packages` is a list of software packages (.pkg, .msi, .exe, .rpm, or .deb) and software specific options.
- `app_store_apps` is a list of Apple App Store apps.
-#### Example
+### Example
-##### Inline
+#### Inline
```yaml
software:
@@ -348,7 +327,7 @@ software:
- app_store_id: '1091189122'
```
-##### packages
+#### packages
- `url` specifies the URL at which the software is located. Fleet will download the software and upload it to S3 (default: `""`).
- `install_script.path` specifies the command Fleet will run on hosts to install software. The [default script](https://github.com/fleetdm/fleet/tree/main/pkg/file/scripts) is dependent on the software type (i.e. .pkg).
@@ -356,7 +335,7 @@ software:
- `post_install_script.path` is the script Fleet will run on hosts after intalling software (default: `""`).
- `self_service` specifies whether or not end users can install from **Fleet Desktop > Self-service**.
-##### app_store_apps
+#### app_store_apps
- `app_store_id` is the ID of the Apple App Store app. You can find this at the end of the app's App Store URL. For example, "Bear - Markdown Notes" URL is "https://apps.apple.com/us/app/bear-markdown-notes/id1016366447" and the `app_store_id` is `1016366447`.
@@ -364,7 +343,7 @@ software:
`self_service` only applies to macOS, and is ignored for other platforms. For example, if the app is supported on macOS, iOS, and iPadOS, and `self_service` is set to `true`, it will be self-service on macOS workstations but not iPhones or iPads.
-##### Separate file
+#### Separate file
`lib/software-name.package.yml`:
@@ -393,16 +372,16 @@ software:
# path is relative to default.yml or teams/team-name.yml
```
-### org_settings and team_settings
+## org_settings and team_settings
-#### features
+### features
The `features` section of the configuration YAML lets you define what predefined queries are sent to the hosts and later on processed by Fleet for different functionalities.
- `additional_queries` adds extra host details. This information will be updated at the same time as other host details and is returned by the API when host objects are returned (default: empty).
- `enable_host_users` specifies whether or not Fleet collects user data from hosts (default: `true`).
- `enable_software_inventory` specifies whether or not Fleet collects softwre inventory from hosts (default: `true`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -414,13 +393,13 @@ org_settings:
enable_software_inventory: true
```
-#### fleet_desktop
+### fleet_desktop
Direct end users to a custom URL when they select **Transparency** in the Fleet Desktop dropdown (default: [https://fleetdm.com/transparency](https://fleetdm.com/transparency)).
Can only be configured for all teams (`org_settings`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -428,13 +407,13 @@ org_settings:
transparency_url: "https://example.org/transparency"
```
-#### host_expiry_settings
+### host_expiry_settings
The `host_expiry_settings` section lets you define if and when hosts should be automatically deleted from Fleet if they have not checked in.
- `host_expiry_enabled` (default: `false`)
- `host_expiry_window` if a host has not communicated with Fleet in the specified number of days, it will be removed. Must be > `0` when host expiry is enabled (default: `0`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -443,7 +422,7 @@ org_settings:
host_expiry_window: 10
```
-#### org_info
+### org_info
- `name` is the name of your organization (default: `""`)
- `logo_url` is a public URL of the logo for your organization (default: Fleet logo).
@@ -452,7 +431,7 @@ org_settings:
Can only be configured for all teams (`org_settings`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -463,11 +442,11 @@ org_settings:
contact_url: https://fleetdm.com/company/contact
```
-#### secrets
+### secrets
The `secrets` section defines the valid secrets that hosts can use to enroll to Fleet. Supply one of these secrets when generating the fleetd agent you'll use to enroll hosts. Learn more [here](https://fleetdm.com/docs/using-fleet/enroll-hosts).
-##### Example
+#### Example
```yaml
org_settings:
@@ -475,18 +454,18 @@ org_settings:
- $ENROLL_SECRET
```
-#### server_settings
+### server_settings
- `enable_analytics` specifies whether or not to enable Fleet's [usage statistics](https://fleetdm.com/docs/using-fleet/usage-statistics) (default: `true`).
- `live_query_disabled` disables the ability to run live queries (ad hoc queries executed via the UI or fleetctl) (default: `false`).
- `query_reports_disabled` disables query reports and deletes existing repors (default: `false`).
- `query_report_cap` sets the maximum number of results to store per query report before the report is clipped. If increasing this cap, we recommend enabling reports for one query at time and monitoring your infrastructure. (Default: `1000`)
-- `scripts_disabled` blocks access to run scripts. Scripts may still be added in the UI and CLI (defaul: `false`).
+- `scripts_disabled` blocks access to run scripts. Scripts may still be added in the UI and CLI (default: `false`).
- `server_url` is the base URL of the Fleet instance (default: provided during Fleet setup)
Can only be configured for all teams (`org_settings`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -499,7 +478,7 @@ org_settings:
```
-#### sso_settings
+### sso_settings
The `sso_settings` section lets you define single sign-on (SSO) settings. Learn more about SSO in Fleet [here](https://fleetdm.com/docs/deploying/configuration#configuring-single-sign-on-sso).
@@ -513,7 +492,7 @@ The `sso_settings` section lets you define single sign-on (SSO) settings. Learn
Can only be configured for all teams (`org_settings`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -526,11 +505,11 @@ org_settings:
enable_sso_idp_login: true
```
-#### integrations
+### integrations
The `integrations` section lets you define calendar events and ticket settings for failing policy and vulnerablity automations. Learn more about automations in Fleet [here](https://fleetdm.com/docs/using-fleet/automations).
-##### Example
+#### Example
```yaml
org_settings:
@@ -552,37 +531,37 @@ org_settings:
For secrets, you can add [GitHub environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow)
-##### google_calendar
+#### google_calendar
- `api_key_json` is the contents of the JSON file downloaded when you create your Google Workspace service account API key (default: `""`).
- `domain` is the primary domain used to identify your end user's work calendar (default: `""`).
-##### jira
+#### jira
- `url` is the URL of your Jira (default: `""`)
- `username` is the username of your Jira account (default: `""`).
- `api_token` is the Jira API token (default: `""`).
- `project_key` is the project key location in your Jira project's URL. For example, in "jira.example.com/projects/EXMPL," "EXMPL" is the project key (default: `""`).
-##### zendesk
+#### zendesk
- `url` is the URL of your Zendesk (default: `""`)
- `username` is the username of your Zendesk account (default: `""`).
- `api_token` is the Zendesk API token (default: `""`).
- `group_id`is found by selecting **Admin > People > Groups** in Zendesk. Find your group and select it. The group ID will appear in the search field.
-#### webhook_settings
+### webhook_settings
The `webhook_settings` section lets you define webhook settings for failing policy, vulnerability, and host status automations. Learn more about automations in Fleet [here](https://fleetdm.com/docs/using-fleet/automations).
-##### failing_policies_webhook
+#### failing_policies_webhook
- `enable_failing_policies_webhook` (default: `false`)
- `destination_url` is the URL to `POST` to when the condition for the webhook triggers (default: `""`).
- `policy_ids` is the list of policies that will trigger a webhook.
- `host_batch_size` is the maximum number of hosts to batch in each webhook. A value of `0` means no batching (default: `0`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -597,14 +576,14 @@ org_settings:
- 3
```
-##### host_status_webhook
+#### host_status_webhook
- `enable_host_status_webhook` (default: `false`)
- `destination_url` is the URL to `POST` to when the condition for the webhook triggers (default: `""`).
- `days_count` is the number of days that hosts need to be offline to count as part of the percentage (default: `0`).
- `host_percentage` is the percentage of hosts that need to be offline to trigger the webhook. (default: `0`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -616,14 +595,14 @@ org_settings:
host_percentage: 25
```
-##### vulnerabilities_webhook
+#### vulnerabilities_webhook
- `enable_vulnerabilities_webhook` (default: `false`)
- `destination_url` is the URL to `POST` to when the condition for the webhook triggers (default: `""`).
- `days_count` is the number of days that hosts need to be offline to count as part of the percentage (default: `0`).
- `host_batch_size` is the maximum number of hosts to batch in each webhook. A value of `0` means no batching (default: `0`).
-##### Example
+#### Example
```yaml
org_settings:
@@ -636,16 +615,16 @@ org_settings:
Can only be configured for all teams (`org_settings`).
-#### mdm
+### mdm
-##### apple_business_manager
+#### apple_business_manager
- `organization_name` is the organization name associated with the Apple Business Manager account.
- `macos_team` is the team where macOS hosts are automatically added when they appear in Apple Business Manager.
- `ios_team` is the the team where iOS hosts are automatically added when they appear in Apple Business Manager.
- `ipados_team` is the team where iPadOS hosts are automatically added when they appear in Apple Business Manager.
-##### Example
+#### Example
```yaml
org_settings:
@@ -659,12 +638,12 @@ org_settings:
> Apple Business Manager settings can only be configured for all teams (`org_settings`).
-##### volume_purchasing_program
+#### volume_purchasing_program
- `location` is the name of the location in the Apple Business Manager account.
- `teams` is a list of team names. If you choose specific teams, App Store apps in this VPP account will only be available to install on hosts in these teams. If not specified, App Store apps are available to install on hosts in all teams.
-##### Example
+#### Example
```yaml
org_settings:
@@ -680,7 +659,7 @@ org_settings:
Can only be configured for all teams (`org_settings`).
-##### end_user_authentication
+#### end_user_authentication
The `end_user_authentication` section lets you define the identity provider (IdP) settings used for end user authentication during Automated Device Enrollment (ADE). Learn more about end user authentication in Fleet [here](https://fleetdm.com/guides/macos-setup-experience#end-user-authentication-and-eula).
@@ -693,7 +672,7 @@ Once the IdP settings are configured, you can use the [`controls.macos_setup.ena
Can only be configured for all teams (`org_settings`).
-##### end_user_authentication
+#### end_user_authentication
The `end_user_authentication` section lets you define the identity provider (IdP) settings used for end user authentication during Automated Device Enrollment (ADE). Learn more about end user authentication in Fleet [here](https://fleetdm.com/guides/macos-setup-experience#end-user-authentication-and-eula).
diff --git a/docs/Contributing/Configuration-for-contributors.md b/docs/Contributing/Configuration-for-contributors.md
index 7e3c6798ce4d..ce3dc4e574ba 100644
--- a/docs/Contributing/Configuration-for-contributors.md
+++ b/docs/Contributing/Configuration-for-contributors.md
@@ -156,6 +156,26 @@ This is the content of the PEM-encoded private key for the Apple Business Manage
-----END RSA PRIVATE KEY-----
```
+##### license.enforce_host_limit
+
+Whether Fleet should enforce the host limit of the license, if true, attempting to enroll new hosts when the limit is reached will fail.
+
+- Default value: `false`
+- Environment variable: `FLEET_LICENSE_ENFORCE_HOST_LIMIT`
+- Config file format:
+ ```yaml
+ license:
+ enforce_host_limit: true
+ ```
+
+##### Example YAML
+
+```yaml
+license:
+ key: foobar
+ enforce_host_limit: false
+```
+
## Environment variables
### FLEET_ENABLE_POST_CLIENT_DEBUG_ERRORS
diff --git a/docs/Deploy/deploy-fleet.md b/docs/Deploy/deploy-fleet.md
index 672e6283b09c..b1771569a242 100644
--- a/docs/Deploy/deploy-fleet.md
+++ b/docs/Deploy/deploy-fleet.md
@@ -33,7 +33,7 @@ Render is a cloud hosting service that makes it easy to get up and running fast,
- A Render account with payment information.
->The Fleet Render Blueprint will provision a web service, a MySQL database, and a Redis in-memory data store. Each service requires Render's standard plan at a cost of **$7/month** each, totaling **$21/month**.
+>The Fleet Render Blueprint will provision a web service, a MySQL database, and a Redis in-memory data store. At current pricing this will total **$62/month**.
### Instructions
diff --git a/docs/Get started/FAQ.md b/docs/Get started/FAQ.md
index 5c7feeacbdb0..706f32c353cb 100644
--- a/docs/Get started/FAQ.md
+++ b/docs/Get started/FAQ.md
@@ -73,13 +73,13 @@ We test each browser on Windows whenever possible, because our engineering team
Fleet supports the following operating system versions on hosts.
-| OS | Supported version(s) |
-| :------ | :------------------------------------- |
-| macOS | 13+ (Ventura) |
-| iOS | 17+ |
-| Windows | Pro and Enterprise 10+, Server 2012+ |
-| Linux | CentOS 7.1+, Ubuntu 20.04+, Fedora 38+ |
-| ChromeOS | 112.0.5615.134+ |
+| OS | Supported version(s) |
+| :--------- | :-------------------------------------- |
+| macOS | 13+ (Ventura) |
+| iOS/iPadOS | 17+ |
+| Windows | Pro and Enterprise 10+, Server 2012+ |
+| Linux | CentOS 7.1+, Ubuntu 20.04+, Fedora 38+ |
+| ChromeOS | 112.0.5615.134+ |
While Fleet may still function partially or fully with OS versions older than those above, Fleet does not actively test against unsupported versions and does not pursue bugs on them.
diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md
index 6dc53f0289f8..efa44a27066d 100644
--- a/docs/REST API/rest-api.md
+++ b/docs/REST API/rest-api.md
@@ -3686,6 +3686,8 @@ _Available in Fleet Premium_
### Turn off MDM for a host
+Turns off MDM for the specified macOS host.
+
`DELETE /api/v1/fleet/hosts/:id/mdm`
#### Parameters
@@ -5865,7 +5867,7 @@ Learn more about OTA profiles [here](https://developer.apple.com/library/archive
```http
Content-Length: 542
- Content-Type: application/x-apple-aspen-config; charset=urf-8
+ Content-Type: application/x-apple-aspen-config; charset=utf-8
Content-Disposition: attachment;filename="fleet-mdm-enrollment-profile.mobileconfig"
X-Content-Type-Options: nosniff
```
@@ -9293,7 +9295,7 @@ Add a package (.pkg, .msi, .exe, .deb, .rpm) to install on macOS, Windows, or Li
| Name | Type | In | Description |
| ---- | ------- | ---- | -------------------------------------------- |
-| software | file | form | **Required**. Installer package file. Supported packages are PKG, MSI, EXE, DEB, and RPM. |
+| software | file | form | **Required**. Installer package file. Supported packages are .pkg, .msi, .exe, .deb, and .rpm. |
| team_id | integer | form | **Required**. The team ID. Adds a software package to the specified team. |
| install_script | string | form | Script that Fleet runs to install software. If not specified Fleet runs [default install script](https://github.com/fleetdm/fleet/tree/f71a1f183cc6736205510580c8366153ea083a8d/pkg/file/scripts) for each package type. |
| pre_install_query | string | form | Query that is pre-install condition. If the query doesn't return any result, Fleet won't proceed to install. |
@@ -9354,7 +9356,7 @@ Update a package to install on macOS, Windows, or Linux (Ubuntu) hosts.
| Name | Type | In | Description |
| ---- | ------- | ---- | -------------------------------------------- |
-| software | file | form | Installer package file. Supported packages are PKG, MSI, EXE, DEB, and RPM. |
+| software | file | form | Installer package file. Supported packages are .pkg, .msi, .exe, .deb, and .rpm. |
| team_id | integer | form | **Required**. The team ID. Updates a software package in the specified team. |
| install_script | string | form | Command that Fleet runs to install software. If not specified Fleet runs the [default install command](https://github.com/fleetdm/fleet/tree/f71a1f183cc6736205510580c8366153ea083a8d/pkg/file/scripts) for each package type. |
| pre_install_query | string | form | Query that is pre-install condition. If the query doesn't return any result, the package will not be installed. |
diff --git a/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js b/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js
index a840b666d706..abe14930ea66 100644
--- a/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js
+++ b/ee/bulk-operations-dashboard/api/controllers/software/edit-software.js
@@ -61,7 +61,12 @@ module.exports = {
// let { Readable } = require('stream');
let axios = require('axios');
// Cast the strings in the newTeamIds array to numbers.
- newTeamIds = newTeamIds.map(Number);
+ if(newTeamIds){
+ newTeamIds = newTeamIds.map(Number);
+ } else {
+ newTeamIds = [];
+ }
+
let currentSoftwareTeamIds = _.pluck(software.teams, 'fleetApid');
// If the teams have changed, or a new installer package was provided, we'll need to upload the package to an s3 bucket to deploy it to other teams.
if(_.xor(newTeamIds, currentSoftwareTeamIds).length !== 0 || newSoftware) {
@@ -248,6 +253,28 @@ module.exports = {
});
// console.timeEnd(`transfering ${software.name} to fleet instance for team id ${teamApid}`);
});// After every team the software is currently deployed to.
+ } else if(preInstallQuery !== software.preInstallQuery ||
+ installScript !== software.installScript ||
+ postInstallScript !== software.postInstallScript ||
+ uninstallScript !== software.uninstallScript) {
+ await sails.helpers.flow.simultaneouslyForEach(unchangedTeamIds, async (teamApid)=>{
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'PATCH',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/software/titles/${software.fleetApid}/package?team_id=${teamApid}`,
+ enctype: 'multipart/form-data',
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`
+ },
+ body: {
+ team_id: teamApid, // eslint-disable-line camelcase
+ pre_install_query: preInstallQuery, // eslint-disable-line camelcase
+ install_script: installScript, // eslint-disable-line camelcase
+ post_install_script: postInstallScript, // eslint-disable-line camelcase
+ uninstall_script: uninstallScript, // eslint-disable-line camelcase
+ }
+ });
+ });
}
// Now delete the software from teams it was removed from.
for(let team of removedTeams) {
@@ -275,7 +302,11 @@ module.exports = {
uploadFd: softwareFd,
uploadMime: softwareMime,
name: softwareName,
- platform: _.endsWith(softwareName, '.deb') ? 'Linux' : _.endsWith(softwareName, '.pkg') ? 'macOS' : 'Windows',
+ platform: software.platform,
+ postInstallScript,
+ preInstallQuery,
+ installScript,
+ uninstallScript,
});
} else {
// Save the information about the undeployed software in the app's DB.
@@ -289,17 +320,17 @@ module.exports = {
installScript,
uninstallScript,
});
- // Now delete the software on the Fleet instance.
- for(let team of software.teams) {
- await sails.helpers.http.sendHttpRequest.with({
- method: 'DELETE',
- baseUrl: sails.config.custom.fleetBaseUrl,
- url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team.fleetApid}`,
- headers: {
- Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
- }
- });
- }
+ }
+ // Now delete the software on the Fleet instance.
+ for(let team of software.teams) {
+ await sails.helpers.http.sendHttpRequest.with({
+ method: 'DELETE',
+ baseUrl: sails.config.custom.fleetBaseUrl,
+ url: `/api/v1/fleet/software/titles/${software.fleetApid}/available_for_install?team_id=${team.fleetApid}`,
+ headers: {
+ Authorization: `Bearer ${sails.config.custom.fleetApiToken}`,
+ }
+ });
}
} else {
@@ -308,6 +339,10 @@ module.exports = {
name: softwareName,
uploadMime: softwareMime,
uploadFd: softwareFd,
+ preInstallQuery,
+ installScript,
+ postInstallScript,
+ uninstallScript,
});
// console.log('removing old stored copy of '+softwareName);
await sails.rm(sails.config.uploads.prefixForFileDeletion+software.uploadFd);
diff --git a/ee/bulk-operations-dashboard/assets/js/components/ace-editor.component.js b/ee/bulk-operations-dashboard/assets/js/components/ace-editor.component.js
index 094b21866c6d..e3a3ea2ef07b 100644
--- a/ee/bulk-operations-dashboard/assets/js/components/ace-editor.component.js
+++ b/ee/bulk-operations-dashboard/assets/js/components/ace-editor.component.js
@@ -47,7 +47,7 @@ parasails.registerComponent('aceEditor', {
// ╩ ╩ ╩ ╩ ╩╩═╝
template: `
`,
diff --git a/ee/bulk-operations-dashboard/assets/js/pages/software/software.page.js b/ee/bulk-operations-dashboard/assets/js/pages/software/software.page.js
index 3eb72e810932..a9c50ef8a279 100644
--- a/ee/bulk-operations-dashboard/assets/js/pages/software/software.page.js
+++ b/ee/bulk-operations-dashboard/assets/js/pages/software/software.page.js
@@ -60,13 +60,13 @@ parasails.registerPage('software', {
}
},
clickOpenEditModal: async function(software) {
- this.softwareToEdit = _.clone(software);
+ this.softwareToEdit = _.cloneDeep(software);
this.formData.newTeamIds = _.pluck(this.softwareToEdit.teams, 'fleetApid');
this.formData.software = software;
- this.formData.preInstallQuery = software.preInstallQuery;
- this.formData.installScript = software.installScript;
- this.formData.postInstallScript = software.postInstallScript;
- this.formData.uninstallScript = software.uninstallScript;
+ this.formData.preInstallQuery = this.softwareToEdit.preInstallQuery;
+ this.formData.installScript = this.softwareToEdit.installScript;
+ this.formData.postInstallScript = this.softwareToEdit.postInstallScript;
+ this.formData.uninstallScript = this.softwareToEdit.uninstallScript;
this.modal = 'edit-software';
},
clickOpenDeleteModal: async function(software) {
@@ -108,9 +108,11 @@ parasails.registerPage('software', {
this.softwareToDisplay = softwareOnThisTeam;
},
handleSubmittingEditSoftwareForm: async function() {
- let argins = _.clone(this.formData);
- if(argins.newTeamIds === [undefined]){
- argins.newTeamIds = [];
+ let argins = _.cloneDeep(this.formData);
+ if(argins.newTeamIds[0] === undefined){
+ argins.newTeamIds = undefined;
+ } else {
+ argins.newTeamIds = _.uniq(argins.newTeamIds);
}
await Cloud.editSoftware.with(argins);
if(!this.cloudError) {
diff --git a/ee/bulk-operations-dashboard/package.json b/ee/bulk-operations-dashboard/package.json
index b19e2f9a18f3..f36c7a9e5073 100644
--- a/ee/bulk-operations-dashboard/package.json
+++ b/ee/bulk-operations-dashboard/package.json
@@ -20,7 +20,7 @@
},
"devDependencies": {
"eslint": "5.16.0",
- "grunt": "1.0.4",
+ "grunt": "1.5.3",
"htmlhint": "0.11.0",
"lesshint": "6.3.6",
"sails-hook-grunt": "^5.0.0"
diff --git a/ee/bulk-operations-dashboard/views/pages/software/software.ejs b/ee/bulk-operations-dashboard/views/pages/software/software.ejs
index a4282aae0455..f9ab497303cc 100644
--- a/ee/bulk-operations-dashboard/views/pages/software/software.ejs
+++ b/ee/bulk-operations-dashboard/views/pages/software/software.ejs
@@ -187,7 +187,7 @@
Please select the teams you want to deploy this software to.
- A software with the same name as the uploaded software already exists on one or more of the selected teams.
+ A software with the same name as the uploaded software already exists on one or more of the selected teams.
Cancel
diff --git a/ee/server/service/vpp.go b/ee/server/service/vpp.go
index dc1c1c461e74..453bdcee132d 100644
--- a/ee/server/service/vpp.go
+++ b/ee/server/service/vpp.go
@@ -86,6 +86,12 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
}}...)
}
+ if dryRun {
+ // On dry runs return early because the VPP token might not exist yet
+ // and we don't want to apply the VPP apps.
+ return nil
+ }
+
var vppAppTeams []fleet.VPPAppTeam
// Don't check for token if we're only disassociating assets
if len(payloads) > 0 {
@@ -136,35 +142,33 @@ func (svc *Service) BatchAssociateVPPApps(ctx context.Context, teamName string,
}
}
- if !dryRun {
- if len(vppAppTeams) > 0 {
- apps, err := getVPPAppsMetadata(ctx, vppAppTeams)
- if err != nil {
- return ctxerr.Wrap(ctx, err, "refreshing VPP app metadata")
- }
- if len(apps) == 0 {
- return fleet.NewInvalidArgumentError("app_store_apps",
- "no valid apps found matching the provided app store IDs and platforms")
- }
-
- if err := svc.ds.BatchInsertVPPApps(ctx, apps); err != nil {
- return ctxerr.Wrap(ctx, err, "inserting vpp app metadata")
- }
- // Filter out the apps with invalid platforms
- if len(apps) != len(vppAppTeams) {
- vppAppTeams = make([]fleet.VPPAppTeam, 0, len(apps))
- for _, app := range apps {
- vppAppTeams = append(vppAppTeams, app.VPPAppTeam)
- }
- }
+ if len(vppAppTeams) > 0 {
+ apps, err := getVPPAppsMetadata(ctx, vppAppTeams)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "refreshing VPP app metadata")
+ }
+ if len(apps) == 0 {
+ return fleet.NewInvalidArgumentError("app_store_apps",
+ "no valid apps found matching the provided app store IDs and platforms")
+ }
+ if err := svc.ds.BatchInsertVPPApps(ctx, apps); err != nil {
+ return ctxerr.Wrap(ctx, err, "inserting vpp app metadata")
}
- if err := svc.ds.SetTeamVPPApps(ctx, &team.ID, vppAppTeams); err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- return fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "no vpp token to set team vpp assets"), http.StatusUnprocessableEntity)
+ // Filter out the apps with invalid platforms
+ if len(apps) != len(vppAppTeams) {
+ vppAppTeams = make([]fleet.VPPAppTeam, 0, len(apps))
+ for _, app := range apps {
+ vppAppTeams = append(vppAppTeams, app.VPPAppTeam)
}
- return ctxerr.Wrap(ctx, err, "set team vpp assets")
}
+
+ }
+ if err := svc.ds.SetTeamVPPApps(ctx, &team.ID, vppAppTeams); err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return fleet.NewUserMessageError(ctxerr.Wrap(ctx, err, "no vpp token to set team vpp assets"), http.StatusUnprocessableEntity)
+ }
+ return ctxerr.Wrap(ctx, err, "set team vpp assets")
}
return nil
diff --git a/frontend/components/ActionsDropdown/ActionsDropdown.tests.tsx b/frontend/components/ActionsDropdown/ActionsDropdown.tests.tsx
new file mode 100644
index 000000000000..a1f7a71c14c5
--- /dev/null
+++ b/frontend/components/ActionsDropdown/ActionsDropdown.tests.tsx
@@ -0,0 +1,92 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import { renderWithSetup } from "test/test-utils";
+
+import ActionsDropdown from "./ActionsDropdown";
+
+const DROPDOWN_OPTIONS = [
+ { disabled: false, label: "Edit", value: "edit-query" },
+ { disabled: false, label: "Show query", value: "show-query" },
+ { disabled: true, label: "Delete", value: "delete-query" },
+];
+const PLACEHOLDER = "Actions";
+const ON_CHANGE = (value: string) => {
+ console.log(value);
+};
+
+describe("Actions dropdown", () => {
+ it("renders dropdown placeholder and options", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.click(screen.getByText("Actions"));
+
+ expect(screen.queryAllByText(/edit/i)[1]).toBeInTheDocument(); // Aria shows Edit twice since it's focused
+ expect(screen.queryByText(/show query/i)).toBeInTheDocument();
+ expect(screen.queryByText(/delete/i)).toBeInTheDocument();
+ });
+
+ it("renders dropdown as disabled when disabled prop is true", () => {
+ renderWithSetup(
+
+ );
+ expect(screen.getByRole("combobox")).toBeDisabled();
+ });
+
+ it("calls onChange with correct value when an option is selected", async () => {
+ const mockOnChange = jest.fn();
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.click(screen.getByText("Actions"));
+ await user.click(screen.getByText("Edit"));
+
+ expect(mockOnChange).toHaveBeenCalledWith("edit-query");
+ });
+
+ it("renders disabled option as non-selectable", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.click(screen.getByText("Actions"));
+ const deleteOption = screen.getByText("Delete");
+
+ expect(deleteOption).toHaveAttribute("aria-disabled", "true");
+ });
+
+ it("closes the dropdown when clicking outside", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ await user.click(screen.getByText("Actions"));
+ expect(screen.getByText("Edit")).toBeVisible();
+
+ await user.click(document.body);
+ expect(screen.queryByText(/edit/i)).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/ActionsDropdown/ActionsDropdown.tsx b/frontend/components/ActionsDropdown/ActionsDropdown.tsx
new file mode 100644
index 000000000000..9e068bf735d6
--- /dev/null
+++ b/frontend/components/ActionsDropdown/ActionsDropdown.tsx
@@ -0,0 +1,252 @@
+import React from "react";
+import Select, {
+ StylesConfig,
+ DropdownIndicatorProps,
+ OptionProps,
+ components,
+} from "react-select-5";
+
+import { PADDING } from "styles/var/padding";
+import { COLORS } from "styles/var/colors";
+import classnames from "classnames";
+
+import { IDropdownOption } from "interfaces/dropdownOption";
+
+import Icon from "components/Icon";
+import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper";
+
+const baseClass = "actions-dropdown";
+
+interface IActionsDropdownProps {
+ options: IDropdownOption[];
+ placeholder: string;
+ onChange: (value: string) => void;
+ disabled?: boolean;
+ isSearchable?: boolean;
+ className?: string;
+ menuAlign?: "right" | "left" | "default";
+}
+
+const getOptionBackgroundColor = (state: any) => {
+ return state.isSelected || state.isFocused
+ ? COLORS["ui-vibrant-blue-10"]
+ : "transparent";
+};
+
+const getLeftMenuAlign = (menuAlign: "right" | "left" | "default") => {
+ switch (menuAlign) {
+ case "right":
+ return "auto";
+ case "left":
+ return "0";
+ default:
+ return "-12px";
+ }
+};
+
+const getRightMenuAlign = (menuAlign: "right" | "left" | "default") => {
+ switch (menuAlign) {
+ case "right":
+ return "0";
+ default:
+ return "undefined";
+ }
+};
+
+const CustomDropdownIndicator = (
+ props: DropdownIndicatorProps
+) => {
+ const { isFocused, selectProps } = props;
+ // no access to hover state here from react-select so that is done in the scss
+ // file of ActionsDropdown.
+ const color =
+ isFocused || selectProps.menuIsOpen
+ ? "core-fleet-blue"
+ : "core-fleet-black";
+
+ return (
+
+
+
+ );
+};
+
+const CustomOption: React.FC> = (props) => {
+ const { innerProps, innerRef, data, isDisabled } = props;
+
+ const optionContent = (
+
+ {data.label}
+ {data.helpText && (
+ {data.helpText}
+ )}
+
+ );
+
+ return (
+
+ {data.tooltipContent ? (
+
+ {optionContent}
+
+ ) : (
+ optionContent
+ )}
+
+ );
+};
+
+const ActionsDropdown = ({
+ options,
+ placeholder,
+ onChange,
+ disabled,
+ isSearchable = false,
+ className,
+ menuAlign = "default",
+}: IActionsDropdownProps): JSX.Element => {
+ const dropdownClassnames = classnames(baseClass, className);
+
+ const handleChange = (newValue: IDropdownOption | null) => {
+ if (newValue) {
+ onChange(newValue.value.toString());
+ }
+ };
+
+ const customStyles: StylesConfig = {
+ container: (provided) => ({
+ ...provided,
+ width: "80px",
+ }),
+ control: (provided, state) => ({
+ ...provided,
+ display: "flex",
+ flexDirection: "row",
+ width: "max-content",
+ padding: "8px 0",
+ backgroundColor: "initial",
+ border: 0,
+ boxShadow: "none",
+ cursor: "pointer",
+ "&:hover": {
+ boxShadow: "none",
+ ".actions-dropdown-select__placeholder": {
+ color: COLORS["core-vibrant-blue-over"],
+ },
+ ".actions-dropdown-select__indicator path": {
+ stroke: COLORS["core-vibrant-blue-over"],
+ },
+ },
+ "&:active .actions-dropdown-select__indicator path": {
+ stroke: COLORS["core-vibrant-blue-down"],
+ },
+ // TODO: Figure out a way to apply separate &:focus-visible styling
+ // Currently only relying on &:focus styling for tabbing through app
+ ...(state.menuIsOpen && {
+ ".actions-dropdown-select__indicator svg": {
+ transform: "rotate(180deg)",
+ transition: "transform 0.25s ease",
+ },
+ }),
+ }),
+ placeholder: (provided, state) => ({
+ ...provided,
+ color: state.isFocused
+ ? COLORS["core-fleet-blue"]
+ : COLORS["core-fleet-black"],
+ fontSize: "14px",
+ lineHeight: "normal",
+ paddingLeft: 0,
+ marginTop: "1px",
+ }),
+ dropdownIndicator: (provided) => ({
+ ...provided,
+ display: "flex",
+ padding: "2px",
+ svg: {
+ transition: "transform 0.25s ease",
+ },
+ }),
+ menu: (provided) => ({
+ ...provided,
+ boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
+ borderRadius: "4px",
+ zIndex: 6,
+ overflow: "hidden",
+ border: 0,
+ marginTop: 0,
+ minWidth: "158px",
+ maxHeight: "220px",
+ position: "absolute",
+ left: getLeftMenuAlign(menuAlign),
+ right: getRightMenuAlign(menuAlign),
+ animation: "fade-in 150ms ease-out",
+ }),
+ menuList: (provided) => ({
+ ...provided,
+ padding: PADDING["pad-small"],
+ }),
+ valueContainer: (provided) => ({
+ ...provided,
+ padding: 0,
+ }),
+ option: (provided, state) => ({
+ ...provided,
+ padding: "10px 8px",
+ fontSize: "14px",
+ backgroundColor: getOptionBackgroundColor(state),
+ "&:hover": {
+ backgroundColor: state.isDisabled
+ ? "transparent"
+ : COLORS["ui-vibrant-blue-10"],
+ },
+ "&:active": {
+ backgroundColor: state.isDisabled
+ ? "transparent"
+ : COLORS["ui-vibrant-blue-10"],
+ },
+ ...(state.isDisabled && {
+ color: COLORS["ui-fleet-black-50"],
+ fontStyle: "italic",
+ // pointerEvents: "none", // Prevents any mouse interaction
+ }),
+ }),
+ };
+
+ return (
+
+
+ options={options}
+ placeholder={placeholder}
+ onChange={handleChange}
+ isDisabled={disabled}
+ isSearchable={isSearchable}
+ styles={customStyles}
+ components={{
+ DropdownIndicator: CustomDropdownIndicator,
+ IndicatorSeparator: () => null,
+ Option: CustomOption,
+ SingleValue: () => null, // Doesn't replace placeholder text with selected text
+ // Note: react-select doesn't support skipping disabled options when keyboarding through
+ }}
+ controlShouldRenderValue={false} // Doesn't change placeholder text to selected text
+ isOptionSelected={() => false} // Hides any styling on selected option
+ className={dropdownClassnames}
+ classNamePrefix={`${baseClass}-select`}
+ isOptionDisabled={(option) => !!option.disabled}
+ />
+
+ );
+};
+
+export default ActionsDropdown;
diff --git a/frontend/components/ActionsDropdown/_styles.scss b/frontend/components/ActionsDropdown/_styles.scss
new file mode 100644
index 000000000000..ba8549808dd8
--- /dev/null
+++ b/frontend/components/ActionsDropdown/_styles.scss
@@ -0,0 +1,6 @@
+// All styling in customStyles part of react-select-5
+.actions-dropdown-select__control {
+ &:focus-visible {
+ background-color: $core-fleet-blue;
+ }
+}
diff --git a/frontend/components/ActionsDropdown/index.ts b/frontend/components/ActionsDropdown/index.ts
new file mode 100644
index 000000000000..92d81527a2c7
--- /dev/null
+++ b/frontend/components/ActionsDropdown/index.ts
@@ -0,0 +1 @@
+export { default } from "./ActionsDropdown";
diff --git a/frontend/components/Editor/Editor.tsx b/frontend/components/Editor/Editor.tsx
index b724a296c39e..0ad54be1cf68 100644
--- a/frontend/components/Editor/Editor.tsx
+++ b/frontend/components/Editor/Editor.tsx
@@ -2,6 +2,7 @@ import classnames from "classnames";
import TooltipWrapper from "components/TooltipWrapper";
import React, { ReactNode } from "react";
import AceEditor from "react-ace";
+import { IAceEditor } from "react-ace/lib/types";
const baseClass = "editor";
@@ -66,6 +67,19 @@ const Editor = ({
[`${baseClass}__error`]: !!error,
});
+ const onLoadHandler = (editor: IAceEditor) => {
+ // Lose focus using the Escape key so you can Tab forward (or Shift+Tab backwards) through app
+ editor.commands.addCommand({
+ name: "escapeToBlur",
+ bindKey: { win: "Esc", mac: "Esc" },
+ exec: (aceEditor) => {
+ aceEditor.blur(); // Lose focus from the editor
+ return true;
+ },
+ readOnly: true,
+ });
+ };
+
const renderLabel = () => {
const labelText = error || label;
const labelClassName = classnames(`${baseClass}__label`, {
@@ -117,6 +131,7 @@ const Editor = ({
tabSize={2}
focus={focus}
onChange={onChange}
+ onLoad={onLoadHandler}
/>
{renderHelpText()}
diff --git a/frontend/components/FileUploader/FileUploader.tsx b/frontend/components/FileUploader/FileUploader.tsx
index 6ba3bfa42f02..9951ae3509a0 100644
--- a/frontend/components/FileUploader/FileUploader.tsx
+++ b/frontend/components/FileUploader/FileUploader.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useRef } from "react";
import classnames from "classnames";
import Button from "components/buttons/Button";
@@ -74,18 +74,32 @@ export const FileUploader = ({
fileDetails,
}: IFileUploaderProps) => {
const [isFileSelected, setIsFileSelected] = useState(!!fileDetails);
+ const fileInputRef = useRef(null);
const classes = classnames(baseClass, className, {
[`${baseClass}__file-preview`]: isFileSelected,
});
const buttonVariant = buttonType === "button" ? "brand" : "text-icon";
+ const triggerFileInput = () => {
+ fileInputRef.current?.click();
+ };
+
const onFileSelect = (e: React.ChangeEvent) => {
const files = e.target.files;
onFileUpload(files);
setIsFileSelected(true);
- e.target.value = "";
+ if (fileInputRef.current) {
+ fileInputRef.current.value = "";
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ triggerFileInput();
+ }
};
const renderGraphics = () => {
@@ -113,6 +127,9 @@ export const FileUploader = ({
variant={buttonVariant}
isLoading={isLoading}
disabled={disabled}
+ customOnKeyDown={handleKeyDown}
+ tabIndex={0}
+ onClick={triggerFileInput}
>
{buttonType === "link" && }
@@ -120,6 +137,7 @@ export const FileUploader = ({
{
fixHotkeys(editor);
+
+ // Lose focus using the Escape key so you can Tab forward (or Shift+Tab backwards) through app
+ editor.commands.addCommand({
+ name: "escapeToBlur",
+ bindKey: { win: "Esc", mac: "Esc" },
+ exec: (aceEditor) => {
+ aceEditor.blur(); // Lose focus from the editor
+ return true;
+ },
+ readOnly: true,
+ });
+
onLoad && onLoad(editor);
};
diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx
index deb2d10c724b..8342002ac323 100644
--- a/frontend/components/Modal/Modal.tsx
+++ b/frontend/components/Modal/Modal.tsx
@@ -113,7 +113,7 @@ const Modal = ({
{title}
{!disableClosingModal && (
-
+
diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx b/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx
deleted file mode 100644
index 8c0c98852516..000000000000
--- a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from "react";
-import { screen } from "@testing-library/react";
-import { renderWithSetup } from "test/test-utils";
-
-import DropdownCell from "./DropdownCell";
-
-const DROPDOWN_OPTIONS = [
- { disabled: false, label: "Edit", value: "edit-query" },
- { disabled: false, label: "Show query", value: "show-query" },
- { disabled: true, label: "Delete", value: "delete-query" },
-];
-const PLACEHOLDER = "Actions";
-const ON_CHANGE = (value: string) => {
- console.log(value);
-};
-
-describe("Dropdown cell", () => {
- it("renders dropdown placeholder and options", async () => {
- const { user } = renderWithSetup(
-
- );
-
- await user.click(screen.getByText("Actions"));
-
- expect(screen.getByText(/edit/i)).toBeInTheDocument();
- expect(screen.getByText(/show query/i)).toBeInTheDocument();
- expect(screen.getByText(/delete/i)).toBeInTheDocument();
- });
-});
diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tsx b/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tsx
deleted file mode 100644
index 6eb4fad3eab7..000000000000
--- a/frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from "react";
-
-// ignore TS error for now until these are rewritten in ts.
-// @ts-ignore
-import Dropdown from "components/forms/fields/Dropdown";
-
-import { IDropdownOption } from "interfaces/dropdownOption";
-
-const baseClass = "dropdown-cell";
-
-interface IDropdownCellProps {
- options: IDropdownOption[];
- placeholder: string;
- onChange: (value: string) => void;
- disabled?: boolean;
-}
-
-const DropdownCell = ({
- options,
- placeholder,
- onChange,
- disabled,
-}: IDropdownCellProps): JSX.Element => {
- return (
-
-
-
- );
-};
-
-export default DropdownCell;
diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/_styles.scss b/frontend/components/TableContainer/DataTable/DropdownCell/_styles.scss
deleted file mode 100644
index eaf1d1c3c254..000000000000
--- a/frontend/components/TableContainer/DataTable/DropdownCell/_styles.scss
+++ /dev/null
@@ -1,87 +0,0 @@
-.dropdown-cell {
- width: 80px;
-
- .form-field {
- margin: 0;
- }
-
- .Select {
- position: relative;
- border: 0;
- height: auto;
-
- &.is-focused,
- &:hover {
- border: 0;
- }
-
- &.is-focused:not(.is-open) {
- .Select-control {
- background-color: initial;
- }
- }
-
- &.is-disabled {
- .Select-control {
- .Select-placeholder {
- @include disabled;
- }
- }
- }
-
- .Select-control {
- display: flex;
- background-color: initial;
- height: auto;
- justify-content: space-between;
- border: 0;
- cursor: pointer;
-
- &:hover {
- box-shadow: none;
- }
-
- &:hover .Select-placeholder {
- color: $core-vibrant-blue;
- }
-
- .Select-placeholder {
- color: $core-fleet-black;
- font-size: 14px;
- line-height: normal;
- padding-left: 0;
- margin-top: 1px;
- }
-
- .Select-input {
- height: auto;
- }
-
- .Select-arrow-zone {
- display: flex;
- }
- }
-
- .Select-menu-outer {
- margin-top: $pad-xsmall;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
- border-radius: $border-radius;
- z-index: 6;
- overflow: hidden;
- border: 0;
- width: 188px;
- left: unset;
- top: unset;
- max-height: 220px;
- padding: $pad-small;
- position: absolute;
- left: -12px;
- }
-
- &.is-open {
- .Select-control .Select-placeholder {
- color: $core-vibrant-blue;
- }
- }
- }
-}
diff --git a/frontend/components/TableContainer/DataTable/DropdownCell/index.ts b/frontend/components/TableContainer/DataTable/DropdownCell/index.ts
deleted file mode 100644
index d2a9324aa2cf..000000000000
--- a/frontend/components/TableContainer/DataTable/DropdownCell/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./DropdownCell";
diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx
index 31f2400d3dac..f96a0a77d29f 100644
--- a/frontend/components/TeamsDropdown/TeamsDropdown.tsx
+++ b/frontend/components/TeamsDropdown/TeamsDropdown.tsx
@@ -85,6 +85,7 @@ const TeamsDropdown = ({
onChange={onChange}
onOpen={onOpen}
onClose={onClose}
+ tabIndex={0}
/>
);
}
diff --git a/frontend/components/ViewAllHostsLink/_styles.scss b/frontend/components/ViewAllHostsLink/_styles.scss
index 486834a65cec..1afb8b8f2282 100644
--- a/frontend/components/ViewAllHostsLink/_styles.scss
+++ b/frontend/components/ViewAllHostsLink/_styles.scss
@@ -7,4 +7,9 @@
}
}
}
+
+ // For tabbing through the app
+ &:focus-visible.row-hover-link {
+ opacity: 1;
+ }
}
diff --git a/frontend/components/YamlAce/YamlAce.jsx b/frontend/components/YamlAce/YamlAce.jsx
index 9632e68c2b75..b8979f99e8fa 100644
--- a/frontend/components/YamlAce/YamlAce.jsx
+++ b/frontend/components/YamlAce/YamlAce.jsx
@@ -17,6 +17,19 @@ class YamlAce extends Component {
wrapperClassName: PropTypes.string,
};
+ onLoadHandler = (editor) => {
+ // Lose focus using the Escape key so you can Tab forward (or Shift+Tab backwards) through app
+ editor.commands.addCommand({
+ name: "escapeToBlur",
+ bindKey: { win: "Esc", mac: "Esc" },
+ exec: (aceEditor) => {
+ aceEditor.blur(); // Lose focus from the editor
+ return true;
+ },
+ readOnly: true,
+ });
+ };
+
renderLabel = () => {
const { name, error, label } = this.props;
@@ -45,7 +58,7 @@ class YamlAce extends Component {
wrapperClassName,
} = this.props;
- const { renderLabel } = this;
+ const { renderLabel, onLoadHandler } = this;
const wrapperClass = classnames(wrapperClassName, "form-field", {
[`${baseClass}__wrapper--error`]: error,
@@ -67,6 +80,7 @@ class YamlAce extends Component {
onChange={onChange}
name={name}
label={label}
+ onLoad={onLoadHandler}
/>
);
diff --git a/frontend/components/buttons/Button/Button.tsx b/frontend/components/buttons/Button/Button.tsx
index 44b28fc4dbf5..48ee81ad00c9 100644
--- a/frontend/components/buttons/Button/Button.tsx
+++ b/frontend/components/buttons/Button/Button.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { Children } from "react";
import classnames from "classnames";
import Spinner from "components/Spinner";
@@ -35,6 +35,7 @@ export interface IButtonProps {
tabIndex?: number;
type?: "button" | "submit" | "reset";
title?: string;
+ /** Default: "brand" */
variant?: ButtonVariant;
onClick?:
| ((value?: any) => void)
@@ -44,6 +45,7 @@ export interface IButtonProps {
| React.KeyboardEvent
) => void);
isLoading?: boolean;
+ customOnKeyDown?: (e: React.KeyboardEvent) => void;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -57,7 +59,7 @@ class Button extends React.Component {
static defaultProps = {
size: "",
type: "button",
- variant: "default",
+ variant: "brand",
};
componentDidMount(): void {
@@ -115,6 +117,7 @@ class Button extends React.Component {
title,
variant,
isLoading,
+ customOnKeyDown,
} = this.props;
const fullClassName = classnames(
baseClass,
@@ -136,7 +139,7 @@ class Button extends React.Component {
className={fullClassName}
disabled={disabled}
onClick={handleClick}
- onKeyDown={handleKeyDown}
+ onKeyDown={customOnKeyDown || handleKeyDown}
tabIndex={tabIndex}
type={type}
title={title}
diff --git a/frontend/components/buttons/Button/_styles.scss b/frontend/components/buttons/Button/_styles.scss
index f38badbccec3..ca6fc095987e 100644
--- a/frontend/components/buttons/Button/_styles.scss
+++ b/frontend/components/buttons/Button/_styles.scss
@@ -301,8 +301,13 @@ $base-class: "button";
color: $core-vibrant-blue-down;
}
- &:focus {
+ &:focus-visible {
+ color: $core-vibrant-blue-over;
+ border: 1px;
+ border-radius: 2px; // Visble when tabbing
+ background: var(--Core-White, #fff);
outline: none;
+ box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #d9d9fe;
}
&:hover,
diff --git a/frontend/components/buttons/DropdownButton/_styles.scss b/frontend/components/buttons/DropdownButton/_styles.scss
index 18f6d59871af..d22bb4e2089d 100644
--- a/frontend/components/buttons/DropdownButton/_styles.scss
+++ b/frontend/components/buttons/DropdownButton/_styles.scss
@@ -1,5 +1,5 @@
.dropdown-button {
- padding: 8px 24px 8px 0;
+ padding: 8px 0;
&__wrapper {
display: flex;
position: relative;
diff --git a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss
index c61763161579..7fcd24ad33ed 100644
--- a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss
+++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss
@@ -1,4 +1,7 @@
-.Select > .Select-menu-outer {
+// Used with old react-select dropdown and
+// New react-select-5 ActionsDropdown.tsx
+.Select > .Select-menu-outer,
+.actions-dropdown {
.is-disabled * {
color: $ui-fleet-black-50;
}
diff --git a/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx b/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx
index b93aea08dfda..089302eedc0f 100644
--- a/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx
+++ b/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx
@@ -12,7 +12,7 @@ import { IScheduledQuery } from "interfaces/scheduled_query";
import { IDropdownOption } from "interfaces/dropdownOption";
import Checkbox from "components/forms/fields/Checkbox";
-import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
+import ActionsDropdown from "components/ActionsDropdown";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
@@ -54,7 +54,7 @@ interface IPerformanceImpactCellProps extends IRowProps {
};
}
-interface IDropdownCellProps extends IRowProps {
+interface IActionsDropdownProps extends IRowProps {
cell: {
value: IDropdownOption[];
};
@@ -68,7 +68,7 @@ interface IDataColumn {
Cell:
| ((props: ICellProps) => JSX.Element)
| ((props: IPerformanceImpactCellProps) => JSX.Element)
- | ((props: IDropdownCellProps) => JSX.Element);
+ | ((props: IActionsDropdownProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
}
@@ -182,8 +182,8 @@ const generateTableHeaders = (
Header: "",
disableSortBy: true,
accessor: "actions",
- Cell: (cellProps: IDropdownCellProps) => (
- (
+
actionSelectHandler(value, cellProps.row.original)
diff --git a/frontend/components/top_nav/UserMenu/UserMenu.tsx b/frontend/components/top_nav/UserMenu/UserMenu.tsx
index 0cae736a3246..b1228abc25ce 100644
--- a/frontend/components/top_nav/UserMenu/UserMenu.tsx
+++ b/frontend/components/top_nav/UserMenu/UserMenu.tsx
@@ -71,7 +71,7 @@ const UserMenu = ({
return (
-
+
void;
+ router: InjectedRouter;
}
const DiskEncryption = ({
currentTeamId,
onMutation,
+ router,
}: IDiskEncryptionProps) => {
const { isPremiumTier, config, setConfig } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
@@ -123,7 +126,10 @@ const DiskEncryption = ({
) : (
{showAggregate && (
-
+
)}
{
+const DiskEncryptionTable = ({
+ currentTeamId,
+ router,
+}: IDiskEncryptionTableProps) => {
const {
data: diskEncryptionStatusData,
error: diskEncryptionStatusError,
@@ -31,6 +50,21 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
}
);
+ const onSelectSingleRow = useCallback(
+ (row: IDiskEncryptionRowProps) => {
+ const { status, teamId } = row.original;
+
+ const queryParams = {
+ [HOSTS_QUERY_PARAMS.DISK_ENCRYPTION]: status?.value,
+ team_id: teamId,
+ };
+ const endpoint = PATHS.MANAGE_HOSTS;
+ const path = `${endpoint}?${buildQueryStringFromParams(queryParams)}`;
+ router.push(path);
+ },
+ [router]
+ );
+
const tableHeaders = generateTableHeaders();
const tableData = generateTableData(diskEncryptionStatusData, currentTeamId);
@@ -60,6 +94,9 @@ const DiskEncryptionTable = ({ currentTeamId }: IDiskEncryptionTableProps) => {
catches up."
/>
)}
+ // these 2 properties allow linking on click anywhere in the row
+ disableMultiRowSelect
+ onSelectSingleRow={onSelectSingleRow}
/>
);
diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx
index a5522d011cc7..1eddba30ad07 100644
--- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx
+++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTableConfig.tsx
@@ -5,7 +5,6 @@ import {
IDiskEncryptionStatusAggregate,
IDiskEncryptionSummaryResponse,
} from "services/entities/mdm";
-import { HOSTS_QUERY_PARAMS } from "services/entities/hosts";
import TextCell from "components/TableContainer/DataTable/TextCell";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
@@ -13,7 +12,7 @@ import StatusIndicatorWithIcon from "components/StatusIndicatorWithIcon";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import { IndicatorStatus } from "components/StatusIndicatorWithIcon/StatusIndicatorWithIcon";
-interface IStatusCellValue {
+export interface IStatusCellValue {
displayName: string;
statusName: IndicatorStatus;
value: DiskEncryptionStatus;
@@ -118,15 +117,7 @@ const defaultTableHeaders: IDataColumn[] = [
return (
<>
{cellProps.row.original && (
-
+
)}
>
);
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx
index 0a8863abe646..152522c55961 100644
--- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/SoftwarePackageCard.tsx
@@ -16,10 +16,9 @@ import { buildQueryStringFromParams } from "utilities/url";
import { internationalTimeFormat } from "utilities/helpers";
import { uploadedFromNow } from "utilities/date_format";
-// @ts-ignore
-import Dropdown from "components/forms/fields/Dropdown";
import Card from "components/Card";
import Graphic from "components/Graphic";
+import ActionsDropdown from "components/ActionsDropdown";
import TooltipWrapper from "components/TooltipWrapper";
import DataSet from "components/DataSet";
import Icon from "components/Icon";
@@ -183,7 +182,7 @@ interface IActionsDropdownProps {
onEditSoftwareClick: () => void;
}
-const ActionsDropdown = ({
+const SoftwareActionsDropdown = ({
isSoftwarePackage,
onDownloadClick,
onDeleteClick,
@@ -207,16 +206,17 @@ const ActionsDropdown = ({
return (
-
);
@@ -353,7 +353,7 @@ const SoftwarePackageCard = ({
)}
{showActions && (
- JSX.Element)
- | ((props: IDropdownCellProps) => JSX.Element);
+ | ((props: IActionsDropdownProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
sortType?: string;
@@ -98,8 +98,8 @@ const generateTableHeaders = (
Header: "",
disableSortBy: true,
accessor: "actions",
- Cell: (cellProps: IDropdownCellProps) => (
- (
+
actionSelectHandler(value, cellProps.row.original)
diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx
index d79e1d3eb16c..03a633b599bb 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/AppleBusinessManagerPage/components/AppleBusinessManagerTable/AppleBusinessManagerTableConfig.tsx
@@ -6,7 +6,7 @@ import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config";
import { IDropdownOption } from "interfaces/dropdownOption";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
-import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
+import ActionsDropdown from "components/ActionsDropdown";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
@@ -163,7 +163,7 @@ export const generateTableConfig = (
// but we don't use it.
accessor: "id",
Cell: (cellProps) => (
-
actionSelectHandler(value, cellProps.row.original)
diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx
index 28e1e32cae02..2a10c65bf77e 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/VppTableConfig.tsx
@@ -6,7 +6,7 @@ import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config";
import { IDropdownOption } from "interfaces/dropdownOption";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
-import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
+import ActionsDropdown from "components/ActionsDropdown";
import TextCell from "components/TableContainer/DataTable/TextCell";
import RenewDateCell from "../../../components/RenewDateCell";
@@ -104,7 +104,7 @@ export const generateTableConfig = (
// but we don't use it.
accessor: "id",
Cell: (cellProps) => (
-
actionSelectHandler(value, cellProps.row.original)
diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/IdpSection/IdpSection.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/IdpSection/IdpSection.tsx
index a3f50e6e2f79..6bc821b46846 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/IdpSection/IdpSection.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/components/IdpSection/IdpSection.tsx
@@ -114,6 +114,7 @@ const IdpSection = () => {
disabled={!completedForm}
onClick={onSubmit}
className="button-wrap"
+ variant="brand"
>
Save
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx
index 49c66aaab7b3..6a564141de15 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx
@@ -295,12 +295,11 @@ const TeamDetailsWrapper = ({
try {
await teamsAPI.destroy(teamIdForApi);
- return router.push(PATHS.ADMIN_TEAMS);
+ router.push(PATHS.ADMIN_TEAMS);
renderFlash("success", "Team removed");
} catch (response) {
renderFlash("error", "Something went wrong removing the team");
console.error(response);
- return false;
} finally {
toggleDeleteTeamModal();
setIsUpdatingTeams(false);
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig.tsx
index 20d166e251ee..6cb769b90df6 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig.tsx
@@ -1,7 +1,7 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import TextCell from "components/TableContainer/DataTable/TextCell/TextCell";
-import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
+import ActionsDropdown from "components/ActionsDropdown";
import CustomLink from "components/CustomLink";
import { IUser, UserRole } from "interfaces/user";
import { ITeam } from "interfaces/team";
@@ -29,7 +29,7 @@ interface ICellProps extends IRowProps {
};
}
-interface IDropdownCellProps extends IRowProps {
+interface IActionsDropdownProps extends IRowProps {
cell: {
value: IDropdownOption[];
};
@@ -41,7 +41,7 @@ interface IDataColumn {
accessor: string;
Cell:
| ((props: ICellProps) => JSX.Element)
- | ((props: IDropdownCellProps) => JSX.Element);
+ | ((props: IActionsDropdownProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
sortType?: string;
@@ -174,8 +174,8 @@ const generateColumnConfigs = (
Header: "",
disableSortBy: true,
accessor: "actions",
- Cell: (cellProps: IDropdownCellProps) => (
- (
+
actionSelectHandler(value, cellProps.row.original)
diff --git a/frontend/pages/admin/TeamManagementPage/TeamTableConfig.tsx b/frontend/pages/admin/TeamManagementPage/TeamTableConfig.tsx
index 55edae36d2cc..31a7de9ebee2 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamTableConfig.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamTableConfig.tsx
@@ -2,7 +2,7 @@ import React from "react";
import LinkCell from "components/TableContainer/DataTable/LinkCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
-import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
+import DropdownCell from "components/ActionsDropdown";
import { ITeam } from "interfaces/team";
import { IDropdownOption } from "interfaces/dropdownOption";
import PATHS from "router/paths";
diff --git a/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx b/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx
index 825eda6aea88..bd013cfba084 100644
--- a/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx
+++ b/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx
@@ -20,7 +20,7 @@ const DeleteTeamModal = ({
}: IDeleteTeamModalProps): JSX.Element => {
return (
JSX.Element)
- | ((props: IDropdownCellProps) => JSX.Element);
+ | ((props: IActionsDropdownProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
}
@@ -200,8 +200,8 @@ const generateTableHeaders = (
Header: "",
disableSortBy: true,
accessor: "actions",
- Cell: (cellProps: IDropdownCellProps) => (
- (
+
actionSelectHandler(value, cellProps.row.original)
diff --git a/frontend/pages/admin/components/HostStatusWebhookPreviewModal/HostStatusWebhookPreviewModal.tsx b/frontend/pages/admin/components/HostStatusWebhookPreviewModal/HostStatusWebhookPreviewModal.tsx
index f46dfaf4ae1f..2a4885499c09 100644
--- a/frontend/pages/admin/components/HostStatusWebhookPreviewModal/HostStatusWebhookPreviewModal.tsx
+++ b/frontend/pages/admin/components/HostStatusWebhookPreviewModal/HostStatusWebhookPreviewModal.tsx
@@ -54,7 +54,7 @@ const HostStatusWebhookPreviewModal = ({
/>
-
+
Done
diff --git a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx
index f6e564b21688..729bd31bc61e 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/CustomLabelGroupHeading.tsx
@@ -30,7 +30,7 @@ const CustomLabelGroupHeading = (
const handleInputClick = (
event: React.MouseEvent
) => {
- onClickLabelSearchInput(event);
+ onClickLabelSearchInput && onClickLabelSearchInput(event);
inputRef.current?.focus();
event.stopPropagation();
};
diff --git a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss
index 8f4a9c76a377..a71973b8636a 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss
+++ b/frontend/pages/hosts/ManageHostsPage/components/FilterPill/_styles.scss
@@ -17,11 +17,6 @@
padding: 0px;
border: none;
margin-left: 0;
-
- img {
- padding: 0px;
- margin: 0px;
- }
}
.premium-icon-tip {
diff --git a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx
index 355e87d66550..40c0dcc9ede1 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/LabelFilterSelect.tsx
@@ -24,12 +24,12 @@ declare module "react-select-5/dist/declarations/src/Select" {
IsMulti extends boolean,
Group extends GroupBase
> {
- labelQuery: string;
- canAddNewLabels: boolean;
- onAddLabel: () => void;
- onChangeLabelQuery: (event: React.ChangeEvent) => void;
- onClickLabelSearchInput: React.MouseEventHandler;
- onBlurLabelSearchInput: React.FocusEventHandler;
+ labelQuery?: string;
+ canAddNewLabels?: boolean;
+ onAddLabel?: () => void;
+ onChangeLabelQuery?: (event: React.ChangeEvent) => void;
+ onClickLabelSearchInput?: React.MouseEventHandler;
+ onBlurLabelSearchInput?: React.FocusEventHandler;
}
}
@@ -131,6 +131,10 @@ const LabelFilterSelect = ({
if (e.key === "Escape") {
setMenuIsOpen(false);
selectRef.current?.blur();
+ } else if (e.key === "Tab" && !e.shiftKey) {
+ // Allow tabbing out of the component
+ setMenuIsOpen(false);
+ selectRef.current?.blur();
} else {
setMenuIsOpen(true);
}
diff --git a/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tsx b/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tsx
index b30c79a4da23..003b167c745b 100644
--- a/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tsx
+++ b/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tsx
@@ -4,7 +4,6 @@ import strUtils from "utilities/strings";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
-import CustomLink from "components/CustomLink";
const baseClass = "delete-host-modal";
@@ -59,12 +58,7 @@ const DeleteHostModal = ({
};
return (
-
+
<>
This will remove the record of {hostText()} .{largeVolumeText()}
diff --git a/frontend/pages/hosts/details/DeviceUserPage/helpers.ts b/frontend/pages/hosts/details/DeviceUserPage/helpers.ts
index a5a362e026ef..80c5037131ae 100644
--- a/frontend/pages/hosts/details/DeviceUserPage/helpers.ts
+++ b/frontend/pages/hosts/details/DeviceUserPage/helpers.ts
@@ -1,16 +1,6 @@
-import { getErrorReason } from "interfaces/errors";
-
const DEFAULT_ERROR_MESSAGE = "refetch error.";
// eslint-disable-next-line import/prefer-default-export
export const getErrorMessage = (e: unknown, hostName: string) => {
- let errorMessage = getErrorReason(e, {
- reasonIncludes: "Host does not have MDM turned on",
- });
-
- if (!errorMessage) {
- errorMessage = DEFAULT_ERROR_MESSAGE;
- }
-
- return `Host "${hostName}" ${errorMessage}`;
+ return `Host "${hostName}" ${DEFAULT_ERROR_MESSAGE}`;
};
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx
index ecc33559d4de..ba82675e8a76 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx
@@ -116,7 +116,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Query").parentElement?.parentElement?.parentElement
- ).toHaveClass("is-disabled");
+ ).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -153,7 +153,7 @@ describe("Host Actions Dropdown", () => {
await user.click(screen.getByText("Actions"));
expect(
screen.getByText("Query").parentElement?.parentElement?.parentElement
- ).toHaveClass("is-disabled");
+ ).toHaveClass("actions-dropdown-select__option--is-disabled");
});
it("renders the Query action as disabled when a host is updating", async () => {
@@ -180,7 +180,7 @@ describe("Host Actions Dropdown", () => {
await user.click(screen.getByText("Actions"));
expect(screen.getByText("Query").parentElement).toHaveClass(
- "is-disabled"
+ "actions-dropdown-select__option--is-disabled"
);
});
});
@@ -388,7 +388,7 @@ describe("Host Actions Dropdown", () => {
debug();
expect(screen.getByText("Turn off MDM").parentElement).toHaveClass(
- "is-disabled"
+ "actions-dropdown-select__option--is-disabled"
);
});
@@ -590,7 +590,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Lock").parentElement?.parentElement?.parentElement
- ).toHaveClass("is-disabled");
+ ).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -845,7 +845,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Unlock").parentElement?.parentElement?.parentElement
- ).toHaveClass("is-disabled");
+ ).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -981,7 +981,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Wipe").parentElement?.parentElement?.parentElement
- ).toHaveClass("is-disabled");
+ ).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -1055,7 +1055,7 @@ describe("Host Actions Dropdown", () => {
screen
.getByText("Run script")
.parentElement?.parentElement?.parentElement?.classList.contains(
- "is-disabled"
+ "actions-dropdown-select__option--is-disabled"
)
).toBeFalsy();
@@ -1098,7 +1098,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Run script").parentElement?.parentElement
?.parentElement
- ).toHaveClass("is-disabled");
+ ).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
index c28352741043..c8a62d193ff8 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx
@@ -4,8 +4,7 @@ import { MdmEnrollmentStatus } from "interfaces/mdm";
import permissions from "utilities/permissions";
import { AppContext } from "context/app";
-// @ts-ignore
-import Dropdown from "components/forms/fields/Dropdown";
+import ActionsDropdown from "components/ActionsDropdown";
import { generateHostActionOptions } from "./helpers";
import { HostMdmDeviceStatusUIState } from "../../helpers";
@@ -81,12 +80,12 @@ const HostActionsDropdown = ({
return (
-
);
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index 0c56d7c6ca4d..aa553f116ecd 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -104,6 +104,7 @@ import {
import WipeModal from "./modals/WipeModal";
import SoftwareDetailsModal from "../cards/Software/SoftwareDetailsModal";
import { parseHostSoftwareQueryParams } from "../cards/Software/HostSoftware";
+import { getErrorMessage } from "./helpers";
const baseClass = "host-details";
@@ -546,11 +547,11 @@ const HostDetailsPage = ({
setIsUpdatingHost(true);
try {
await hostAPI.destroy(host);
+ router.push(PATHS.MANAGE_HOSTS);
renderFlash(
"success",
`Host "${host.display_name}" was successfully deleted.`
);
- router.push(PATHS.MANAGE_HOSTS);
} catch (error) {
console.log(error);
renderFlash(
@@ -579,8 +580,7 @@ const HostDetailsPage = ({
}, 1000);
});
} catch (error) {
- console.log(error);
- renderFlash("error", `Host "${host.display_name}" refetch error`);
+ renderFlash("error", getErrorMessage(error, host.display_name));
setShowRefetchSpinner(false);
}
}
diff --git a/frontend/pages/hosts/details/HostDetailsPage/helpers.ts b/frontend/pages/hosts/details/HostDetailsPage/helpers.ts
new file mode 100644
index 000000000000..a5a362e026ef
--- /dev/null
+++ b/frontend/pages/hosts/details/HostDetailsPage/helpers.ts
@@ -0,0 +1,16 @@
+import { getErrorReason } from "interfaces/errors";
+
+const DEFAULT_ERROR_MESSAGE = "refetch error.";
+
+// eslint-disable-next-line import/prefer-default-export
+export const getErrorMessage = (e: unknown, hostName: string) => {
+ let errorMessage = getErrorReason(e, {
+ reasonIncludes: "Host does not have MDM turned on",
+ });
+
+ if (!errorMessage) {
+ errorMessage = DEFAULT_ERROR_MESSAGE;
+ }
+
+ return `Host "${hostName}" ${errorMessage}`;
+};
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig.tsx
index d115185f1a30..a8ecb5e3689c 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig.tsx
@@ -5,7 +5,7 @@ import { IHostScript, ILastExecution } from "interfaces/script";
import { IUser } from "interfaces/user";
import Icon from "components/Icon";
-import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
+import ActionsDropdown from "components/ActionsDropdown";
import {
isGlobalAdmin,
isTeamMaintainer,
@@ -24,7 +24,7 @@ interface IStatusCellProps {
};
}
-interface IDropdownCellProps {
+interface IActionsDropdownProps {
cell: {
value: IDropdownOption[];
};
@@ -94,7 +94,7 @@ export const generateTableColumnConfigs = (
Header: "",
disableSortBy: true,
accessor: "actions",
- Cell: (cellProps: IDropdownCellProps) => {
+ Cell: (cellProps: IActionsDropdownProps) => {
if (scriptsDisabled) {
// create a basic span that doesn't use the dropdown component (which relies on react-select
// and makes it difficult for us to style the disabled tooltip underline on the placeholder text.
@@ -120,7 +120,7 @@ export const generateTableColumnConfigs = (
cellProps.row.original
);
return (
-
onSelectAction(value, cellProps.row.original)
diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss
index 585b916ef80f..f2736bf14b7d 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss
+++ b/frontend/pages/hosts/details/HostDetailsPage/modals/SelectQueryModal/_styles.scss
@@ -1,14 +1,25 @@
+// Similar to AddPolicyModal
.select-query-modal {
min-height: 400px;
- height: 80%;
+ height: 90%;
overflow: hidden;
+ .modal__content-wrapper {
+ height: 95%;
+ }
+
.modal__content {
display: flex;
flex-direction: column;
gap: $pad-large;
- height: 80%;
- overflow: auto;
+ height: 100%;
+ }
+
+ &__query-selection {
+ overflow-y: auto;
+ .children-wrapper {
+ width: 680px;
+ }
}
&__no-queries {
@@ -31,10 +42,4 @@
font-weight: $bold;
}
}
-
- &__query-selection {
- .children-wrapper {
- width: 680px;
- }
- }
}
diff --git a/frontend/pages/hosts/details/_styles.scss b/frontend/pages/hosts/details/_styles.scss
index f2ef26fe9d9a..cd225a757717 100644
--- a/frontend/pages/hosts/details/_styles.scss
+++ b/frontend/pages/hosts/details/_styles.scss
@@ -84,11 +84,12 @@
}
.react-tabs__tab--selected {
background-color: $ui-off-white;
- }
- }
- .focus-visible {
- background-color: $ui-vibrant-blue-10;
+ // When tabbing through the app
+ &:focus-visible {
+ background-color: $ui-vibrant-blue-10;
+ }
+ }
}
}
diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx
index e52b93ea1236..8e0bf618debd 100644
--- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx
+++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx
@@ -23,7 +23,7 @@ import PATHS from "router/paths";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import SoftwareNameCell from "components/TableContainer/DataTable/SoftwareNameCell";
-import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
+import ActionsDropdown from "components/ActionsDropdown";
import VulnerabilitiesCell from "pages/SoftwarePage/components/VulnerabilitiesCell";
import VersionCell from "pages/SoftwarePage/components/VersionCell";
@@ -237,7 +237,7 @@ export const generateSoftwareTableHeaders = ({
} = original;
return (
- {
const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints;
- return sendRequest("GET", DEVICE_USER_MDM_ENROLLMENT_PROFILE(token));
+ return sendRequest(
+ "GET",
+ DEVICE_USER_MDM_ENROLLMENT_PROFILE(token),
+ undefined,
+ "blob"
+ );
},
getSetupExperienceSoftware: (
diff --git a/frontend/styles/var/colors.ts b/frontend/styles/var/colors.ts
index 4298ecca74df..9b3928c4174d 100644
--- a/frontend/styles/var/colors.ts
+++ b/frontend/styles/var/colors.ts
@@ -26,4 +26,9 @@ export const COLORS = {
"status-success": "#3DB67B",
"status-warning": "#F8CD6B",
"status-error": "#ED6E85",
+
+ "core-vibrant-blue-over": "#5d5ae7",
+ "core-vibrant-blue-down": "#4b4ab4",
+ "ui-vibrant-blue-25": "#d9d9fe",
+ "ui-vibrant-blue-10": "#f1f0ff",
};
diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss
index 565320664025..d55db4c93327 100644
--- a/frontend/styles/var/mixins.scss
+++ b/frontend/styles/var/mixins.scss
@@ -294,6 +294,13 @@ $max-width: 2560px;
.Select-input {
height: auto;
+
+ // When tabbing
+ &:focus-visible {
+ outline: 2px solid $ui-vibrant-blue-25;
+ outline-offset: 1px;
+ border-radius: 4px;
+ }
}
.Select-arrow-zone {
diff --git a/frontend/styles/var/padding.ts b/frontend/styles/var/padding.ts
new file mode 100644
index 000000000000..540a2fe6a8ce
--- /dev/null
+++ b/frontend/styles/var/padding.ts
@@ -0,0 +1,19 @@
+const pxToRem = (px: number): string => {
+ const baseSize = 16; // Assuming the base font size is 16px
+ return `${px / baseSize}rem`;
+};
+
+export const PADDING = {
+ "pad-auto": "auto",
+ "pad-xxsmall": pxToRem(2),
+ "pad-xsmall": pxToRem(4),
+ "pad-small": pxToRem(8),
+ "pad-icon": pxToRem(14),
+ "pad-medium": pxToRem(16),
+ "pad-large": pxToRem(24),
+ "pad-xlarge": pxToRem(32),
+ "pad-xxlarge": pxToRem(40),
+ "pad-xxxlarge": pxToRem(80),
+};
+
+export type Padding = keyof typeof PADDING;
diff --git a/go.mod b/go.mod
index 03c5ae100b6a..0e0302c11eb1 100644
--- a/go.mod
+++ b/go.mod
@@ -6,12 +6,13 @@ require (
cloud.google.com/go/pubsub v1.37.0
fyne.io/systray v1.10.1-0.20240111184411-11c585fff98d
github.com/AbGuthrie/goquery/v2 v2.0.1
+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/Masterminds/semver v1.5.0
github.com/RobotsAndPencils/buford v0.14.0
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea
- github.com/XSAM/otelsql v0.10.0
+ github.com/XSAM/otelsql v0.35.0
github.com/andygrunwald/go-jira v1.16.0
github.com/antchfx/xmlquery v1.3.14
github.com/apex/log v1.9.0
@@ -21,6 +22,7 @@ require (
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/boltdb/bolt v1.3.1
github.com/briandowns/spinner v1.23.1
+ github.com/cavaliergopher/rpm v1.2.0
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v4 v4.3.0
github.com/clbanning/mxj v1.8.4
@@ -53,7 +55,7 @@ require (
github.com/google/uuid v1.6.0
github.com/goreleaser/goreleaser v1.1.0
github.com/goreleaser/nfpm/v2 v2.10.0
- github.com/gorilla/mux v1.8.0
+ github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/gosuri/uilive v0.0.4
github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda
@@ -114,23 +116,23 @@ require (
go.elastic.co/apm/v2 v2.4.3
go.etcd.io/bbolt v1.3.9
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
- go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0
- go.opentelemetry.io/otel v1.28.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0
- go.opentelemetry.io/otel/sdk v1.28.0
- golang.org/x/crypto v0.24.0
+ go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.56.0
+ go.opentelemetry.io/otel v1.31.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0
+ go.opentelemetry.io/otel/sdk v1.31.0
+ golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/image v0.18.0
golang.org/x/mod v0.17.0
- golang.org/x/net v0.26.0
- golang.org/x/oauth2 v0.20.0
- golang.org/x/sync v0.7.0
- golang.org/x/sys v0.21.0
- golang.org/x/text v0.16.0
+ golang.org/x/net v0.30.0
+ golang.org/x/oauth2 v0.22.0
+ golang.org/x/sync v0.8.0
+ golang.org/x/sys v0.26.0
+ golang.org/x/text v0.19.0
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
google.golang.org/api v0.178.0
- google.golang.org/grpc v1.64.1
+ google.golang.org/grpc v1.67.1
gopkg.in/guregu/null.v3 v3.5.0
gopkg.in/ini.v1 v1.67.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
@@ -143,7 +145,7 @@ require (
cloud.google.com/go v0.112.2 // indirect
cloud.google.com/go/auth v0.3.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
- cloud.google.com/go/compute/metadata v0.3.0 // indirect
+ cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/iam v1.1.8 // indirect
cloud.google.com/go/kms v1.15.9 // indirect
cloud.google.com/go/storage v1.39.1 // indirect
@@ -163,7 +165,6 @@ require (
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
- github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/DisgoOrg/disgohook v1.4.3 // indirect
github.com/DisgoOrg/log v1.1.0 // indirect
@@ -201,10 +202,8 @@ require (
github.com/caarlos0/env/v6 v6.7.0 // indirect
github.com/caarlos0/go-shellwords v1.0.12 // indirect
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
- github.com/cavaliergopher/cpio v1.0.1 // indirect
- github.com/cavaliergopher/rpm v1.2.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
- github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.3.8 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
@@ -235,7 +234,7 @@ require (
github.com/gobwas/glob v0.2.3 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
- github.com/golang/glog v1.2.0 // indirect
+ github.com/golang/glog v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
@@ -249,7 +248,7 @@ require (
github.com/goreleaser/chglog v0.1.2 // indirect
github.com/goreleaser/fileglob v1.2.0 // indirect
github.com/gorilla/schema v1.4.1 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
@@ -321,18 +320,17 @@ require (
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
- go.opentelemetry.io/otel/metric v1.28.0 // indirect
- go.opentelemetry.io/otel/trace v1.28.0 // indirect
+ go.opentelemetry.io/otel/metric v1.31.0 // indirect
+ go.opentelemetry.io/otel/trace v1.31.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
- go.uber.org/goleak v1.3.0 // indirect
gocloud.dev v0.24.0 // indirect
- golang.org/x/term v0.21.0 // indirect
+ golang.org/x/term v0.25.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
- google.golang.org/protobuf v1.34.2 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
+ google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
diff --git a/go.sum b/go.sum
index c6df6615f640..80e08ec53a4f 100644
--- a/go.sum
+++ b/go.sum
@@ -43,8 +43,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
-cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
@@ -195,8 +195,8 @@ github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f h1:HR5nRmUQgX
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f/go.mod h1:f3HiCrHjHBdcm6E83vGaXh1KomZMA2P6aeo3hKx/wg0=
github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea h1:C9Xwp9fZf9BFJMsTqs8P+4PETXwJPUOuJZwBfVci+4A=
github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea/go.mod h1:N5eJIl14rhNCrE5I3O10HIyhZ1HpjaRHT9WDg1eXxtI=
-github.com/XSAM/otelsql v0.10.0 h1:y8o7q4NaZEV0dBiUC7TuNTHNKyDaX3Z4anntNu7dfYw=
-github.com/XSAM/otelsql v0.10.0/go.mod h1:7n9dZASOnVJncMmBPQjL5OdjQosb5gryCgsgNISnJVo=
+github.com/XSAM/otelsql v0.35.0 h1:nMdbU/XLmBIB6qZF61uDqy46E0LVA4ZgF/FCNw8Had4=
+github.com/XSAM/otelsql v0.35.0/go.mod h1:wO028mnLzmBpstK8XPsoeRLl/kgt417yjAwOGDIptTc=
github.com/aai/gocrypto v0.0.0-20160205191751-93df0c47f8b8/go.mod h1:nE/FnVUmtbP0EbgMVCUtDrm1+86H47QfJIdcmZb+J1s=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
@@ -324,8 +324,6 @@ github.com/caarlos0/testfs v0.4.3 h1:q1zEM5hgsssqWanAfevJYYa0So60DdK6wlJeTc/yfUE
github.com/caarlos0/testfs v0.4.3/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
-github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
-github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/cavaliergopher/rpm v1.2.0 h1:s0h+QeVK252QFTolkhGiMeQ1f+tMeIMhGl8B1HUmGUc=
github.com/cavaliergopher/rpm v1.2.0/go.mod h1:R0q3vTqa7RUvPofAZYrnjJ63hh2vngjFfphuXiExVos=
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
@@ -339,8 +337,8 @@ github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEex
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -511,12 +509,9 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
-github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@@ -559,8 +554,8 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
-github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
+github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -698,8 +693,8 @@ github.com/goreleaser/nfpm/v2 v2.10.0/go.mod h1:Bj/ztLvdnBnEgMae0fl/bLF6By1+yFFK
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -716,8 +711,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -1025,8 +1020,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -1230,29 +1225,28 @@ go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0 h1:QaNUlLvmettd1vnmFHrgBYQHearxWP3uO4h4F3pVtkM=
-go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0/go.mod h1:cJu+5jZwoZfkBOECSFtBZK/O7h/pY5djn0fwnIGnQ4A=
+go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.56.0 h1:k5inBHeCb4SXSmzkZGNX5oJj2RGg0y8LyLNHKR4hlb8=
+go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.56.0/go.mod h1:Q3hUOabe0Dekk+iwIJZDB3AzB/TVaECQ03Es8OV+vZ0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
-go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
-go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
-go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I=
+go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
+go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
-go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
-go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
-go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
-go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
-go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
-go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
-go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
-go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
+go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
+go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
+go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
+go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
+go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
+go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
+go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
@@ -1296,8 +1290,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
-golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1402,8 +1396,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
-golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
-golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1422,8 +1416,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
-golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
+golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1437,8 +1431,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1504,7 +1498,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1537,8 +1530,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
-golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1547,8 +1540,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
-golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
-golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1562,8 +1555,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
-golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1747,10 +1740,10 @@ google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEc
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae h1:HjgkYCl6cWQEKSHkpUp4Q8VB74swzyBwTz1wtTzahm0=
google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:i4np6Wrjp8EujFAUn0CM0SH+iZhY1EbrfzEIJbFkHFM=
-google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
-google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
+google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
+google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1776,8 +1769,8 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
-google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1792,8 +1785,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
diff --git a/handbook/company/communications.md b/handbook/company/communications.md
index c18434633734..12bf24e3f2ee 100644
--- a/handbook/company/communications.md
+++ b/handbook/company/communications.md
@@ -889,7 +889,7 @@ We're happy you've ventured a trip around the sun with Fleet- let's celebrate! T
### Compensation changes
-Fleet evaluates and (if relevant) updates compensation decisions yearly, shortly after the anniversary of a team member's start date. The Head of Digital Experience is responsible for the process to [update compensation](https://fleetdm.com/handbook/digital-experience#updating-compensation)
+Fleet evaluates and (if relevant) updates compensation decisions yearly, shortly after the anniversary of a team member's start date. The Head of Digital Experience is responsible for the process to [update compensation](https://fleetdm.com/handbook/digital-experience#update-a-team-members-compensation)
### Relocating
diff --git a/handbook/company/leadership.md b/handbook/company/leadership.md
index 18348d4020c4..34ee3493a083 100644
--- a/handbook/company/leadership.md
+++ b/handbook/company/leadership.md
@@ -244,7 +244,7 @@ A completed open position entry should look something like this:
- Create a pull request to add the new position to the YAML file.
-- _**Note:** The "living" URL where the new page will eventually exist on fleetdm.com won't ACTUALLY exist until your pull request is merged. A link will be added in the ["Open positions" section](https://fleetdm.com/handbook/company#open-positions) of the company handbook page.
+> _**Note:**_ The "living" URL where the new page will eventually exist on fleetdm.com won't ACTUALLY exist until your pull request is merged. A link will be added in the ["Open positions" section](https://fleetdm.com/handbook/company#open-positions) of the company handbook page.
3. **Link to pull request in "Fleeties:"** Include a link to your GitHub pull request in the "Job description" column for the new row you just added in "Fleeties".
diff --git a/handbook/company/open-positions.yml b/handbook/company/open-positions.yml
index 414a9478aa32..a03c8e0f8ad5 100644
--- a/handbook/company/open-positions.yml
+++ b/handbook/company/open-positions.yml
@@ -60,56 +60,3 @@
- 🛠️ Technical: You understand the software development processes. You understand that software quality matters.
- 🟣 Openness: You are flexible and open to new ideas and ways of working.
- ➕ Bonus: Cybersecurity or IT background.
-
-- jobTitle: 🚀 Quality Assurance Engineer
- department: Engineering
- hiringManagerName: Luke Heath
- hiringManagerGithubUsername: lukeheath
- hiringManagerLinkedInUrl: https://www.linkedin.com/in/lukeheath/
- responsibilities: |
- - ⏫ Work closely with CTO to continually improve overall quality assurance efficiency and effectiveness throughout the product design and engineering process.
- - 🐶 Own using Fleet the product at Fleet the business by ensuring all newly released features are leverged in Fleet's dogfood environment.
- - 🤝 Collaborate with the engineering managers and quality assurance engineers in the product groups, actively participating in some engineering scrum meetings, sprint planning, daily standups, sprint demos, sprint retrospectives, and estimation sessions.
- - 🌟 Contribute to the overall success of both the [MDM](https://fleetdm.com/handbook/company/product-groups#mdm-group) and [Endpoint Ops](https://fleetdm.com/handbook/company/product-groups#endpoint-ops-group) product groups by ensuring users receive valuable new features that work as intended.
- - 🧪 Develop and execute testing plans based on feature specifications, outlining step-by-step actions for each user role to confirm that features function as intended.
- - 🚀 Perform manual testing of newly developed features on all supported devices, platforms, and browsers, ensuring a seamless user experience.
- - 🐞 Identify, document, and report any bugs or unusual behavior, creating and assigning bug tickets to the appropriate engineering manager for resolution.
- - 🔧 Verify that bugs have been resolved after engineers have addressed them, repeating the testing process as needed.
- experience: |
- - 💭 3-5 years' of experience in a product quality, QA, or testing role.
- - 💖 Proficient in creating comprehensive testing plans.
- - ✍️ Experience working with engineering and product teams in an agile environment.
- - 🎯 Strong attention to detail and ability to identify inconsistencies or deviations from specifications.
- - 💡 Excellent communication and collaboration skills, with the ability to work closely with engineering and product teams.
- - 🌐 Experience in manual testing across various devices, platforms, and browsers.
- - 🏃♂️ Familiarity with agile development processes and scrum methodologies.
- - 👥 A customer-centric mindset, focusing on delivering value and a positive user experience.
- - 🤝 Collaboration: You work best in a participatory, team-based environment.
- - 🛠️ Technical: You understand the software development processes.
- - 🟣 Openness: You are flexible and open to new ideas and ways of working
- - ➕ Bonus: Cybersecurity or IT background
-
-- jobTitle: 🌐 Marketing Apprentice
- department: 🌐 Digital Experience
- hiringManagerName: Sam Pfluger
- hiringManagerGithubUsername: sampfluger88
- hiringManagerLinkedInUrl: https://www.linkedin.com/in/sampfluger88/
- responsibilities: |
- - 🧑💻 Work remotely, both within a team and individually, to help develop, document, and perform relevant responsibilities outlined at https://fleetdm.com/handbook/demand#responsibilities.
- - 🗣️ Act as a departmental point of contact, both internally and externally for Fleet.
- - 🦺 Help manage the flow of "planned" and "unplanned" work using multiple tools and ticketing systems.
- - 📣 Record and communicate relevant information and decisions to your team, other departments, and community members.
- - 📈 Collect and report on the departmental "Key Performance Indicators" (KPIs).
- - 🔧 Perform manual data collection and translation.
- - 🧑💼 Represent Fleet and interact with the community using multiple social media platforms.
- - 🎥 Edit image and video content to be used in social media posts, articles, ads, and on the website.
- experience: |
- - 🏃♂️ Strong desire to build a technical and operational-based skill set.
- - 🚀 Detail-oriented, highly organized, and able to move quickly to solve complex problems using boring solutions.
- - 🦉 Good understanding of Google Suite (Gmail, Google Calendar, Google Sheets, Google Docs, etc.)
- - 🫀 Experience dealing with sensitive personal information of team members and customers.
- - 🛠️ Strong writing and oral communication for general and technical topics.
- - 💭 Capable of understanding and translating technical concepts and personas.
- - 🤝 Ability to work in a process-driven team-based environment.
- - 🟣 Openness: You are flexible and open to new ideas and ways of working.
- - ➕ Bonus: Customer service\support background.
diff --git a/handbook/company/testimonials.yml b/handbook/company/testimonials.yml
index 909daf02e3b8..093d0dfeb59c 100644
--- a/handbook/company/testimonials.yml
+++ b/handbook/company/testimonials.yml
@@ -180,3 +180,11 @@
quoteAuthorProfileImageFilename: testimonial-author-eric-tan-99x99@2x.png
quoteAuthorJobTitle: CIO & Chief Security Officer at Flock Safety
productCategories: [Device management, Endpoint operations]
+-
+ quote: We've been using Fleet for a few years at Stripe and we couldn't be happier. The fact that it's also open-source made it easy for us to try it out, customise it to our needs, and seamlessly integrate it into our existing environment.
+ quoteImageFilename: social-proof-logo-stripe-67x32@2x.png
+ quoteLinkUrl: https://www.linkedin.com/posts/scottmacvicar_fleet-expands-its-gitops-focused-device-management-activity-7245288577876566017-vAHG?utm_source=share&utm_medium=member_desktop
+ quoteAuthorName: Scott MacVicar
+ quoteAuthorProfileImageFilename: testimonial-author-scott-macvicar-100x100@2x.png
+ quoteAuthorJobTitle: Head of Developer Infrastructure & Corporate Technology
+ productCategories: [Device management, Endpoint operations]
diff --git a/handbook/customer-success/README.md b/handbook/customer-success/README.md
index 694305a72094..4f0646c1fed1 100644
--- a/handbook/customer-success/README.md
+++ b/handbook/customer-success/README.md
@@ -11,6 +11,7 @@ This handbook page details processes specific to working [with](#contact-us) and
| Infrastructure Engineer | [Robert Fairburn](https://www.linkedin.com/in/robert-fairburn/) _([@rfairburn](https://github.com/rfairburn))_
| Customer Support (CSE/CSA) | [Kathy Satterlee](https://www.linkedin.com/in/ksatter/) _([@ksatter](https://github.com/ksatter))_ [Rebecca Cowart](https://www.linkedin.com/in/rebeccaui/) _([@rebeccaui](https://github.com/rebeccaui))_ [Brock Walters (CSA)](https://www.linkedin.com/in/brock-walters-247a2990/) _([@nonpunctual](https://github.com/nonpunctual))_ [Dale Ribeiro (CSA)](https://www.linkedin.com/in/daleribeiro/) _([@ddribeiro](https://github.com/ddribeiro))_ Ben Edwards _([@edwardsb](https://github.com/edwardsb))_
| Customer Success Manager (CSM) | [Jason Lewis](https://www.linkedin.com/in/jlewis0451/) _([@patagonia121](https://github.com/patagonia121))_ [Michael Pinto](https://www.linkedin.com/in/michael-pinto-a06b4515a/) _([@pintomi1989](https://github.com/pintomi1989))_
+| Technical Evangelist | [Zach Wasserman](https://www.linkedin.com/in/zacharywasserman/) _([@zwass](https://github.com/zwass))_
## Contact us
diff --git a/handbook/engineering/README.md b/handbook/engineering/README.md
index 312770cb5f5f..b4d949d8afba 100644
--- a/handbook/engineering/README.md
+++ b/handbook/engineering/README.md
@@ -66,7 +66,7 @@ If there are no product changes, and the DRI decides to prioritize the story, th
### Fix a bug
-All bug fix pull requests should have a mention back to the issue they resolve with # in the description or even in a comment. Please do not use any [automated words](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) since we don't want the tickets auto-closing when PR's are merged.
+All bug fix pull requests should have a mention back to the issue they resolve with `#` in the description or even in a comment. Please do not use any [automated words](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) since we don't want the tickets auto-closing when PR's are merged.
If the bug is labeled `~unreleased bug`, branch off and put your PR into `main`. These issues can be closed as soon as they complete QA.
If the bug is labeled `~released bug`, branch off and put your PR into `main`. After merging checkout the latest tag, for example `git checkout fleet-v4.48.2`, then `git fetch; git cherry-pick `. If the cherry-pick fails with a conflict call out in the ticket how to resolve or if it is sufficiently complicated call out this fix is not suited for the patch release process and should only be included in the end of sprint release. This approach makes sure the bug fix is not built on top of unreleased feature code, which can cause merge conflicts during patch releases.
diff --git a/handbook/product-design/product-design.rituals.yml b/handbook/product-design/product-design.rituals.yml
index 438cd1b5ca88..4bbf2029d870 100644
--- a/handbook/product-design/product-design.rituals.yml
+++ b/handbook/product-design/product-design.rituals.yml
@@ -9,7 +9,7 @@
task: "🎁 Feature fest" # 2024-03-06 TODO: Link to responsibility or corresponding "how to" info e.g. https://fleetdm.com/handbook/company/product-groups#making-changes
startedOn: "2024-03-07"
frequency: "Triweekly"
- description: "We make a decision regarding which customer and community feature requests can be committed to in the next six weeks."
+ description: "We make a decision regarding which feature requests can be prioritized."
moreInfoUrl: "https://fleetdm.com/handbook/company/product-groups#feature-fest"
dri: "noahtalerman"
-
@@ -65,7 +65,7 @@
task: "Product confirm and celebrate" # 2024-03-06 TODO: Link to responsibility or corresponding "how to" info e.g. https://fleetdm.com/handbook/company/product-groups#making-changes
startedOn: "2024-02-27"
frequency: "Weekly"
- description: "Review user stories we shipped but haven't closed/ Confirm all the loose ends are tied up: docs, internal and external comms, guides, pricing page, transparency page, user permissions."
+ description: "Review the checkboxes in user stories we shipped but haven't closed. Are they done? If not notify relevant contributor to help get them done."
moreInfoUrl:
dri: "noahtalerman"
-
diff --git a/handbook/sales/README.md b/handbook/sales/README.md
index e06cc96e6491..b0483b01f41e 100644
--- a/handbook/sales/README.md
+++ b/handbook/sales/README.md
@@ -8,7 +8,7 @@ This handbook page details processes specific to working [with](#contact-us) and
| Role | Contributor(s) |
|:--------------------------------------|:------------------------------------------------------------------------------------------------------------------------|
| Chief Revenue Officer (CRO) | [Alex Mitchell](https://www.linkedin.com/in/alexandercmitchell/) _([@alexmitchelliii](https://github.com/alexmitchelliii))_
-| Solutions Consulting (SC) | [Dave Herder](https://www.linkedin.com/in/daveherder/) _([@dherder](https://github.com/dherder))_ [Zach Wasserman](https://www.linkedin.com/in/zacharywasserman/) _([@zwass](https://github.com/zwass))_ [Allen Houchins](https://www.linkedin.com/in/allenhouchins/) _([@allenhouchins](https://github.com/allenhouchins))_ [Harrison Ravazzolo](https://www.linkedin.com/in/harrison-ravazzolo/) _([@harrisonravazzolo](https://github.com/harrisonravazzolo))_
+| Solutions Consulting (SC) | [Dave Herder](https://www.linkedin.com/in/daveherder/) _([@dherder](https://github.com/dherder))_ [Allen Houchins](https://www.linkedin.com/in/allenhouchins/) _([@allenhouchins](https://github.com/allenhouchins))_ [Harrison Ravazzolo](https://www.linkedin.com/in/harrison-ravazzolo/) _([@harrisonravazzolo](https://github.com/harrisonravazzolo))_
| Channel Sales | [Tom Ostertag](https://www.linkedin.com/in/tom-ostertag-77212791/) _([@tomostertag](https://github.com/TomOstertag))_
| Account Executive (AE) | [Patricia Ambrus](https://www.linkedin.com/in/pambrus/) _([@ambrusps](https://github.com/ambrusps))_ [Anthony Snyder](https://www.linkedin.com/in/anthonysnyder8/) _([@anthonysnyder8](https://github.com/AnthonySnyder8))_ [Paul Tardif](https://www.linkedin.com/in/paul-t-750833/) _([@phtardif1](https://github.com/phtardif1))_ [Kendra McKeever](https://www.linkedin.com/in/kendramckeever/) _([@KendraAtFleet](https://github.com/KendraAtFleet))_
diff --git a/it-and-security/default.yml b/it-and-security/default.yml
index 9b60ffb92cd7..9a8fef163259 100644
--- a/it-and-security/default.yml
+++ b/it-and-security/default.yml
@@ -20,6 +20,13 @@ org_settings:
entity_id: dogfood-eula.fleetdm.com
idp_name: Google Workspace
metadata_url: $DOGFOOD_MDM_SSO_METADATA_URL
+ volume_purchasing_program:
+ - location: Fleet Device Management Inc.
+ teams:
+ - "💻 Workstations"
+ - "💻🐣 Workstations (canary)"
+ - "📱🏢 Company-owned iPhones"
+ - "🔳🏢 Company-owned iPads"
org_info:
contact_url: https://fleetdm.com/company/contact
org_logo_url: ""
diff --git a/it-and-security/lib/servers.agent-options.yml b/it-and-security/lib/servers.agent-options.yml
index 61559952c08f..8098469bb025 100644
--- a/it-and-security/lib/servers.agent-options.yml
+++ b/it-and-security/lib/servers.agent-options.yml
@@ -11,3 +11,9 @@ config:
logger_tls_endpoint: /api/osquery/log
logger_tls_period: 10
pack_delimiter: /
+update_channels:
+ # We want to use these hosts to stick to stable releases
+ # to perform smoke tests after promoting edge to stable.
+ osqueryd: stable
+ orbit: stable
+ desktop: stable
diff --git a/it-and-security/teams/workstations.yml b/it-and-security/teams/workstations.yml
index 096fbeaa6fea..e2310071d6f2 100644
--- a/it-and-security/teams/workstations.yml
+++ b/it-and-security/teams/workstations.yml
@@ -13,7 +13,25 @@ team_settings:
enable_calendar_events: true
webhook_url: $DOGFOOD_WORKSTATIONS_CANARY_CALENDAR_WEBHOOK_URL
agent_options:
- path: ../lib/agent-options.yml
+ config:
+ decorators:
+ load:
+ - SELECT uuid AS host_uuid FROM system_info;
+ - SELECT hostname AS hostname FROM system_info;
+ options:
+ disable_distributed: false
+ distributed_interval: 10
+ distributed_plugin: tls
+ distributed_tls_max_attempts: 3
+ logger_tls_endpoint: /api/osquery/log
+ logger_tls_period: 10
+ pack_delimiter: /
+ update_channels:
+ # We want to use these hosts to stick to stable releases
+ # to perform smoke tests after promoting edge to stable.
+ osqueryd: stable
+ orbit: stable
+ desktop: stable
controls:
enable_disk_encryption: true
macos_settings:
diff --git a/orbit/changes/21256-fleet-desktop-trap b/orbit/changes/21256-fleet-desktop-trap
new file mode 100644
index 000000000000..707b46505f80
--- /dev/null
+++ b/orbit/changes/21256-fleet-desktop-trap
@@ -0,0 +1 @@
+- Gracefully shutdown fleet desktop when receiving interrupt and terminate signals
diff --git a/orbit/cmd/desktop/desktop.go b/orbit/cmd/desktop/desktop.go
index f0f1a5b2cf85..8234ed337b2a 100644
--- a/orbit/cmd/desktop/desktop.go
+++ b/orbit/cmd/desktop/desktop.go
@@ -6,8 +6,10 @@ import (
"errors"
"fmt"
"os"
+ "os/signal"
"path/filepath"
"runtime"
+ "syscall"
"time"
"fyne.io/systray"
@@ -438,17 +440,36 @@ func main() {
// FIXME: it doesn't look like this is actually triggering, at least when desktop gets
// killed (https://github.com/fleetdm/fleet/issues/21256)
onExit := func() {
- log.Info().Msg("exit")
+ log.Info().Msg("exiting")
if mdmMigrator != nil {
+ log.Debug().Err(err).Msg("exiting mdmMigrator")
mdmMigrator.Exit()
}
if swiftDialogCh != nil {
+ log.Debug().Err(err).Msg("exiting swiftDialogCh")
close(swiftDialogCh)
}
+ log.Debug().Msg("stopping ticker")
summaryTicker.Stop()
+ log.Debug().Msg("canceling offline watcher ctx")
cancelOfflineWatcherCtx()
}
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(
+ sigChan,
+ syscall.SIGINT,
+ syscall.SIGTERM,
+ syscall.SIGQUIT,
+ )
+
+ // Catch signals and exit gracefully
+ go func() {
+ s := <-sigChan
+ log.Info().Stringer("signal", s).Msg("Caught signal, exiting")
+ systray.Quit()
+ }()
+
systray.Run(onReady, onExit)
}
diff --git a/orbit/pkg/packaging/macos_templates.go b/orbit/pkg/packaging/macos_templates.go
index 98200821f987..62216d0afdc1 100644
--- a/orbit/pkg/packaging/macos_templates.go
+++ b/orbit/pkg/packaging/macos_templates.go
@@ -62,6 +62,9 @@ pkill fleet-desktop || true
# Remove any pre-existing version of the config
launchctl bootout "system/${DAEMON_LABEL}"
+# Make sure the launch daemon is enabled before we try to bootstrap it
+launchctl enable "system/${DAEMON_LABEL}"
+
# Add the daemon to the launchd system.
#
# We add retries because we've seen "launchctl bootstrap" fail
@@ -81,8 +84,6 @@ while ! launchctl bootstrap system "${DAEMON_PLIST}"; do
done
echo "Successfully bootstrap system ${DAEMON_PLIST}"
-# Enable the daemon
-launchctl enable "system/${DAEMON_LABEL}"
# Force the daemon to start
launchctl kickstart "system/${DAEMON_LABEL}"
{{- end }}
diff --git a/orbit/pkg/useraction/mdm_migration_darwin.go b/orbit/pkg/useraction/mdm_migration_darwin.go
index d835dad2d6bf..9e2ba90042be 100644
--- a/orbit/pkg/useraction/mdm_migration_darwin.go
+++ b/orbit/pkg/useraction/mdm_migration_darwin.go
@@ -102,7 +102,7 @@ type baseDialog struct {
}
func newBaseDialog(path string) *baseDialog {
- return &baseDialog{path: path, interruptCh: make(chan struct{})}
+ return &baseDialog{path: path, interruptCh: make(chan struct{}, 1)}
}
func (b *baseDialog) CanRun() bool {
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index e11a22ef5f34..b9c995dfb3d2 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -1222,7 +1222,8 @@ func upsertHostDEPAssignmentsDB(ctx context.Context, tx sqlx.ExtContext, hosts [
VALUES %s
ON DUPLICATE KEY UPDATE
added_at = CURRENT_TIMESTAMP,
- deleted_at = NULL`
+ deleted_at = NULL,
+ abm_token_id = VALUES(abm_token_id)`
args := []interface{}{}
values := []string{}
@@ -1487,30 +1488,90 @@ func (ds *Datastore) GetHostDEPAssignment(ctx context.Context, hostID uint) (*fl
return &res, nil
}
-func (ds *Datastore) DeleteHostDEPAssignments(ctx context.Context, serials []string) error {
+func (ds *Datastore) DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error {
if len(serials) == 0 {
return nil
}
- var args []interface{}
- for _, serial := range serials {
- args = append(args, serial)
+ type depAssignment struct {
+ HardwareSerial string `db:"hardware_serial"`
+ ABMTokenID uint `db:"abm_token_id"`
}
- stmt, args, err := sqlx.In(`
- UPDATE host_dep_assignments
- SET deleted_at = NOW()
- WHERE host_id IN (
- SELECT id FROM hosts WHERE hardware_serial IN (?)
- )`, args)
+ selectStmt, selectArgs, err := sqlx.In(`
+ SELECT h.hardware_serial, hdep.abm_token_id
+ FROM hosts h
+ JOIN host_dep_assignments hdep ON h.id = hdep.host_id
+ WHERE hdep.abm_token_id != ? AND h.hardware_serial IN (?)`, abmTokenID, serials)
if err != nil {
- return ctxerr.Wrap(ctx, err, "building IN statement")
+ return ctxerr.Wrap(ctx, err, "building IN statement for selecting host serials")
}
- if _, err := ds.writer(ctx).ExecContext(ctx, stmt, args...); err != nil {
- return ctxerr.Wrap(ctx, err, "deleting DEP assignment by serial")
+ var others []depAssignment
+ if err = sqlx.SelectContext(ctx, ds.reader(ctx), &others, selectStmt, selectArgs...); err != nil {
+ return ctxerr.Wrap(ctx, err, "selecting host serials")
+ }
+ tokenToSerials := map[uint][]string{}
+ for _, other := range others {
+ tokenToSerials[other.ABMTokenID] = append(tokenToSerials[other.ABMTokenID], other.HardwareSerial)
+ }
+ for otherTokenID, otherSerials := range tokenToSerials {
+ if err := ds.DeleteHostDEPAssignments(ctx, otherTokenID, otherSerials); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting DEP assignments for other ABM")
+ }
}
return nil
}
+func (ds *Datastore) DeleteHostDEPAssignments(ctx context.Context, abmTokenID uint, serials []string) error {
+ if len(serials) == 0 {
+ return nil
+ }
+
+ selectStmt, selectArgs, err := sqlx.In(`
+ SELECT h.id
+ FROM hosts h
+ JOIN host_dep_assignments hdep ON h.id = hdep.host_id
+ WHERE hdep.abm_token_id = ? AND h.hardware_serial IN (?)`, abmTokenID, serials)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "building IN statement for selecting host IDs")
+ }
+
+ return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
+ var hostIDs []uint
+ if err = sqlx.SelectContext(ctx, tx, &hostIDs, selectStmt, selectArgs...); err != nil {
+ return ctxerr.Wrap(ctx, err, "selecting host IDs")
+ }
+ if len(hostIDs) == 0 {
+ // Nothing to delete. Hosts may have already been transferred to another ABM.
+ return nil
+ }
+
+ stmt, args, err := sqlx.In(`
+ UPDATE host_dep_assignments
+ SET deleted_at = NOW()
+ WHERE host_id IN (?)`, hostIDs)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "building IN statement")
+ }
+ if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting DEP assignment by host_id")
+ }
+
+ // If pending host is no longer in ABM, we should delete it because it will never enroll in Fleet.
+ // If the host is later re-added to ABM, it will be re-created.
+ deletePendingStmt, args, err := sqlx.In(`
+ DELETE h, hmdm FROM hosts h
+ JOIN host_mdm hmdm ON h.id = hmdm.host_id
+ WHERE h.id IN (?) AND hmdm.enrollment_status = 'Pending'`, hostIDs)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "building delete IN statement")
+ }
+ if _, err := tx.ExecContext(ctx, deletePendingStmt, args...); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting pending hosts by host_id")
+ }
+ return nil
+ })
+}
+
func (ds *Datastore) RestoreMDMApplePendingDEPHost(ctx context.Context, host *fleet.Host) error {
ac, err := ds.AppConfig(ctx)
if err != nil {
@@ -3692,7 +3753,15 @@ func (ds *Datastore) GetMDMAppleDefaultSetupAssistant(ctx context.Context, teamI
return asstProf.ProfileUUID, asstProf.UpdatedAt, nil
}
-func (ds *Datastore) UpdateHostDEPAssignProfileResponses(ctx context.Context, payload *godep.ProfileResponse) error {
+func (ds *Datastore) UpdateHostDEPAssignProfileResponses(ctx context.Context, payload *godep.ProfileResponse, abmTokenID uint) error {
+ return ds.updateHostDEPAssignProfileResponses(ctx, payload, &abmTokenID)
+}
+
+func (ds *Datastore) UpdateHostDEPAssignProfileResponsesSameABM(ctx context.Context, payload *godep.ProfileResponse) error {
+ return ds.updateHostDEPAssignProfileResponses(ctx, payload, nil)
+}
+
+func (ds *Datastore) updateHostDEPAssignProfileResponses(ctx context.Context, payload *godep.ProfileResponse, abmTokenID *uint) error {
if payload == nil {
// caller should ensure this does not happen
level.Debug(ds.logger).Log("msg", "update host dep assign profiles responses received nil payload")
@@ -3722,38 +3791,53 @@ func (ds *Datastore) UpdateHostDEPAssignProfileResponses(ctx context.Context, pa
}
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
- if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, success, string(fleet.DEPAssignProfileResponseSuccess)); err != nil {
+ if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, success,
+ string(fleet.DEPAssignProfileResponseSuccess), abmTokenID); err != nil {
return err
}
- if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, notAccessible, string(fleet.DEPAssignProfileResponseNotAccessible)); err != nil {
+ if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, notAccessible,
+ string(fleet.DEPAssignProfileResponseNotAccessible), abmTokenID); err != nil {
return err
}
- if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, failed, string(fleet.DEPAssignProfileResponseFailed)); err != nil {
+ if err := updateHostDEPAssignProfileResponses(ctx, tx, ds.logger, payload.ProfileUUID, failed,
+ string(fleet.DEPAssignProfileResponseFailed), abmTokenID); err != nil {
return err
}
return nil
})
}
-func updateHostDEPAssignProfileResponses(ctx context.Context, tx sqlx.ExtContext, logger log.Logger, profileUUID string, serials []string, status string) error {
+func updateHostDEPAssignProfileResponses(ctx context.Context, tx sqlx.ExtContext, logger log.Logger, profileUUID string, serials []string,
+ status string, abmTokenID *uint) error {
if len(serials) == 0 {
return nil
}
- stmt := `
+ setABMTokenID := ""
+ if abmTokenID != nil {
+ setABMTokenID = "abm_token_id = ?," //nolint:gosec // G101 false positive
+ }
+ stmt := fmt.Sprintf(`
UPDATE
host_dep_assignments
JOIN
hosts ON id = host_id
SET
+ %s
profile_uuid = ?,
assign_profile_response = ?,
response_updated_at = CURRENT_TIMESTAMP,
retry_job_id = 0
WHERE
hardware_serial IN (?)
-`
- stmt, args, err := sqlx.In(stmt, profileUUID, status, serials)
+`, setABMTokenID)
+ var args []interface{}
+ var err error
+ if abmTokenID != nil {
+ stmt, args, err = sqlx.In(stmt, abmTokenID, profileUUID, status, serials)
+ } else {
+ stmt, args, err = sqlx.In(stmt, profileUUID, status, serials)
+ }
if err != nil {
return ctxerr.Wrap(ctx, err, "prepare statement arguments")
}
@@ -3763,7 +3847,8 @@ WHERE
}
n, _ := res.RowsAffected()
- level.Info(logger).Log("msg", "update host dep assign profile responses", "profile_uuid", profileUUID, "status", status, "devices", n, "serials", fmt.Sprintf("%s", serials))
+ level.Info(logger).Log("msg", "update host dep assign profile responses", "profile_uuid", profileUUID, "status", status, "devices", n,
+ "serials", fmt.Sprintf("%s", serials), "abm_token_id", abmTokenID)
return nil
}
diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go
index 8ac54bb87c91..42bf507950e9 100644
--- a/server/datastore/mysql/apple_mdm_test.go
+++ b/server/datastore/mysql/apple_mdm_test.go
@@ -3751,7 +3751,7 @@ func testListMDMAppleSerials(t *testing.T, ds *Datastore) {
// ABM assignment was deleted
err = ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}, abmToken.ID)
require.NoError(t, err)
- err = ds.DeleteHostDEPAssignments(ctx, []string{h.HardwareSerial})
+ err = ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{h.HardwareSerial})
require.NoError(t, err)
case i == 6:
// assigned in ABM, but we don't have a serial
@@ -4687,7 +4687,7 @@ func testMDMAppleDeleteHostDEPAssignments(t *testing.T, ds *Datastore) {
_, err := ds.IngestMDMAppleDevicesFromDEPSync(ctx, devices, abmToken.ID, nil, nil, nil)
require.NoError(t, err)
- err = ds.DeleteHostDEPAssignments(ctx, tt.in)
+ err = ds.DeleteHostDEPAssignments(ctx, abmToken.ID, tt.in)
if tt.err == "" {
require.NoError(t, err)
} else {
@@ -5650,7 +5650,7 @@ func testMDMAppleDEPAssignmentUpdates(t *testing.T, ds *Datastore) {
require.Equal(t, h.ID, assignment.HostID)
require.Nil(t, assignment.DeletedAt)
- err = ds.DeleteHostDEPAssignments(ctx, []string{h.HardwareSerial})
+ err = ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{h.HardwareSerial})
require.NoError(t, err)
assignment, err = ds.GetHostDEPAssignment(ctx, h.ID)
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index a745e3582c75..c2305dfe97ba 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -792,15 +792,7 @@ func queryStatsToScheduledQueryStats(queriesStats []fleet.QueryStats, packName s
// of MDM host data. It assumes that hostMDMJoin is included in the query.
const hostMDMSelect = `,
JSON_OBJECT(
- 'enrollment_status',
- CASE
- WHEN hmdm.is_server = 1 THEN NULL
- WHEN hmdm.enrolled = 1 AND hmdm.installed_from_dep = 0 THEN 'On (manual)'
- WHEN hmdm.enrolled = 1 AND hmdm.installed_from_dep = 1 THEN 'On (automatic)'
- WHEN hmdm.enrolled = 0 AND hmdm.installed_from_dep = 1 THEN 'Pending'
- WHEN hmdm.enrolled = 0 AND hmdm.installed_from_dep = 0 THEN 'Off'
- ELSE NULL
- END,
+ 'enrollment_status', hmdm.enrollment_status,
'dep_profile_error',
CASE
WHEN hdep.assign_profile_response = '` + string(fleet.DEPAssignProfileResponseFailed) + `' THEN CAST(TRUE AS JSON)
@@ -872,6 +864,7 @@ const hostMDMJoin = `
hm.is_server,
hm.enrolled,
hm.installed_from_dep,
+ hm.enrollment_status,
hm.server_url,
hm.mdm_id,
hm.host_id,
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index bf7166352f9e..d87a880552a0 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -7624,7 +7624,7 @@ func testHostsGetHostMDMCheckinInfo(t *testing.T, ds *Datastore) {
require.True(t, info.DEPAssignedToFleet)
require.True(t, info.OsqueryEnrolled)
- err = ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial})
+ err = ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial})
require.NoError(t, err)
info, err = ds.GetHostMDMCheckinInfo(ctx, host.UUID)
require.NoError(t, err)
@@ -7757,7 +7757,7 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
// simulate a failed JSON profile assignment
err = updateHostDEPAssignProfileResponses(
ctx, ds.writer(ctx), ds.logger,
- "foo", []string{hFleet.HardwareSerial}, string(fleet.DEPAssignProfileResponseFailed),
+ "foo", []string{hFleet.HardwareSerial}, string(fleet.DEPAssignProfileResponseFailed), &abmToken.ID,
)
require.NoError(t, err)
loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey)
diff --git a/server/datastore/mysql/migrations/tables/20241022140321_AddStatusToHostMDM.go b/server/datastore/mysql/migrations/tables/20241022140321_AddStatusToHostMDM.go
new file mode 100644
index 000000000000..728c7f7cd101
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20241022140321_AddStatusToHostMDM.go
@@ -0,0 +1,39 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20241022140321, Down_20241022140321)
+}
+
+func Up_20241022140321(tx *sql.Tx) error {
+ if !columnsExists(tx, "host_mdm", "enrollment_status", "created_at", "updated_at") {
+ if _, err := tx.Exec(`
+ALTER TABLE host_mdm
+ADD COLUMN enrollment_status ENUM('On (manual)', 'On (automatic)', 'Pending', 'Off') COLLATE utf8mb4_unicode_ci
+GENERATED ALWAYS AS (
+ CASE
+ WHEN is_server = 1 THEN NULL
+ WHEN enrolled = 1 AND installed_from_dep = 0 THEN 'On (manual)'
+ WHEN enrolled = 1 AND installed_from_dep = 1 THEN 'On (automatic)'
+ WHEN enrolled = 0 AND installed_from_dep = 1 THEN 'Pending'
+ WHEN enrolled = 0 AND installed_from_dep = 0 THEN 'Off'
+ ELSE NULL
+ END
+) VIRTUAL NULL,
+ADD COLUMN created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+ADD COLUMN updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)
+ `); err != nil {
+ return fmt.Errorf("failed to alter host_mdm: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func Down_20241022140321(_ *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/migration.go b/server/datastore/mysql/migrations/tables/migration.go
index ca62d5cd9ba8..779b8c2a873f 100644
--- a/server/datastore/mysql/migrations/tables/migration.go
+++ b/server/datastore/mysql/migrations/tables/migration.go
@@ -3,6 +3,8 @@ package tables
import (
"database/sql"
"encoding/json"
+ "fmt"
+ "strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/goose"
@@ -29,9 +31,23 @@ AND CONSTRAINT_NAME = ?
}
func columnExists(tx *sql.Tx, table, column string) bool {
+ return columnsExists(tx, table, column)
+}
+
+func columnsExists(tx *sql.Tx, table string, columns ...string) bool {
+ if len(columns) == 0 {
+ return false
+ }
+ inColumns := strings.TrimRight(strings.Repeat("?,", len(columns)), ",")
+ args := make([]interface{}, 0, len(columns)+1)
+ args = append(args, table)
+ for _, column := range columns {
+ args = append(args, column)
+ }
+
var count int
err := tx.QueryRow(
- `
+ fmt.Sprintf(`
SELECT
count(*)
FROM
@@ -39,15 +55,14 @@ FROM
WHERE
TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
- AND COLUMN_NAME = ?
-`,
- table, column,
+ AND COLUMN_NAME IN (%s)
+`, inColumns), args...,
).Scan(&count)
if err != nil {
return false
}
- return count > 0
+ return count == len(columns)
}
func tableExists(tx *sql.Tx, table string) bool {
diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go
index ac7c9d976a1a..928e661b6010 100644
--- a/server/datastore/mysql/mysql.go
+++ b/server/datastore/mysql/mysql.go
@@ -399,7 +399,28 @@ var otelTracedDriverName string
func init() {
var err error
- otelTracedDriverName, err = otelsql.Register("mysql", semconv.DBSystemMySQL.Value.AsString())
+ otelTracedDriverName, err = otelsql.Register("mysql",
+ otelsql.WithAttributes(semconv.DBSystemMySQL),
+ otelsql.WithSpanOptions(otelsql.SpanOptions{
+ // DisableErrSkip ignores driver.ErrSkip errors which are frequently returned by the MySQL driver
+ // when certain optional methods or paths are not implemented/taken.
+ // For example: interpolateParams=false (the secure default) will not do a parametrized sql.conn.query directly without preparing it first, causing driver.ErrSkip
+ DisableErrSkip: true,
+ // Omitting span for sql.conn.reset_session since it takes ~1us and doesn't provide useful information
+ OmitConnResetSession: true,
+ // Omitting span for sql.rows since it is very quick and typically doesn't provide useful information beyond what's already reported by prepare/exec/query
+ OmitRows: true,
+ }),
+ // WithSpanNameFormatter allows us to customize the span name, which is especially useful for SQL queries run outside an HTTPS transaction,
+ // which do not belong to a parent span, show up as their own trace, and would otherwise be named "sql.conn.query" or "sql.conn.exec".
+ otelsql.WithSpanNameFormatter(func(ctx context.Context, method otelsql.Method, query string) string {
+ if query == "" {
+ return string(method)
+ }
+ // Append query with extra whitespaces removed
+ return string(method) + ": " + strings.Join(strings.Fields(query), " ")
+ }),
+ )
if err != nil {
panic(err)
}
diff --git a/server/datastore/mysql/mysql_test.go b/server/datastore/mysql/mysql_test.go
index 228715fffff8..89ceed3f179f 100644
--- a/server/datastore/mysql/mysql_test.go
+++ b/server/datastore/mysql/mysql_test.go
@@ -595,7 +595,6 @@ func TestWhereFilterHostsByTeams(t *testing.T) {
for _, tt := range testCases {
tt := tt
t.Run("", func(t *testing.T) {
- t.Parallel()
ds := &Datastore{logger: log.NewNopLogger()}
sql := ds.whereFilterHostsByTeams(tt.filter, "hosts")
assert.Equal(t, tt.expected, sql)
@@ -631,7 +630,6 @@ func TestWhereOmitIDs(t *testing.T) {
for _, tt := range testCases {
tt := tt
t.Run("", func(t *testing.T) {
- t.Parallel()
ds := &Datastore{logger: log.NewNopLogger()}
sql := ds.whereOmitIDs("id", tt.omits)
assert.Equal(t, tt.expected, sql)
@@ -856,7 +854,6 @@ func TestWhereFilterTeams(t *testing.T) {
for _, tt := range testCases {
tt := tt
t.Run("", func(t *testing.T) {
- t.Parallel()
ds := &Datastore{logger: log.NewNopLogger()}
sql := ds.whereFilterTeams(tt.filter, "t")
assert.Equal(t, tt.expected, sql)
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 0c89a8961d65..5e914eef8d9b 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -372,6 +372,9 @@ CREATE TABLE `host_mdm` (
`mdm_id` int unsigned DEFAULT NULL,
`is_server` tinyint(1) DEFAULT NULL,
`fleet_enroll_ref` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
+ `enrollment_status` enum('On (manual)','On (automatic)','Pending','Off') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS ((case when (`is_server` = 1) then NULL when ((`enrolled` = 1) and (`installed_from_dep` = 0)) then _utf8mb4'On (manual)' when ((`enrolled` = 1) and (`installed_from_dep` = 1)) then _utf8mb4'On (automatic)' when ((`enrolled` = 0) and (`installed_from_dep` = 1)) then _utf8mb4'Pending' when ((`enrolled` = 0) and (`installed_from_dep` = 0)) then _utf8mb4'Off' else NULL end)) VIRTUAL,
+ `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+ `updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`host_id`),
KEY `host_mdm_mdm_id_idx` (`mdm_id`),
KEY `host_mdm_enrolled_installed_from_dep_idx` (`enrolled`,`installed_from_dep`)
@@ -1096,9 +1099,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
-) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=326 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=327 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20240925111236,1,'2020-01-01 01:01:01'),(314,20240925112748,1,'2020-01-01 01:01:01'),(315,20241002104104,1,'2020-01-01 01:01:01'),(316,20241002104105,1,'2020-01-01 01:01:01'),(317,20241002104106,1,'2020-01-01 01:01:01'),(318,20241002210000,1,'2020-01-01 01:01:01'),(319,20241003145349,1,'2020-01-01 01:01:01'),(320,20241004005000,1,'2020-01-01 01:01:01'),(321,20241008083925,1,'2020-01-01 01:01:01'),(322,20241009090010,1,'2020-01-01 01:01:01'),(323,20241009141855,1,'2020-01-01 01:01:01'),(324,20241017163402,1,'2020-01-01 01:01:01'),(325,20241021224359,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20240925111236,1,'2020-01-01 01:01:01'),(314,20240925112748,1,'2020-01-01 01:01:01'),(315,20241002104104,1,'2020-01-01 01:01:01'),(316,20241002104105,1,'2020-01-01 01:01:01'),(317,20241002104106,1,'2020-01-01 01:01:01'),(318,20241002210000,1,'2020-01-01 01:01:01'),(319,20241003145349,1,'2020-01-01 01:01:01'),(320,20241004005000,1,'2020-01-01 01:01:01'),(321,20241008083925,1,'2020-01-01 01:01:01'),(322,20241009090010,1,'2020-01-01 01:01:01'),(323,20241009141855,1,'2020-01-01 01:01:01'),(324,20241017163402,1,'2020-01-01 01:01:01'),(325,20241021224359,1,'2020-01-01 01:01:01'),(326,20241022140321,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `mobile_device_management_solutions` (
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index a02d423b8144..93a4d6ff2207 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -1290,13 +1290,21 @@ type Datastore interface {
// a map that only contains the serials that have a matching row in the `hosts` table.
GetMatchingHostSerials(ctx context.Context, serials []string) (map[string]*Host, error)
+ // DeleteHostDEPAssignmentsFromAnotherABM makes as deleted any DEP entry that matches one of the provided serials only if the entry is NOT associated to the provided ABM token.
+ DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error
+
// DeleteHostDEPAssignments marks as deleted entries in
- // host_dep_assignments for host with matching serials.
- DeleteHostDEPAssignments(ctx context.Context, serials []string) error
+ // host_dep_assignments for host with matching serials only if the entry is associated to the provided ABM token.
+ DeleteHostDEPAssignments(ctx context.Context, abmTokenID uint, serials []string) error
// UpdateHostDEPAssignProfileResponses receives a profile UUID and threes lists of serials, each representing
+ // one of the three possible responses, and updates the host_dep_assignments table with the corresponding responses. For each response, it also sets the ABM token id in the table to the provided value.
+ UpdateHostDEPAssignProfileResponses(ctx context.Context, resp *godep.ProfileResponse, abmTokenID uint) error
+
+ // UpdateHostDEPAssignProfileResponsesSameABM receives a profile UUID and threes lists of serials, each representing
// one of the three possible responses, and updates the host_dep_assignments table with the corresponding responses.
- UpdateHostDEPAssignProfileResponses(ctx context.Context, resp *godep.ProfileResponse) error
+ // The ABM token ID remains unchanged.
+ UpdateHostDEPAssignProfileResponsesSameABM(ctx context.Context, resp *godep.ProfileResponse) error
// ScreenDEPAssignProfileSerialsForCooldown returns the serials that are still in cooldown and the
// ones that are ready to be assigned a profile. If `screenRetryJobs` is true, it will also skip
diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go
index cc4af4493633..49e1125f247a 100644
--- a/server/mdm/apple/apple_mdm.go
+++ b/server/mdm/apple/apple_mdm.go
@@ -558,10 +558,25 @@ func (d *DEPService) processDeviceResponse(
return nil
}
- var addedDevices []godep.Device
+ var addedDevicesSlice []godep.Device
+ var addedSerials []string
var deletedSerials []string
var modifiedSerials []string
+ addedDevices := map[string]godep.Device{}
modifiedDevices := map[string]godep.Device{}
+ deletedDevices := map[string]godep.Device{}
+
+ // This service may return the same device more than once. You must resolve duplicates by matching on the device
+ // serial number and the op_type and op_date fields. The record with the latest op_date indicates the last known
+ // state of the device in DEP.
+ // Reference: https://developer.apple.com/documentation/devicemanagement/sync_the_list_of_devices#discussion
+ keepRecent := func(device godep.Device, existing map[string]godep.Device) {
+ existingDevice, ok := existing[device.SerialNumber]
+ if !ok || device.OpDate.After(existingDevice.OpDate) {
+ existing[device.SerialNumber] = device
+ }
+ }
+
for _, device := range resp.Devices {
level.Debug(d.logger).Log(
"msg", "device",
@@ -580,12 +595,11 @@ func (d *DEPService) processDeviceResponse(
// Empty op_type come from the first call to FetchDevices without a cursor,
// and we do want to assign profiles to them.
case "added", "":
- addedDevices = append(addedDevices, device)
+ keepRecent(device, addedDevices)
case "modified":
- modifiedDevices[device.SerialNumber] = device
- modifiedSerials = append(modifiedSerials, device.SerialNumber)
+ keepRecent(device, modifiedDevices)
case "deleted":
- deletedSerials = append(deletedSerials, device.SerialNumber)
+ keepRecent(device, deletedDevices)
default:
level.Warn(d.logger).Log(
"msg", "unrecognized op_type",
@@ -595,6 +609,33 @@ func (d *DEPService) processDeviceResponse(
}
}
+ // Remove added/modified devices if they have been subsequently deleted
+ // Remove deleted devices if they have been subsequently added (or re-added)
+ for _, deletedDevice := range deletedDevices {
+ modifiedDevice, ok := modifiedDevices[deletedDevice.SerialNumber]
+ if ok && deletedDevice.OpDate.After(modifiedDevice.OpDate) {
+ delete(modifiedDevices, deletedDevice.SerialNumber)
+ }
+ addedDevice, ok := addedDevices[deletedDevice.SerialNumber]
+ if ok {
+ if deletedDevice.OpDate.After(addedDevice.OpDate) {
+ delete(addedDevices, deletedDevice.SerialNumber)
+ } else {
+ delete(deletedDevices, deletedDevice.SerialNumber)
+ }
+ }
+ }
+
+ for _, addedDevice := range addedDevices {
+ addedDevicesSlice = append(addedDevicesSlice, addedDevice)
+ }
+ for _, modifiedDevice := range modifiedDevices {
+ modifiedSerials = append(modifiedSerials, modifiedDevice.SerialNumber)
+ }
+ for _, deletedDevice := range deletedDevices {
+ deletedSerials = append(deletedSerials, deletedDevice.SerialNumber)
+ }
+
// find out if we already have entries in the `hosts` table with
// matching serial numbers for any devices with op_type = "modified"
existingSerials, err := d.ds.GetMatchingHostSerials(ctx, modifiedSerials)
@@ -611,16 +652,25 @@ func (d *DEPService) processDeviceResponse(
// the wrong op_type.
for _, d := range modifiedDevices {
if _, ok := existingSerials[d.SerialNumber]; !ok {
- addedDevices = append(addedDevices, d)
+ addedDevicesSlice = append(addedDevicesSlice, d)
}
}
- err = d.ds.DeleteHostDEPAssignments(ctx, deletedSerials)
+ // Check if added devices belong to another ABM server. If so, we must delete them before adding them.
+ for _, device := range addedDevicesSlice {
+ addedSerials = append(addedSerials, device.SerialNumber)
+ }
+ err = d.ds.DeleteHostDEPAssignmentsFromAnotherABM(ctx, abmTokenID, addedSerials)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting dep assignments from another abm")
+ }
+
+ err = d.ds.DeleteHostDEPAssignments(ctx, abmTokenID, deletedSerials)
if err != nil {
return ctxerr.Wrap(ctx, err, "deleting DEP assignments")
}
- n, err := d.ds.IngestMDMAppleDevicesFromDEPSync(ctx, addedDevices, abmTokenID, macOSTeam, iosTeam, ipadTeam)
+ n, err := d.ds.IngestMDMAppleDevicesFromDEPSync(ctx, addedDevicesSlice, abmTokenID, macOSTeam, iosTeam, ipadTeam)
switch {
case err != nil:
level.Error(kitlog.With(d.logger)).Log("err", err)
@@ -631,7 +681,8 @@ func (d *DEPService) processDeviceResponse(
level.Debug(kitlog.With(d.logger)).Log("msg", "no DEP hosts to add")
}
- level.Debug(kitlog.With(d.logger)).Log("msg", "devices to assign DEP profiles", "to_add", len(addedDevices), "to_remove", deletedSerials, "to_modify", modifiedSerials)
+ level.Debug(kitlog.With(d.logger)).Log("msg", "devices to assign DEP profiles", "to_add", len(addedDevicesSlice), "to_remove",
+ deletedSerials, "to_modify", modifiedSerials)
// at this point, the hosts rows are created for the devices, with the
// correct team_id, so we know what team-specific profile needs to be applied.
@@ -652,7 +703,7 @@ func (d *DEPService) processDeviceResponse(
// each new device should be assigned the DEP profile of the default
// ABM team as configured by the IT admin.
devicesByTeam := map[*uint][]godep.Device{}
- for _, newDevice := range addedDevices {
+ for _, newDevice := range addedDevicesSlice {
var teamID *uint
switch newDevice.DeviceFamily {
case "iPhone":
@@ -672,7 +723,9 @@ func (d *DEPService) processDeviceResponse(
for _, existingHost := range existingSerials {
dd, ok := modifiedDevices[existingHost.HardwareSerial]
if !ok {
- level.Error(kitlog.With(d.logger)).Log("msg", "serial coming from ABM is in the databse, but it's not in the list of modified devices", "serial", existingHost.HardwareSerial)
+ level.Error(kitlog.With(d.logger)).Log("msg",
+ "serial coming from ABM is in the database, but it's not in the list of modified devices", "serial",
+ existingHost.HardwareSerial)
continue
}
existingHosts = append(existingHosts, *existingHost)
@@ -748,7 +801,7 @@ func (d *DEPService) processDeviceResponse(
logs = append(logs, logCountsForResults(apiResp.Devices)...)
level.Info(logger).Log(logs...)
- if err := d.ds.UpdateHostDEPAssignProfileResponses(ctx, apiResp); err != nil {
+ if err := d.ds.UpdateHostDEPAssignProfileResponses(ctx, apiResp, abmTokenID); err != nil {
return ctxerr.Wrap(ctx, err, "update host dep assign profile responses")
}
}
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index 9e20f2d1d622..93b125a007ad 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -851,9 +851,13 @@ type GetMDMAppleDefaultSetupAssistantFunc func(ctx context.Context, teamID *uint
type GetMatchingHostSerialsFunc func(ctx context.Context, serials []string) (map[string]*fleet.Host, error)
-type DeleteHostDEPAssignmentsFunc func(ctx context.Context, serials []string) error
+type DeleteHostDEPAssignmentsFromAnotherABMFunc func(ctx context.Context, abmTokenID uint, serials []string) error
-type UpdateHostDEPAssignProfileResponsesFunc func(ctx context.Context, resp *godep.ProfileResponse) error
+type DeleteHostDEPAssignmentsFunc func(ctx context.Context, abmTokenID uint, serials []string) error
+
+type UpdateHostDEPAssignProfileResponsesFunc func(ctx context.Context, resp *godep.ProfileResponse, abmTokenID uint) error
+
+type UpdateHostDEPAssignProfileResponsesSameABMFunc func(ctx context.Context, resp *godep.ProfileResponse) error
type ScreenDEPAssignProfileSerialsForCooldownFunc func(ctx context.Context, serials []string) (skipSerialsByOrgName map[string][]string, serialsByOrgName map[string][]string, err error)
@@ -2381,12 +2385,18 @@ type DataStore struct {
GetMatchingHostSerialsFunc GetMatchingHostSerialsFunc
GetMatchingHostSerialsFuncInvoked bool
+ DeleteHostDEPAssignmentsFromAnotherABMFunc DeleteHostDEPAssignmentsFromAnotherABMFunc
+ DeleteHostDEPAssignmentsFromAnotherABMFuncInvoked bool
+
DeleteHostDEPAssignmentsFunc DeleteHostDEPAssignmentsFunc
DeleteHostDEPAssignmentsFuncInvoked bool
UpdateHostDEPAssignProfileResponsesFunc UpdateHostDEPAssignProfileResponsesFunc
UpdateHostDEPAssignProfileResponsesFuncInvoked bool
+ UpdateHostDEPAssignProfileResponsesSameABMFunc UpdateHostDEPAssignProfileResponsesSameABMFunc
+ UpdateHostDEPAssignProfileResponsesSameABMFuncInvoked bool
+
ScreenDEPAssignProfileSerialsForCooldownFunc ScreenDEPAssignProfileSerialsForCooldownFunc
ScreenDEPAssignProfileSerialsForCooldownFuncInvoked bool
@@ -5715,18 +5725,32 @@ func (s *DataStore) GetMatchingHostSerials(ctx context.Context, serials []string
return s.GetMatchingHostSerialsFunc(ctx, serials)
}
-func (s *DataStore) DeleteHostDEPAssignments(ctx context.Context, serials []string) error {
+func (s *DataStore) DeleteHostDEPAssignmentsFromAnotherABM(ctx context.Context, abmTokenID uint, serials []string) error {
+ s.mu.Lock()
+ s.DeleteHostDEPAssignmentsFromAnotherABMFuncInvoked = true
+ s.mu.Unlock()
+ return s.DeleteHostDEPAssignmentsFromAnotherABMFunc(ctx, abmTokenID, serials)
+}
+
+func (s *DataStore) DeleteHostDEPAssignments(ctx context.Context, abmTokenID uint, serials []string) error {
s.mu.Lock()
s.DeleteHostDEPAssignmentsFuncInvoked = true
s.mu.Unlock()
- return s.DeleteHostDEPAssignmentsFunc(ctx, serials)
+ return s.DeleteHostDEPAssignmentsFunc(ctx, abmTokenID, serials)
}
-func (s *DataStore) UpdateHostDEPAssignProfileResponses(ctx context.Context, resp *godep.ProfileResponse) error {
+func (s *DataStore) UpdateHostDEPAssignProfileResponses(ctx context.Context, resp *godep.ProfileResponse, abmTokenID uint) error {
s.mu.Lock()
s.UpdateHostDEPAssignProfileResponsesFuncInvoked = true
s.mu.Unlock()
- return s.UpdateHostDEPAssignProfileResponsesFunc(ctx, resp)
+ return s.UpdateHostDEPAssignProfileResponsesFunc(ctx, resp, abmTokenID)
+}
+
+func (s *DataStore) UpdateHostDEPAssignProfileResponsesSameABM(ctx context.Context, resp *godep.ProfileResponse) error {
+ s.mu.Lock()
+ s.UpdateHostDEPAssignProfileResponsesSameABMFuncInvoked = true
+ s.mu.Unlock()
+ return s.UpdateHostDEPAssignProfileResponsesSameABMFunc(ctx, resp)
}
func (s *DataStore) ScreenDEPAssignProfileSerialsForCooldown(ctx context.Context, serials []string) (skipSerialsByOrgName map[string][]string, serialsByOrgName map[string][]string, err error) {
diff --git a/server/service/appconfig.go b/server/service/appconfig.go
index 77a4f1440b0e..420a49ddb8b0 100644
--- a/server/service/appconfig.go
+++ b/server/service/appconfig.go
@@ -493,9 +493,13 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
return nil, ctxerr.Wrap(ctx, err, "validating ABM token assignments")
}
- vppAssignments, err := svc.validateVPPAssignments(ctx, &newAppConfig.MDM, invalid, license)
- if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "validating VPP token assignments")
+ var vppAssignments map[uint][]uint
+ vppAssignmentsDefined := newAppConfig.MDM.VolumePurchasingProgram.Set && newAppConfig.MDM.VolumePurchasingProgram.Valid
+ if vppAssignmentsDefined {
+ vppAssignments, err = svc.validateVPPAssignments(ctx, newAppConfig.MDM.VolumePurchasingProgram.Value, invalid, license)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "validating VPP token assignments")
+ }
}
if invalid.HasErrors() {
@@ -669,27 +673,25 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
}
}
- // Reset teams for VPP tokens that exist in Fleet but aren't present in the config being passed
- clear(tokensInCfg)
-
- for _, t := range newAppConfig.MDM.VolumePurchasingProgram.Value {
- tokensInCfg[t.Location] = struct{}{}
- }
-
- vppToks, err := svc.ds.ListVPPTokens(ctx)
- if err != nil {
- return nil, ctxerr.Wrap(ctx, err, "listing VPP tokens")
- }
- for _, tok := range vppToks {
- if _, ok := tokensInCfg[tok.Location]; !ok {
- tok.Teams = nil
- if _, err := svc.ds.UpdateVPPTokenTeams(ctx, tok.ID, nil); err != nil {
- return nil, ctxerr.Wrap(ctx, err, "saving VPP token teams")
+ if vppAssignmentsDefined {
+ // 1. Reset teams for VPP tokens that exist in Fleet but aren't present in the config being passed
+ clear(tokensInCfg)
+ for _, t := range newAppConfig.MDM.VolumePurchasingProgram.Value {
+ tokensInCfg[t.Location] = struct{}{}
+ }
+ vppToks, err := svc.ds.ListVPPTokens(ctx)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "listing VPP tokens")
+ }
+ for _, tok := range vppToks {
+ if _, ok := tokensInCfg[tok.Location]; !ok {
+ tok.Teams = nil
+ if _, err := svc.ds.UpdateVPPTokenTeams(ctx, tok.ID, nil); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "saving VPP token teams")
+ }
}
}
- }
-
- if appConfig.MDM.VolumePurchasingProgram.Set && appConfig.MDM.VolumePurchasingProgram.Valid {
+ // 2. Set VPP assignments that are defined in the config.
for tokenID, tokenTeams := range vppAssignments {
if _, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, tokenTeams); err != nil {
var errTokConstraint fleet.ErrVPPTokenTeamConstraint
@@ -1209,76 +1211,77 @@ func (svc *Service) validateABMAssignments(
func (svc *Service) validateVPPAssignments(
ctx context.Context,
- mdm *fleet.MDM,
+ volumePurchasingProgramInfo []fleet.MDMAppleVolumePurchasingProgramInfo,
invalid *fleet.InvalidArgumentError,
license *fleet.LicenseInfo,
) (map[uint][]uint, error) {
- if mdm.VolumePurchasingProgram.Set && mdm.VolumePurchasingProgram.Valid {
- if !license.IsPremium() {
- invalid.Append("mdm.volume_purchasing_program", ErrMissingLicense.Error())
- return nil, nil
- }
-
- teams, err := svc.ds.TeamsSummary(ctx)
- if err != nil {
- return nil, err
- }
- teamsByName := map[string]uint{fleet.TeamNameNoTeam: 0}
- for _, tm := range teams {
- teamsByName[tm.Name] = tm.ID
- }
- tokens, err := svc.ds.ListVPPTokens(ctx)
- if err != nil {
- return nil, err
- }
- tokensByLocation := map[string]*fleet.VPPTokenDB{}
- for _, token := range tokens {
- // The default assignments for all tokens is "no team"
- // (ie: team_id IS NULL), here we reset the assignments
- // for all tokens, those will be re-added below.
- //
- // This ensures any unassignments are properly handled.
- tokensByLocation[token.Location] = token
- token.Teams = nil
- }
+ // Allow clearing VPP assignments in free and premium.
+ if len(volumePurchasingProgramInfo) == 0 {
+ return nil, nil
+ }
- tokensToSave := make(map[uint][]uint, len(mdm.VolumePurchasingProgram.Value))
- for _, vpp := range mdm.VolumePurchasingProgram.Value {
- for _, tmName := range vpp.Teams {
- if _, ok := teamsByName[norm.NFC.String(tmName)]; !ok && tmName != fleet.TeamNameAllTeams {
- invalid.Appendf("mdm.volume_purchasing_program", "team %s doesn't exist", tmName)
- return nil, nil
- }
- }
+ if !license.IsPremium() {
+ invalid.Append("mdm.volume_purchasing_program", ErrMissingLicense.Error())
+ return nil, nil
+ }
- loc := norm.NFC.String(vpp.Location)
- if _, ok := tokensByLocation[loc]; !ok {
- invalid.Appendf("mdm.volume_purchasing_program", "token with location %s doesn't exist", vpp.Location)
+ teams, err := svc.ds.TeamsSummary(ctx)
+ if err != nil {
+ return nil, err
+ }
+ teamsByName := map[string]uint{fleet.TeamNameNoTeam: 0}
+ for _, tm := range teams {
+ teamsByName[tm.Name] = tm.ID
+ }
+ tokens, err := svc.ds.ListVPPTokens(ctx)
+ if err != nil {
+ return nil, err
+ }
+ tokensByLocation := map[string]*fleet.VPPTokenDB{}
+ for _, token := range tokens {
+ // The default assignments for all tokens is "no team"
+ // (ie: team_id IS NULL), here we reset the assignments
+ // for all tokens, those will be re-added below.
+ //
+ // This ensures any unassignments are properly handled.
+ tokensByLocation[token.Location] = token
+ token.Teams = nil
+ }
+
+ tokensToSave := make(map[uint][]uint, len(volumePurchasingProgramInfo))
+ for _, vpp := range volumePurchasingProgramInfo {
+ for _, tmName := range vpp.Teams {
+ if _, ok := teamsByName[norm.NFC.String(tmName)]; !ok && tmName != fleet.TeamNameAllTeams {
+ invalid.Appendf("mdm.volume_purchasing_program", "team %s doesn't exist", tmName)
return nil, nil
}
+ }
- var tokenTeams []uint
- for _, teamName := range vpp.Teams {
- if teamName == fleet.TeamNameAllTeams {
- if len(vpp.Teams) > 1 {
- invalid.Appendf("mdm.volume_purchasing_program", "token cannot belong to %s and other teams", fleet.TeamNameAllTeams)
- return nil, nil
- }
- tokenTeams = []uint{}
- break
+ loc := norm.NFC.String(vpp.Location)
+ if _, ok := tokensByLocation[loc]; !ok {
+ invalid.Appendf("mdm.volume_purchasing_program", "token with location %s doesn't exist", vpp.Location)
+ return nil, nil
+ }
+
+ var tokenTeams []uint
+ for _, teamName := range vpp.Teams {
+ if teamName == fleet.TeamNameAllTeams {
+ if len(vpp.Teams) > 1 {
+ invalid.Appendf("mdm.volume_purchasing_program", "token cannot belong to %s and other teams", fleet.TeamNameAllTeams)
+ return nil, nil
}
- teamID := teamsByName[teamName]
- tokenTeams = append(tokenTeams, teamID)
+ tokenTeams = []uint{}
+ break
}
-
- tok := tokensByLocation[loc]
- tokensToSave[tok.ID] = tokenTeams
+ teamID := teamsByName[teamName]
+ tokenTeams = append(tokenTeams, teamID)
}
- return tokensToSave, nil
+ tok := tokensByLocation[loc]
+ tokensToSave[tok.ID] = tokenTeams
}
- return nil, nil
+ return tokensToSave, nil
}
func validateSSOProviderSettings(incoming, existing fleet.SSOProviderSettings, invalid *fleet.InvalidArgumentError) {
diff --git a/server/service/client.go b/server/service/client.go
index 52223b60e70e..dd975b9567a5 100644
--- a/server/service/client.go
+++ b/server/service/client.go
@@ -423,7 +423,6 @@ func (c *Client) ApplyGroup(
teamsSoftwareInstallers map[string][]fleet.SoftwarePackageResponse,
teamsScripts map[string][]fleet.ScriptResponse,
) (map[string]uint, map[string][]fleet.SoftwarePackageResponse, map[string][]fleet.ScriptResponse, error) {
-
logfn := func(format string, args ...interface{}) {
if logf != nil {
logf(format, args...)
@@ -1428,6 +1427,11 @@ func (c *Client) DoGitOps(
return nil, errors.New("org_settings.mdm config is not a map")
}
+ // Put in default value for volume_purchasing_program to clear the configuration if it's not set.
+ if v, ok := mdmAppConfig["volume_purchasing_program"]; !ok || v == nil {
+ mdmAppConfig["volume_purchasing_program"] = []interface{}{}
+ }
+
// Put in default values for macos_migration
if config.Controls.MacOSMigration != nil {
mdmAppConfig["macos_migration"] = config.Controls.MacOSMigration
@@ -1938,7 +1942,11 @@ func (c *Client) doGitOpsQueries(config *spec.GitOps, logFn func(format string,
}
if !found {
queriesToDelete = append(queriesToDelete, oldQuery.ID)
- fmt.Printf("[-] deleting query %s\n", oldQuery.Name)
+ if !dryRun {
+ fmt.Printf("[-] deleting query %s\n", oldQuery.Name)
+ } else {
+ fmt.Printf("[-] would've deleted query %s\n", oldQuery.Name)
+ }
}
}
if len(queriesToDelete) > 0 {
diff --git a/server/service/devices.go b/server/service/devices.go
index 494d2b329abe..605503ff8171 100644
--- a/server/service/devices.go
+++ b/server/service/devices.go
@@ -498,7 +498,7 @@ func (r getDeviceMDMManualEnrollProfileResponse) hijackRender(ctx context.Contex
// detect short writes (if it fails to send the full content properly)
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(r.Profile)), 10))
// this content type will make macos open the profile with the proper application
- w.Header().Set("Content-Type", "application/x-apple-aspen-config; charset=urf-8")
+ w.Header().Set("Content-Type", "application/x-apple-aspen-config; charset=utf-8")
// prevent detection of content, obey the provided content-type
w.Header().Set("X-Content-Type-Options", "nosniff")
diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go
index aee886c1e071..670d1de5dbe8 100644
--- a/server/service/integration_mdm_dep_test.go
+++ b/server/service/integration_mdm_dep_test.go
@@ -9,6 +9,7 @@ import (
"net/url"
"os"
"path/filepath"
+ "slices"
"strings"
"testing"
"time"
@@ -28,6 +29,7 @@ import (
"github.com/groob/plist"
"github.com/jmoiron/sqlx"
micromdm "github.com/micromdm/micromdm/mdm/mdm"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -735,6 +737,14 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
{SerialNumber: addedSerial, Model: "MacBook Mini", OS: "osx", OpType: "added"},
}
profileAssignmentReqs = []profileAssignmentReq{}
+
+ // Enroll the host to be deleted. It will stay in Fleet after deletion from DEP.
+ mdmDeviceToDelete := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
+ mdmDeviceToDelete.SerialNumber = deletedSerial
+ require.NoError(t, mdmDeviceToDelete.Enroll())
+ // make sure the host gets post enrollment requests
+ checkPostEnrollmentCommands(mdmDeviceToDelete, true)
+
s.runDEPSchedule()
// all hosts should be returned from the hosts endpoint
@@ -822,6 +832,50 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
require.NoError(t, mdmDevice.Enroll())
checkPostEnrollmentCommands(mdmDevice, true)
+ // Delete a pending device from DEP
+ addedModifiedDeletedSerial := uuid.NewString() // no-op
+ deletedAddedSerial := devices[0].SerialNumber // stay as is
+ deletedSerial = devices[1].SerialNumber
+ devices = []godep.Device{
+ {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now()},
+ {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "modified",
+ OpDate: time.Now().Add(time.Second)},
+ {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(2 * time.Second)},
+ {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(3 * time.Second)},
+ {SerialNumber: addedModifiedDeletedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(4 * time.Second)},
+
+ {SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()},
+ {SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now().Add(time.Second)},
+ {SerialNumber: deletedAddedSerial, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now().Add(2 * time.Second)},
+
+ {SerialNumber: deletedSerial, Model: "MacBook Mini", OS: "osx", OpType: "modified", OpDate: time.Now()},
+ {SerialNumber: deletedSerial, Model: "MacBook Mini", OS: "osx", OpType: "modified", OpDate: time.Now().Add(time.Second)},
+ {SerialNumber: deletedSerial, Model: "MacBook Mini", OS: "osx", OpType: "deleted", OpDate: time.Now().Add(2 * time.Second)},
+ }
+ profileAssignmentReqs = []profileAssignmentReq{}
+ s.runDEPSchedule()
+ // all hosts should be returned from the hosts endpoint
+ listHostsRes = listHostsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
+ // all previous devices minus the pending deleted one
+ wantSerials = slices.DeleteFunc(wantSerials, func(s string) bool { return s == deletedSerial })
+ assert.Len(t, listHostsRes.Hosts, len(wantSerials))
+ gotSerials = []string{}
+ for _, device := range listHostsRes.Hosts {
+ gotSerials = append(gotSerials, device.HardwareSerial)
+ }
+ assert.ElementsMatch(t, wantSerials, gotSerials)
+ assert.Len(t, profileAssignmentReqs, 2)
+ gotSerials = []string{}
+ for _, req := range profileAssignmentReqs {
+ assert.Len(t, req.Devices, 1)
+ gotSerials = append(gotSerials, req.Devices...)
+ }
+ assert.ElementsMatch(t, []string{addedModifiedDeletedSerial, deletedAddedSerial}, gotSerials)
+
// delete all MDM info
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm WHERE host_id = ?`, listHostsRes.Hosts[0].ID)
@@ -1150,6 +1204,279 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
require.Empty(t, profileAssignmentReqs)
}
+func (s *integrationMDMTestSuite) TestDEPProfileAssignmentWithMultipleABMs() {
+ t := s.T()
+
+ ctx := context.Background()
+ type hostDEPRow struct {
+ HostID uint `db:"host_id"`
+ ProfileUUID string `db:"profile_uuid"`
+ AssignProfileResponse string `db:"assign_profile_response"`
+ ResponseUpdatedAt time.Time `db:"response_updated_at"`
+ RetryJobID uint `db:"retry_job_id"`
+ }
+ checkHostDEPAssignProfileResponses := func(deviceSerials []string, expectedProfileUUID string,
+ expectedStatus fleet.DEPAssignProfileResponseStatus) map[string]hostDEPRow {
+ bySerial := make(map[string]hostDEPRow, len(deviceSerials))
+ for _, deviceSerial := range deviceSerials {
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ var dest hostDEPRow
+ err := sqlx.GetContext(ctx, q, &dest,
+ "SELECT host_id, assign_profile_response, profile_uuid, response_updated_at, retry_job_id FROM host_dep_assignments WHERE profile_uuid = ? AND host_id = (SELECT id FROM hosts WHERE hardware_serial = ?)",
+ expectedProfileUUID, deviceSerial)
+ require.NoError(t, err)
+ require.Equal(t, string(expectedStatus), dest.AssignProfileResponse)
+ bySerial[deviceSerial] = dest
+ return nil
+ })
+ }
+ return bySerial
+ }
+
+ devices := []godep.Device{
+ {SerialNumber: uuid.New().String(), Model: "MacBook Pro M1", OS: "osx", OpType: "added", OpDate: time.Now()},
+ {SerialNumber: uuid.New().String(), Model: "MacBook Mini M1", OS: "osx", OpType: "added", OpDate: time.Now()},
+ }
+ defaultOrgDevices := []godep.Device{
+ {SerialNumber: uuid.New().String(), Model: "MacBook Mini M2", OS: "osx", OpType: "added", OpDate: time.Now()},
+ {SerialNumber: uuid.New().String(), Model: "MacBook Pro M2", OS: "osx", OpType: "added", OpDate: time.Now()},
+ }
+
+ // set release device manually to true so there is no job enqueued at a later
+ // time to release the device (this is not what this test is about)
+ s.Do("PATCH", "/api/latest/fleet/setup_experience", json.RawMessage(jsonMustMarshal(t, map[string]any{
+ "enable_release_device_manually": true,
+ })), http.StatusNoContent)
+
+ // set up multiple ABM tokens with different org names
+ defaultOrgName := "default_" + t.Name()
+ s.enableABM(defaultOrgName)
+ tmOrgName := t.Name()
+ s.enableABM(tmOrgName)
+
+ // create a new team
+ tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{
+ Name: tmOrgName,
+ Description: "desc",
+ })
+ require.NoError(t, err)
+ // set the default bm assignment for that token to that team
+ acResp := appConfigResponse{}
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
+ "mdm": {
+ "apple_business_manager": [{
+ "organization_name": %q,
+ "macos_team": %q,
+ "ios_team": %q,
+ "ipados_team": %q
+ }]
+ }
+ }`, tmOrgName, tm.Name, tm.Name, tm.Name)), http.StatusOK, &acResp)
+ t.Cleanup(func() {
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "mdm": {
+ "apple_business_manager": []
+ }
+ }`), http.StatusOK, &acResp)
+ })
+ tmProf := `{"profile_name": "Team Profile"}`
+ var createResp createMDMAppleSetupAssistantResponse
+ s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
+ TeamID: &tm.ID,
+ Name: tmOrgName,
+ EnrollmentProfile: json.RawMessage(tmProf),
+ }, http.StatusOK, &createResp)
+ assert.Equal(t, tm.ID, *createResp.TeamID)
+
+ var teamProfileUUIDs []string
+ var defaultProfileUUIDs []string
+ s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ encoder := json.NewEncoder(w)
+ switch r.URL.Path {
+ case "/session":
+ err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
+ require.NoError(t, err)
+ case "/profile":
+ profileUUID := uuid.NewString()
+ teamProfileUUIDs = append(teamProfileUUIDs, profileUUID)
+ err := encoder.Encode(godep.ProfileResponse{ProfileUUID: profileUUID})
+ require.NoError(t, err)
+ case "/server/devices":
+ // This endpoint is used to get an initial list of
+ // devices, return a single device
+ err := encoder.Encode(godep.DeviceResponse{Devices: devices[:1]})
+ require.NoError(t, err)
+ case "/devices/sync":
+ // This endpoint is polled over time to sync devices from
+ // ABM, send a repeated serial and a new one
+ err := encoder.Encode(godep.DeviceResponse{Devices: devices, Cursor: "foo"})
+ require.NoError(t, err)
+ case "/profile/devices":
+ b, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var prof profileAssignmentReq
+ require.NoError(t, json.Unmarshal(b, &prof))
+ var resp godep.ProfileResponse
+ resp.ProfileUUID = prof.ProfileUUID
+ assert.Contains(t, teamProfileUUIDs, resp.ProfileUUID)
+ resp.Devices = make(map[string]string, len(prof.Devices))
+ for _, device := range prof.Devices {
+ resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess)
+ }
+ err = encoder.Encode(resp)
+ require.NoError(t, err)
+ default:
+ _, _ = w.Write([]byte(`{}`))
+ }
+ }))
+
+ s.mockDEPResponse(defaultOrgName, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ encoder := json.NewEncoder(w)
+ switch r.URL.Path {
+ case "/session":
+ err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
+ require.NoError(t, err)
+ case "/profile":
+ profileUUID := uuid.NewString()
+ defaultProfileUUIDs = append(defaultProfileUUIDs, profileUUID)
+ err := encoder.Encode(godep.ProfileResponse{ProfileUUID: profileUUID})
+ require.NoError(t, err)
+ case "/server/devices":
+ // This endpoint is used to get an initial list of
+ // devices, return a single device
+ err := encoder.Encode(godep.DeviceResponse{Devices: defaultOrgDevices[:1]})
+ require.NoError(t, err)
+ case "/devices/sync":
+ // This endpoint is polled over time to sync devices from
+ // ABM, send a repeated serial and a new one
+ err := encoder.Encode(godep.DeviceResponse{Devices: defaultOrgDevices, Cursor: "foo"})
+ require.NoError(t, err)
+ case "/profile/devices":
+ b, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ var prof profileAssignmentReq
+ require.NoError(t, json.Unmarshal(b, &prof))
+ var resp godep.ProfileResponse
+ resp.ProfileUUID = prof.ProfileUUID
+ assert.Contains(t, defaultProfileUUIDs, resp.ProfileUUID)
+ resp.Devices = make(map[string]string, len(prof.Devices))
+ for _, device := range prof.Devices {
+ resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess)
+ }
+ err = encoder.Encode(resp)
+ require.NoError(t, err)
+ default:
+ _, _ = w.Write([]byte(`{}`))
+ }
+ }))
+
+ // query all hosts
+ listHostsRes := listHostsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
+ require.Empty(t, listHostsRes.Hosts)
+
+ // trigger a profile sync
+ s.runDEPSchedule()
+
+ // all hosts should be returned from the hosts endpoint
+ listHostsRes = listHostsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
+ numHosts := len(devices) + len(defaultOrgDevices)
+ assert.Len(t, listHostsRes.Hosts, numHosts)
+ defaultSerials := []string{defaultOrgDevices[0].SerialNumber, defaultOrgDevices[1].SerialNumber}
+ teamSerials := []string{devices[0].SerialNumber, devices[1].SerialNumber}
+ for _, host := range listHostsRes.Hosts {
+ switch {
+ case slices.Contains(defaultSerials, host.HardwareSerial):
+ assert.Nil(t, host.TeamID)
+ case slices.Contains(teamSerials, host.HardwareSerial):
+ assert.NotNil(t, host.TeamID)
+ default:
+ t.Errorf("unexpected host serial %s", host.HardwareSerial)
+ }
+ }
+ require.GreaterOrEqual(t, len(defaultProfileUUIDs), 1)
+ require.Len(t, teamProfileUUIDs, 2)
+ checkHostDEPAssignProfileResponses(defaultSerials, defaultProfileUUIDs[len(defaultProfileUUIDs)-1],
+ fleet.DEPAssignProfileResponseSuccess)
+ checkHostDEPAssignProfileResponses(teamSerials, teamProfileUUIDs[len(teamProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess)
+
+ // Delete the devices in one org, and add them to the other (x2)
+ devices = []godep.Device{
+ {SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "added", OpDate: time.Now()},
+ {SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted", OpDate: time.Now()},
+ {SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(time.Microsecond)},
+ }
+ defaultOrgDevices = []godep.Device{
+ {SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "added", OpDate: time.Now()},
+ {SerialNumber: defaultOrgDevices[1].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted", OpDate: time.Now()},
+ {SerialNumber: devices[1].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "added",
+ OpDate: time.Now().Add(time.Microsecond)},
+ }
+
+ // trigger a profile sync
+ s.runDEPSchedule()
+
+ // all hosts should be returned from the hosts endpoint; the 2 deleted and re-added hosts should switch teams and profiles
+ listHostsRes = listHostsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
+ assert.Len(t, listHostsRes.Hosts, numHosts)
+ defaultSerials = []string{defaultOrgDevices[0].SerialNumber, defaultOrgDevices[2].SerialNumber}
+ teamSerials = []string{devices[0].SerialNumber, devices[2].SerialNumber}
+ for _, host := range listHostsRes.Hosts {
+ switch {
+ case slices.Contains(defaultSerials, host.HardwareSerial):
+ assert.Nil(t, host.TeamID)
+ case slices.Contains(teamSerials, host.HardwareSerial):
+ assert.NotNil(t, host.TeamID)
+ default:
+ t.Errorf("unexpected host serial %s", host.HardwareSerial)
+ }
+ }
+ checkHostDEPAssignProfileResponses(defaultSerials, defaultProfileUUIDs[len(defaultProfileUUIDs)-1],
+ fleet.DEPAssignProfileResponseSuccess)
+ checkHostDEPAssignProfileResponses(teamSerials, teamProfileUUIDs[len(teamProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess)
+
+ // Delete the devices
+ devices = []godep.Device{
+ {SerialNumber: devices[0].SerialNumber, Model: "MacBook Pro M1", OS: "osx", OpType: "modified", OpDate: time.Now()},
+ {SerialNumber: devices[2].SerialNumber, Model: "MacBook Pro M2", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(time.Microsecond)},
+ }
+ defaultOrgDevices = []godep.Device{
+ {SerialNumber: defaultOrgDevices[0].SerialNumber, Model: "MacBook Mini M2", OS: "osx", OpType: "modified", OpDate: time.Now()},
+ {SerialNumber: defaultOrgDevices[2].SerialNumber, Model: "MacBook Mini M1", OS: "osx", OpType: "deleted",
+ OpDate: time.Now().Add(time.Microsecond)},
+ }
+
+ // trigger a profile sync
+ s.runDEPSchedule()
+
+ // 2 hosts should be gone
+ listHostsRes = listHostsResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
+ assert.Len(t, listHostsRes.Hosts, numHosts-2)
+ defaultSerials = []string{defaultOrgDevices[0].SerialNumber}
+ teamSerials = []string{devices[0].SerialNumber}
+ for _, host := range listHostsRes.Hosts {
+ switch {
+ case slices.Contains(defaultSerials, host.HardwareSerial):
+ assert.Nil(t, host.TeamID)
+ case slices.Contains(teamSerials, host.HardwareSerial):
+ assert.NotNil(t, host.TeamID)
+ default:
+ t.Errorf("unexpected host serial %s", host.HardwareSerial)
+ }
+ }
+ checkHostDEPAssignProfileResponses(defaultSerials, defaultProfileUUIDs[len(defaultProfileUUIDs)-1],
+ fleet.DEPAssignProfileResponseSuccess)
+ checkHostDEPAssignProfileResponses(teamSerials, teamProfileUUIDs[len(teamProfileUUIDs)-1], fleet.DEPAssignProfileResponseSuccess)
+
+}
+
func (s *integrationMDMTestSuite) TestDeprecatedDefaultAppleBMTeam() {
t := s.T()
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index 5280bf97aeea..26e96e7d48bb 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -5620,7 +5620,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
"mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "https://example.com" } }
}`), http.StatusOK, &acResp)
- s.enableABM(t.Name())
+ abmToken := s.enableABM(t.Name())
checkMigrationResponses := func(host *fleet.Host, token string) {
getDesktopResp := fleetDesktopResponse{}
@@ -5740,7 +5740,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
- require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial}))
+ require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial}))
cleanAssignmentStatus()
// simulate a "NOT_ACCESSIBLE" JSON profile assignment
@@ -5755,7 +5755,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
- require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial}))
+ require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial}))
cleanAssignmentStatus()
// simulate a "SUCCESS" JSON profile assignment
@@ -5925,7 +5925,7 @@ func (s *integrationMDMTestSuite) TestMDMMigration() {
host := createOrbitEnrolledHost(t, "darwin", "h", s.ds)
createDeviceTokenForHost(t, s.ds, host.ID, token)
checkMigrationResponses(host, token)
- require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, []string{host.HardwareSerial}))
+ require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial}))
tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team-1"})
require.NoError(t, err)
@@ -9573,7 +9573,7 @@ func (s *integrationMDMTestSuite) TestABMAssetManagement() {
s.enableABM(t.Name())
}
-func (s *integrationMDMTestSuite) enableABM(orgName string) {
+func (s *integrationMDMTestSuite) enableABM(orgName string) *fleet.ABMToken {
t := s.T()
var abmResp generateABMKeyPairResponse
s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &abmResp)
@@ -9659,6 +9659,7 @@ func (s *integrationMDMTestSuite) enableABM(orgName string) {
depClient := apple_mdm.NewDEPClient(s.depStorage, s.ds, s.logger)
_, err = depClient.AccountDetail(ctx, orgName)
require.NoError(t, err)
+ return tok
}
func (s *integrationMDMTestSuite) appleCoreCertsSetup() {
@@ -10579,13 +10580,44 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp)
team := newTeamResp.Team
+ // Associate team to the VPP token.
var resPatchVPP patchVPPTokensTeamsResponse
- s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)
- // Reset the token's teams by omitting the token from app config
+ // A PATCH endpoint omitting mdm.volume_purchasing_program should not remove the VPP token association.
acResp := appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
- "mdm": { "volume_purchasing_program": null }
+ "agent_options": {
+ "config": {
+ "options": {
+ "pack_delimiter": "/",
+ "logger_tls_period": 10,
+ "distributed_plugin": "tls",
+ "disable_distributed": false,
+ "logger_tls_endpoint": "/api/osquery/log",
+ "distributed_interval": 10,
+ "distributed_tls_max_attempts": 3
+ }
+ }
+ }
+ }`), http.StatusOK, &acResp)
+
+ // Check that the VPP token is still associated to the team.
+ resp = getVPPTokensResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
+ require.NoError(t, resp.Err)
+ require.Len(t, resp.Tokens, 1)
+ require.Equal(t, orgName, resp.Tokens[0].OrgName)
+ require.Equal(t, location, resp.Tokens[0].Location)
+ require.Equal(t, expTime, resp.Tokens[0].RenewDate)
+ require.Len(t, resp.Tokens[0].Teams, 1)
+ require.Equal(t, team.ID, resp.Tokens[0].Teams[0].ID)
+ require.Equal(t, team.Name, resp.Tokens[0].Teams[0].Name)
+
+ // Reset the token's teams by omitting the token from app config
+ acResp = appConfigResponse{}
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "mdm": { "volume_purchasing_program": [] }
}`), http.StatusOK, &acResp)
resp = getVPPTokensResponse{}
@@ -10597,9 +10629,23 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
require.Equal(t, expTime, resp.Tokens[0].RenewDate)
require.Empty(t, resp.Tokens[0].Teams)
- // Add the team back
- resPatchVPP = patchVPPTokensTeamsResponse{}
- s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)
+ // Add the team back using the PATCH /api/latest/fleet/config endpoint now (what GitOps uses).
+ acResp = appConfigResponse{}
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
+ "mdm": { "volume_purchasing_program": [ {"location": "%s", "teams": [ "%s" ]} ] }
+ }`, location, team.Name)), http.StatusOK, &acResp)
+
+ // Check again that the VPP token is associated to the team.
+ resp = getVPPTokensResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
+ require.NoError(t, resp.Err)
+ require.Len(t, resp.Tokens, 1)
+ require.Equal(t, orgName, resp.Tokens[0].OrgName)
+ require.Equal(t, location, resp.Tokens[0].Location)
+ require.Equal(t, expTime, resp.Tokens[0].RenewDate)
+ require.Len(t, resp.Tokens[0].Teams, 1)
+ require.Equal(t, team.ID, resp.Tokens[0].Teams[0].ID)
+ require.Equal(t, team.Name, resp.Tokens[0].Teams[0].Name)
// Get list of VPP apps from "Apple"
// We're passing team 1 here, but we haven't added any app store apps to that team, so we get
diff --git a/server/worker/macos_setup_assistant.go b/server/worker/macos_setup_assistant.go
index d47391713832..cbc6082ddef9 100644
--- a/server/worker/macos_setup_assistant.go
+++ b/server/worker/macos_setup_assistant.go
@@ -133,7 +133,7 @@ func (m *MacosSetupAssistant) runProfileChanged(ctx context.Context, args macosS
if err != nil {
return ctxerr.Wrap(ctx, err, "assign profile")
}
- if err := m.Datastore.UpdateHostDEPAssignProfileResponses(ctx, resp); err != nil {
+ if err := m.Datastore.UpdateHostDEPAssignProfileResponsesSameABM(ctx, resp); err != nil {
return ctxerr.Wrap(ctx, err, "worker: run profile changed")
}
}
@@ -204,7 +204,7 @@ func (m *MacosSetupAssistant) runProfileDeleted(ctx context.Context, args macosS
if err != nil {
return ctxerr.Wrap(ctx, err, "assign profile")
}
- if err := m.Datastore.UpdateHostDEPAssignProfileResponses(ctx, resp); err != nil {
+ if err := m.Datastore.UpdateHostDEPAssignProfileResponsesSameABM(ctx, resp); err != nil {
return ctxerr.Wrap(ctx, err, "worker: run profile deleted")
}
}
@@ -272,7 +272,7 @@ func (m *MacosSetupAssistant) runHostsTransferred(ctx context.Context, args maco
if err != nil {
return ctxerr.Wrap(ctx, err, "assign profile")
}
- if err := m.Datastore.UpdateHostDEPAssignProfileResponses(ctx, resp); err != nil {
+ if err := m.Datastore.UpdateHostDEPAssignProfileResponsesSameABM(ctx, resp); err != nil {
return ctxerr.Wrap(ctx, err, "worker: run hosts transferred")
}
}
diff --git a/website/api/controllers/save-questionnaire-progress.js b/website/api/controllers/save-questionnaire-progress.js
index 2571eb1fd1b1..2c4182df47e1 100644
--- a/website/api/controllers/save-questionnaire-progress.js
+++ b/website/api/controllers/save-questionnaire-progress.js
@@ -216,6 +216,9 @@ module.exports = {
} catch(err){
sails.log.warn(`When converting a user's (email: ${this.req.me.emailAddress}) getStartedQuestionnaireAnswers to a formatted string to send to the CRM, and error occurred`, err);
}
+ // Prepend the user's reported organization to the questionnaireProgressAsAFormattedString
+ questionnaireProgressAsAFormattedString = `organization-acording-to-fleetdm.com: ${this.req.me.organization}\n` + questionnaireProgressAsAFormattedString;
+
// Create a dictionary of values to send to the CRM for this user.
let contactInformation = {
emailAddress: this.req.me.emailAddress,
diff --git a/website/api/controllers/view-device-management.js b/website/api/controllers/view-device-management.js
index 44b86bd21027..2960b440a2aa 100644
--- a/website/api/controllers/view-device-management.js
+++ b/website/api/controllers/view-device-management.js
@@ -29,7 +29,7 @@ module.exports = {
});
// Specify an order for the testimonials on this page using the last names of quote authors
- let testimonialOrderForThisPage = ['Erik Gomez', 'Kenny Botelho', 'Wes Whetstone', 'Matt Carr', 'Dan Grzelak', 'Nick Fohs'];
+ let testimonialOrderForThisPage = ['Scott MacVicar', 'Erik Gomez', 'Kenny Botelho', 'Wes Whetstone', 'Matt Carr', 'Dan Grzelak', 'Nick Fohs'];
testimonialsForScrollableTweets.sort((a, b)=>{
if(testimonialOrderForThisPage.indexOf(a.quoteAuthorName) === -1){
return 1;
diff --git a/website/api/hooks/custom/index.js b/website/api/hooks/custom/index.js
index 83c057418908..c6c0b388b8ab 100644
--- a/website/api/hooks/custom/index.js
+++ b/website/api/hooks/custom/index.js
@@ -311,6 +311,14 @@ will be disabled and/or hidden in the UI.
await salesforceConnection.login(sails.config.custom.salesforceIntegrationUsername, sails.config.custom.salesforceIntegrationPasskey);
let today = new Date();
let nowOn = today.toISOString().replace('Z', '+0000');
+ let websiteVisitReason;
+ if(req.session.adAttributionString && this.req.session.visitedSiteFromAdAt) {
+ let thirtyMinutesAgoAt = Date.now() - (1000 * 60 * 30);
+ // If this user visited the website from an ad, set the websiteVisitReason to be the adAttributionString stored in their session.
+ if(req.session.visitedSiteFromAdAt > thirtyMinutesAgoAt) {
+ websiteVisitReason = this.req.session.adAttributionString;
+ }
+ }
// Create the new Fleet website page view record.
return await sails.helpers.flow.build(async ()=>{
return await salesforceConnection.sobject('fleet_website_page_views__c')
@@ -318,6 +326,7 @@ will be disabled and/or hidden in the UI.
Contact__c: recordIds.salesforceContactId,// eslint-disable-line camelcase
Page_URL__c: `https://fleetdm.com${req.url}`,// eslint-disable-line camelcase
Visited_on__c: nowOn,// eslint-disable-line camelcase
+ Website_visit_reason__c: websiteVisitReason// eslint-disable-line camelcase
});
}).intercept((err)=>{
return new Error(`Could not create new Fleet website page view record. Error: ${err}`);
diff --git a/website/assets/images/logo-vanta-82x28@2x.png b/website/assets/images/logo-vanta-82x28@2x.png
new file mode 100644
index 000000000000..6ed1374ad113
Binary files /dev/null and b/website/assets/images/logo-vanta-82x28@2x.png differ
diff --git a/website/assets/images/testimonial-author-scott-macvicar-100x100@2x.png b/website/assets/images/testimonial-author-scott-macvicar-100x100@2x.png
new file mode 100644
index 000000000000..9dbdeabb20fb
Binary files /dev/null and b/website/assets/images/testimonial-author-scott-macvicar-100x100@2x.png differ
diff --git a/website/assets/js/pages/handbook/basic-handbook.page.js b/website/assets/js/pages/handbook/basic-handbook.page.js
index 28bd198af910..00f81a41de37 100644
--- a/website/assets/js/pages/handbook/basic-handbook.page.js
+++ b/website/assets/js/pages/handbook/basic-handbook.page.js
@@ -123,6 +123,13 @@ parasails.registerPage('basic-handbook', {
let startValue = parseInt(ol.getAttribute('start'), 10) - 1;
ol.style.counterReset = 'custom-counter ' + startValue;
});
+ // Add links to the responsibilities under the responsibilities heading.
+ if($('#responsibilities')){
+ let responsibilitiesLinksHtml = '\n';
+ $('h3').each((unused, el)=>{ responsibilitiesLinksHtml += ''+_.escape($(el).text())+' \n'; });
+ responsibilitiesLinksHtml+= ' ';
+ $('#responsibilities + p').after(responsibilitiesLinksHtml);
+ }
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
diff --git a/website/assets/styles/pages/start.less b/website/assets/styles/pages/start.less
index 3057b43e75e7..7e42ac554cab 100644
--- a/website/assets/styles/pages/start.less
+++ b/website/assets/styles/pages/start.less
@@ -230,7 +230,7 @@
margin-bottom: 40px;
}
[purpose='card'] {
- width: 252px;
+ width: 256px;
height: 200px;
display: flex;
flex-direction: column;
@@ -238,7 +238,7 @@
align-items: center;
text-decoration: none;
cursor: pointer;
- padding: 24px;
+ padding: 32px;
background: #FFF;
color: @core-fleet-black-75;
border-radius: 12px;
diff --git a/website/config/custom.js b/website/config/custom.js
index 51438306de02..7c191d3f7b04 100644
--- a/website/config/custom.js
+++ b/website/config/custom.js
@@ -267,7 +267,7 @@ module.exports.custom = {
'handbook/README.md': 'mikermcneil', // See https://github.com/fleetdm/fleet/pull/13195
'handbook/company': 'mikermcneil',
'handbook/company/product-groups.md': ['lukeheath', 'sampfluger88','mikermcneil'],
- 'handbook/company/open-positions.yml': ['@sampfluger88','mikermcneil'],
+ 'handbook/company/open-positions.yml': ['sampfluger88','mikermcneil'],
'handbook/digital-experience': ['sampfluger88','mikermcneil'],
'handbook/finance': ['sampfluger88','mikermcneil'],
'handbook/engineering': ['sampfluger88','mikermcneil', 'lukeheath'],
diff --git a/website/scripts/get-bug-and-pr-report.js b/website/scripts/get-bug-and-pr-report.js
index 06791bf10ab5..189517ae5589 100644
--- a/website/scripts/get-bug-and-pr-report.js
+++ b/website/scripts/get-bug-and-pr-report.js
@@ -365,22 +365,22 @@ module.exports = {
let averageDaysContributorPullRequestsAreOpenFor = Math.round(_.sum(daysSinceContributorPullRequestsWereOpened)/daysSinceContributorPullRequestsWereOpened.length);
- // Compute CEO-dependent PR KPIs, which are slightly simpler.
+ // Compute Handbook PR KPIs, which are slightly simpler.
// FUTURE: Refactor this to be less messy.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- let ceoDependentOpenPrs = [];
- ceoDependentOpenPrs = ceoDependentOpenPrs.concat(allPublicOpenPrs.filter((pr) => !pr.draft && _.pluck(pr.labels, 'name').includes('~ceo')));
- ceoDependentOpenPrs = ceoDependentOpenPrs.concat(allNonPublicOpenPrs.filter((pr) => !pr.draft && _.pluck(pr.labels, 'name').includes('~ceo')));
+ let handbookOpenPrs = [];
+ handbookOpenPrs = handbookOpenPrs.concat(allPublicOpenPrs.filter((pr) => !pr.draft && _.pluck(pr.labels, 'name').includes('#handbook')));
+ handbookOpenPrs = handbookOpenPrs.concat(allNonPublicOpenPrs.filter((pr) => !pr.draft && _.pluck(pr.labels, 'name').includes('#handbook')));
- let ceoDependentPrsMergedRecently = [];
- ceoDependentPrsMergedRecently = ceoDependentPrsMergedRecently.concat(publicPrsMergedInThePastThreeWeeks.filter((pr) => !pr.draft && _.pluck(pr.labels, 'name').includes('~ceo')));
- ceoDependentPrsMergedRecently = ceoDependentPrsMergedRecently.concat(nonPublicPrsClosedInThePastThreeWeeks.filter((pr) => !pr.draft && _.pluck(pr.labels, 'name').includes('~ceo')));
+ let handbookPrsMergedRecently = [];
+ handbookPrsMergedRecently = handbookPrsMergedRecently.concat(publicPrsMergedInThePastThreeWeeks.filter((pr) => !pr.draft && _.pluck(pr.labels, 'name').includes('#handbook')));
+ handbookPrsMergedRecently = handbookPrsMergedRecently.concat(nonPublicPrsClosedInThePastThreeWeeks.filter((pr) => !pr.draft && _.pluck(pr.labels, 'name').includes('#handbook')));
- let ceoDependentPrOpenTime = ceoDependentPrsMergedRecently.reduce((avgDaysOpen, pr)=>{
+ let handbookPrOpenTime = handbookPrsMergedRecently.reduce((avgDaysOpen, pr)=>{
let openedAt = new Date(pr.created_at).getTime();
let closedAt = new Date(pr.closed_at).getTime();
let daysOpen = Math.abs(closedAt - openedAt) / ONE_DAY_IN_MILLISECONDS;
- avgDaysOpen = avgDaysOpen + (daysOpen / ceoDependentPrsMergedRecently.length);
+ avgDaysOpen = avgDaysOpen + (daysOpen / handbookPrsMergedRecently.length);
sails.log.verbose('Processing',pr.head.repo.name,':: #'+pr.number,'open '+daysOpen+' days', 'rolling avg now '+avgDaysOpen);
return avgDaysOpen;
}, 0);
@@ -451,15 +451,15 @@ module.exports = {
Number of issues with the "#g-mdm", "bug", and "customer-" labels opened in the past week: ${allBugsCreatedInPastWeekMobileDeviceManagementCustomerImpacting.length}
- Number of issues with the "#g-emdm", "bug", and "~released bug" labels opened in the past week: ${allBugsCreatedInPastWeekMobileDeviceManagementReleased.length}
+ Number of issues with the "#g-mdm", "bug", and "~released bug" labels opened in the past week: ${allBugsCreatedInPastWeekMobileDeviceManagementReleased.length}
Number of issues with the "#g-mdm", "bug", and "~unreleased bug" labels opened in the past week: ${allBugsCreatedInPastWeekMobileDeviceManagementUnreleased.length}
- Pull requests requiring CEO review
+ Handbook Pull requests
---------------------------------------
- Number of open ~ceo pull requests in the fleetdm Github org: ${ceoDependentOpenPrs.length}
+ Number of open #handbook pull requests in the fleetdm Github org: ${handbookOpenPrs.length}
- Average open time (~ceo PRs): ${Math.round(ceoDependentPrOpenTime*100)/100} days.
+ Average open time (#handbook PRs): ${Math.round(handbookPrOpenTime*100)/100} days.
`);
}
diff --git a/website/views/pages/contact.ejs b/website/views/pages/contact.ejs
index f18609cb9701..bbe4f5235e75 100644
--- a/website/views/pages/contact.ejs
+++ b/website/views/pages/contact.ejs
@@ -115,17 +115,17 @@
-
+
- Exciting. This is a team that listens to feedback.
+ We've been using Fleet for a few years at Stripe and we couldn't be happier. The fact that it's also open-source made it easy for us to try it out, customise it to our needs, and seamlessly integrate it into our existing environment.
-
+
-
Erik Gomez
-
Staff Client Platform Engineer
+
Scott MacVicar
+
Head of Developer Infrastructure & Corporate Technology
diff --git a/website/views/pages/entrance/login.ejs b/website/views/pages/entrance/login.ejs
index 4d55f0a28106..ea57e2e00f08 100644
--- a/website/views/pages/entrance/login.ejs
+++ b/website/views/pages/entrance/login.ejs
@@ -33,21 +33,21 @@
<% if(typeof primaryBuyingSituation === 'undefined' || ['mdm'].includes(primaryBuyingSituation)) { %>
-
-
-
- Exciting. This is a team that listens to feedback.
-
-
-
-
-
-
-
Erik Gomez
-
Staff Client Platform Engineer
-
+
+
+
+ We've been using Fleet for a few years at Stripe and we couldn't be happier. The fact that it's also open-source made it easy for us to try it out, customise it to our needs, and seamlessly integrate it into our existing environment.
+
+
+
+
+
+
+
Scott MacVicar
+
Head of Developer Infrastructure & Corporate Technology
+
<% } else if (['eo-it'].includes(primaryBuyingSituation)) { %>
diff --git a/website/views/pages/entrance/signup.ejs b/website/views/pages/entrance/signup.ejs
index 40b71c7c1da4..330d8e8c2913 100644
--- a/website/views/pages/entrance/signup.ejs
+++ b/website/views/pages/entrance/signup.ejs
@@ -64,15 +64,15 @@
- Exciting. This is a team that listens to feedback.
+ We've been using Fleet for a few years at Stripe and we couldn't be happier. The fact that it's also open-source made it easy for us to try it out, customise it to our needs, and seamlessly integrate it into our existing environment.
-
+
-
Erik Gomez
-
Staff Client Platform Engineer
+
Scott MacVicar
+
Head of Developer Infrastructure & Corporate Technology
diff --git a/website/views/pages/integrations.ejs b/website/views/pages/integrations.ejs
index 02c9bb5b8a9d..b40988616af9 100644
--- a/website/views/pages/integrations.ejs
+++ b/website/views/pages/integrations.ejs
@@ -305,6 +305,20 @@
+
+
+
+
+
+
+ Send information about Fleet users and enrolled hosts to Vanta.
+
+
+
Learn more
+
Get started
+
+
+
diff --git a/website/views/pages/start.ejs b/website/views/pages/start.ejs
index 02bb0f7ffd67..6d2412cdb136 100644
--- a/website/views/pages/start.ejs
+++ b/website/views/pages/start.ejs
@@ -703,10 +703,9 @@
Unfortunately, managed cloud hosting is not yet available for growing deployments of less than 700 hosts.