From c7b4127e3313adf7fbb9f06b309e3e01d2b0469a Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 23 Oct 2024 09:24:42 -0400 Subject: [PATCH 01/57] Fleet UI: Tabbing through hdp tabs and global view all host links fixed (#23101) --- frontend/components/ViewAllHostsLink/_styles.scss | 5 +++++ frontend/pages/hosts/details/_styles.scss | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) 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/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; + } + } } } From 3e29f16f5367d190d683dd90ca5d1dda7e264ce9 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 23 Oct 2024 11:15:56 -0300 Subject: [PATCH 02/57] dogfood: Restore VPP token association to teams (#23114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To fix https://github.com/fleetdm/fleet/actions/runs/11468989615/job/31915263035#step:7:174 ``` Error: applying app store apps for team: "🔳🏢 Company-owned iPads": POST /api/latest/fleet/software/app_store_apps/batch received status 422 Unprocessable Entity: could not retrieve vpp token: No available VPP Token ``` https://github.com/fleetdm/fleet/pull/22326 fixed so that GitOps removes associations if they are not set (GitOps mode of operation where stuff that's not set is removed), thus we now need to define it. --- it-and-security/default.yml | 7 +++++++ 1 file changed, 7 insertions(+) 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: "" From 921d8c8afdb6b996f83b69e9fb5d219c4fb74e99 Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Wed, 23 Oct 2024 10:06:03 -0500 Subject: [PATCH 03/57] Check for TUF expirations 2x daily, and warn 4 days in advance rather than 2 (#23039) --- .github/workflows/check-tuf-timestamps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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") From 58ce48dea88a85a1c9bbaba692506933d5b9e801 Mon Sep 17 00:00:00 2001 From: Neil Blazevic Date: Wed, 23 Oct 2024 18:07:20 +0300 Subject: [PATCH 04/57] Update Render deploy pricing (#23113) --- docs/Deploy/deploy-fleet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 30b3a1ea3750eae45da125a3de488f6c98fe742f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:19:45 -0500 Subject: [PATCH 05/57] Bump grunt from 1.0.4 to 1.5.3 in /ee/bulk-operations-dashboard (#21510) --- ee/bulk-operations-dashboard/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From b12f8695a20f65e6912bf97544f55a182fabf46b Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:49:01 -0400 Subject: [PATCH 06/57] Update windows-mdm-setup.md (#23122) Changed instances of Azure AD to Microsoft Entra ID. Did not change URLs because they still seem to work to connect to the service. @noahtalerman has already verbally ok'd this change. --- articles/windows-mdm-setup.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/articles/windows-mdm-setup.md b/articles/windows-mdm-setup.md index 87188e11ee11..624daf96ec6c 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 @@ -48,11 +48,11 @@ 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. @@ -70,7 +70,7 @@ Each end user who automatically enrolls needs a Microsoft Intune license. 6. Find and buy an Intune 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 > Automatic enrollment**. 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**. From c1835c6a01ee51f53fe0039d07c1f08644f94a0a Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 23 Oct 2024 12:49:43 -0300 Subject: [PATCH 07/57] Document `umask` requirement for fleetctl package (#23120) Documentation for #22877. --- articles/enroll-hosts.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/articles/enroll-hosts.md b/articles/enroll-hosts.md index 0dccd003aa3e..7f383099627a 100644 --- a/articles/enroll-hosts.md +++ b/articles/enroll-hosts.md @@ -27,6 +27,8 @@ The `--type` flag is used to specify the fleetd installer type. A `--fleet-url` (Fleet instance URL) and `--enroll-secret` (Fleet enrollment secret) must be specified in order to communicate with Fleet instance. +> `fleetctl` on macOS/Linux requires `umask` to be `022`/`002` and `/tmp` (used during package generation) has to be mounted without `noexec`. + #### Example Generate fleetd on macOS (.pkg) From 8fa5aafa9df608713c15475dec20f8efac273cba Mon Sep 17 00:00:00 2001 From: Rachael Shaw Date: Wed, 23 Oct 2024 10:50:08 -0500 Subject: [PATCH 08/57] Update configuration docs (#22990) Move `license.enforce_host_limit` to contributor docs --- .../fleet-server-configuration.md | 20 ------------------- .../Configuration-for-contributors.md | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 20 deletions(-) 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/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 From 4d6a81c56431fb02ff55446c7c5678b753dcdfb6 Mon Sep 17 00:00:00 2001 From: Mike McNeil Date: Wed, 23 Oct 2024 10:54:31 -0500 Subject: [PATCH 09/57] Update testimonials.yml (#22483) Co-authored-by: Eric --- handbook/company/testimonials.yml | 8 ++++++++ ...imonial-author-scott-macvicar-100x100@2x.png | Bin 0 -> 86779 bytes 2 files changed, 8 insertions(+) create mode 100644 website/assets/images/testimonial-author-scott-macvicar-100x100@2x.png 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/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 0000000000000000000000000000000000000000..9dbdeabb20fb67afbefbb245fe879d212717c037 GIT binary patch literal 86779 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 literal 0 HcmV?d00001 From b5c0e61f7d9766ae10d2edb290a3748c471ff141 Mon Sep 17 00:00:00 2001 From: aysiu Date: Wed, 23 Oct 2024 09:54:41 -0700 Subject: [PATCH 10/57] Enable launch daemon before trying to bootstrap it (#22764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Moves the enabling of the launch daemon to before trying to launch it, so it doesn't fail if the launch daemon is disabled ## Testing Done launch daemon is enabled ``` launchctl print system | grep fleetdm 78074 - com.fleetdm.orbit "com.fleetdm.orbit" => enabled ``` uninstalling FleetDM via https://github.com/fleetdm/fleet/blob/main/orbit/tools/cleanup/cleanup_macos.sh ``` sudo ~/Desktop/removefleet.sh Removing fleet, system will be unenrolled in 15 seconds... Executing detached child process ``` launch daemon is still enabled: ``` launchctl print system | grep fleetdm 78074 - com.fleetdm.orbit "com.fleetdm.orbit" => enabled ``` If I manually disable the launch daemon... ``` sudo launchctl disable system/com.fleetdm.orbit ``` ``` launchctl print system | grep fleetdm 0 78 com.fleetdm.orbit "com.fleetdm.orbit" => disabled ``` ... then the install will fail: ``` sudo installer -pkg ~/Desktop/fleet-osquery-1.18.3.pkg -target / installer: Package name is Fleet osquery installer: Installing at base path / installer: The install failed. (The Installer encountered an error that caused the installation to fail. Contact the software manufacturer for assistance. An error occurred while running scripts from the package “fleet-osquery-1.18.3.pkg”.) ``` Excerpt from `/var/log/install.log`: ``` 2024-10-08 15:00:57-07 HOSTNAME package_script_service[80350]: ./postinstall: Retrying launchctl bootstrap... 2024-10-08 15:00:57-07 HOSTNAME package_script_service[80350]: ./postinstall: Bootstrap failed: 5: Input/output error 2024-10-08 15:00:58-07 HOSTNAME package_script_service[80350]: ./postinstall: Retrying launchctl bootstrap... 2024-10-08 15:00:58-07 HOSTNAME package_script_service[80350]: ./postinstall: Bootstrap failed: 5: Input/output error 2024-10-08 15:00:59-07 HOSTNAME package_script_service[80350]: ./postinstall: Retrying launchctl bootstrap... 2024-10-08 15:00:59-07 HOSTNAME package_script_service[80350]: ./postinstall: Bootstrap failed: 5: Input/output error 2024-10-08 15:01:00-07 HOSTNAME package_script_service[80350]: ./postinstall: Retrying launchctl bootstrap... 2024-10-08 15:01:00-07 HOSTNAME package_script_service[80350]: ./postinstall: Bootstrap failed: 5: Input/output error 2024-10-08 15:01:01-07 HOSTNAME package_script_service[80350]: ./postinstall: Retrying launchctl bootstrap... 2024-10-08 15:01:01-07 HOSTNAME package_script_service[80350]: ./postinstall: Bootstrap failed: 5: Input/output error 2024-10-08 15:01:02-07 HOSTNAME package_script_service[80350]: ./postinstall: Retrying launchctl bootstrap... 2024-10-08 15:01:02-07 HOSTNAME package_script_service[80350]: ./postinstall: Bootstrap failed: 5: Input/output error 2024-10-08 15:01:03-07 HOSTNAME package_script_service[80350]: ./postinstall: Retrying launchctl bootstrap... 2024-10-08 15:01:03-07 HOSTNAME package_script_service[80350]: ./postinstall: Bootstrap failed: 5: Input/output error 2024-10-08 15:01:04-07 HOSTNAME package_script_service[80350]: ./postinstall: Failed to bootstrap system /Library/LaunchDaemons/com.fleetdm.orbit.plist ``` If I then enable the launch daemon... ``` sudo launchctl enable system/com.fleetdm.orbit ``` ``` launchctl print system | grep fleetdm "com.fleetdm.orbit" => enabled ``` ... then the postinstall in the pkg works fine: ``` sudo installer -pkg ~/Desktop/fleet-osquery-1.18.3.pkg -target / installer: Package name is Fleet osquery installer: Installing at base path / installer: The install was successful. ``` So, yeah, the enabling has to be before the launching of the launch daemon. --- orbit/pkg/packaging/macos_templates.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 }} From 010f80d5944359ee74c5d0a952f6084f06cd10c0 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:06:03 -0500 Subject: [PATCH 11/57] Add Scott MacVicar (#23111) image @eashaw, can I get your help with this part? - [ ] Then make Scott's one of the default quotes on the device-management page, and on the contact form and wherever else eric shaw thinks it belongs. --------- Co-authored-by: Eric --- .../api/controllers/view-device-management.js | 2 +- website/views/pages/contact.ejs | 10 +++---- website/views/pages/entrance/login.ejs | 26 +++++++++---------- website/views/pages/entrance/signup.ejs | 8 +++--- 4 files changed, 23 insertions(+), 23 deletions(-) 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/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 @@
-
Uber logo
+
Stripe logo

- 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 + Scott MacVicar
-

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)) { %> -
- an opening quotation mark -

- Exciting. This is a team that listens to feedback. -

-
-
- Erik Gomez -
-
-

Erik Gomez

-

Staff Client Platform Engineer

-
+
+ an opening quotation mark +

+ 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 +
+
+

Scott MacVicar

+

Head of Developer Infrastructure & Corporate Technology

+
<% } else if (['eo-it'].includes(primaryBuyingSituation)) { %>
an opening quotation mark 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 @@
an opening quotation mark

- 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 + Scott MacVicar
-

Erik Gomez

-

Staff Client Platform Engineer

+

Scott MacVicar

+

Head of Developer Infrastructure & Corporate Technology

From c71237daa29b2654f75871a74d5ae33542c7913c Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Wed, 23 Oct 2024 18:15:53 +0100 Subject: [PATCH 12/57] add error message for host having mdm off to host details (#23080) relates to #22041 This adds an error message when refetching a host when mdm is not turned on for it. This was accidentally added to the My Device page, so we are moving it to the Host details page. --- .../hosts/details/DeviceUserPage/helpers.ts | 12 +----------- .../details/HostDetailsPage/HostDetailsPage.tsx | 4 ++-- .../hosts/details/HostDetailsPage/helpers.ts | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 frontend/pages/hosts/details/HostDetailsPage/helpers.ts 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/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 0c56d7c6ca4d..1ce179ae8792 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"; @@ -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}`; +}; From 0c4fb36e2d6b2704131a0c99dbbc9a766a93bd18 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:42:01 -0500 Subject: [PATCH 13/57] Update seamless-mdm-migration.md (#23143) During the CS+QA offsite we review this guide. - Update title to clarify that this is about macOS MDM migration - Add "domain (DNS)" to make the topic approachable for non-technical readers --------- Co-authored-by: Rachael Shaw --- articles/seamless-mdm-migration.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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. From 64ce873e862941d36ebc320906e4b8b703bc75c9 Mon Sep 17 00:00:00 2001 From: Brock Walters <153771548+nonpunctual@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:07:32 -0400 Subject: [PATCH 14/57] Update windows-mdm-setup.md (#23127) Updated Azure strings to Entra ID. --- articles/windows-mdm-setup.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/articles/windows-mdm-setup.md b/articles/windows-mdm-setup.md index 624daf96ec6c..18273f1526c2 100644 --- a/articles/windows-mdm-setup.md +++ b/articles/windows-mdm-setup.md @@ -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. @@ -54,7 +54,7 @@ After you connect Fleet to Microsoft Entra ID, you can customize the Windows set 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,7 +68,7 @@ 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 [Microsoft Entra ID portal](https://portal.azure.com). @@ -80,7 +80,7 @@ Each end user who automatically enrolls needs a Microsoft Intune license. ### Step 2: Connect Fleet to Microsoft Entra ID -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 > 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 From 95221e5abd5e4e02522454800647d24378f3ddde Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:12:21 -0500 Subject: [PATCH 15/57] Measure #handbook PR open time (#23124) Co-authored-by: Eric --- website/scripts/get-bug-and-pr-report.js | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) 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. `); } From 32890b70f1ce04d2bf165beec56b717f0bb03baf Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:23:03 -0500 Subject: [PATCH 16/57] Fix link (#23153) --- handbook/company/communications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From fe9ccd23c62cda3db16ef4ec0bdf30d714a19be5 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Wed, 23 Oct 2024 17:31:37 -0300 Subject: [PATCH 17/57] dogfood: Non-canary workstations and servers to stick to `stable` channels (#23123) With this change: - Canary teams explicitly use `edge` channels. - Non canary teams explicitly use `stable` channels. --- it-and-security/lib/servers.agent-options.yml | 6 ++++++ it-and-security/teams/workstations.yml | 20 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) 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: From de7c7929456fec9ff72ba92f85bfd5dbf2d478ba Mon Sep 17 00:00:00 2001 From: jacobshandling <61553566+jacobshandling@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:09:14 -0700 Subject: [PATCH 18/57] Restore intended flash messages (#23152) Addresses #23139 and #23141 - Remove a premature `return` that prevented subsequent `renderFlash` from firing - Fix order of `push` and subsequent `renderFlash` for correct behavior - Check codebase for additional instances of problematic `push` calls _after_ associated `renderFlash` calls via regex `\renderFlash\([\s\S\n]{0,400}router\.push/` - Misc cleanup ![ezgif-6-c49d5d0432](https://github.com/user-attachments/assets/40a96439-7647-45ec-8847-5b0c85b448aa) - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- .../TeamDetailsWrapper/TeamDetailsWrapper.tsx | 3 +-- .../components/DeleteTeamModal/DeleteTeamModal.tsx | 2 +- .../pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) 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/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 ( Date: Wed, 23 Oct 2024 15:53:37 -0700 Subject: [PATCH 19/57] Add a line to docu 'turn off mdm' is only available on macOS' (#23155) Add line indicating 'turn off mdm' is only available on macOS --------- Co-authored-by: Rachael Shaw --- docs/REST API/rest-api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 6dc53f0289f8..f280d020244d 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 From dbc9653b3081d551ca4079021f66db919b5c98c6 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:00:34 -0500 Subject: [PATCH 20/57] Update "Enroll hosts" guide (#23151) Remove note b/c these are the defaults for macOS and Linux: https://github.com/fleetdm/fleet/pull/23120#discussion_r1813194204 --- articles/enroll-hosts.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/articles/enroll-hosts.md b/articles/enroll-hosts.md index 7f383099627a..0dccd003aa3e 100644 --- a/articles/enroll-hosts.md +++ b/articles/enroll-hosts.md @@ -27,8 +27,6 @@ The `--type` flag is used to specify the fleetd installer type. A `--fleet-url` (Fleet instance URL) and `--enroll-secret` (Fleet enrollment secret) must be specified in order to communicate with Fleet instance. -> `fleetctl` on macOS/Linux requires `umask` to be `022`/`002` and `/tmp` (used during package generation) has to be mounted without `noexec`. - #### Example Generate fleetd on macOS (.pkg) From c948266ee08dad0745f950ef522fe4224621c645 Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Thu, 24 Oct 2024 09:09:52 -0500 Subject: [PATCH 21/57] Fix manual MDM profile download (#23171) #23162 By default, Axios treats responses as if they're JSON, and the content type delivered for mobileconfig files doesn't make Axios switch back to providing the download as a blob. Explicitly telling Axios to give us a blob back fixes the issue where profiles were 50% larger than they should have been...and unparseable. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Manual QA for all new/changed functionality --- frontend/services/entities/mdm.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts index 7a4efca932d7..c851bc7b7f72 100644 --- a/frontend/services/entities/mdm.ts +++ b/frontend/services/entities/mdm.ts @@ -334,7 +334,12 @@ const mdmService = { downloadManualEnrollmentProfile: (token: string) => { 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" + ); }, }; From 8e631bb5f896d31506b763161868d68d723f9197 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:31:33 -0500 Subject: [PATCH 22/57] Fix call out box (#23177) --- handbook/company/leadership.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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". From caf5a27462a6bfba8283a612c80ffb888789f7c1 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:09:55 -0400 Subject: [PATCH 23/57] Catch signals for graceful fleet desktop exit (#23054) --- orbit/changes/21256-fleet-desktop-trap | 1 + orbit/cmd/desktop/desktop.go | 23 +++++++++++++++++++- orbit/pkg/useraction/mdm_migration_darwin.go | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 orbit/changes/21256-fleet-desktop-trap 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/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 { From 4e38e8e5c5458554a2d9107107fb0646b11df4e8 Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Thu, 24 Oct 2024 09:54:24 -0700 Subject: [PATCH 24/57] s/urf-8/utf-8 on manual config profile download (#23169) # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Manual QA for all new/changed functionality --- changes/urf-8 | 1 + docs/REST API/rest-api.md | 2 +- server/service/devices.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changes/urf-8 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/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index f280d020244d..d5b7947dcf54 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -5867,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 ``` 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") From 7d4d87d9813ffe75107f0c6e46294450eec9a625 Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:45:42 -0400 Subject: [PATCH 25/57] Feature request issue template: remove `~feature fest` label (#23185) https://github.com/fleetdm/fleet/pull/23184/files#diff-c99d12c3af50c0c2aca2b9ef7597c02ccfe87678291956ff0b2e83d63978ea38R368 --- .github/ISSUE_TEMPLATE/feature-request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: '' --- From 3bc44fc8b3b465c017d7bed2fb434f5ac5b0f67a Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:54:57 -0500 Subject: [PATCH 26/57] Fix reviewer issue (#23193) --- website/config/custom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/config/custom.js b/website/config/custom.js index 51438306de02..3947acfc9dfa 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'], From 3f89b48ca58f6cc29f32d979a813204c0d232dc4 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Thu, 24 Oct 2024 17:02:11 -0300 Subject: [PATCH 27/57] Add iPadOS to minimum versions in FAQ docs (#23197) Follow up to https://github.com/fleetdm/fleet/pull/23104. --- docs/Get started/FAQ.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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. From dd8986d467770d22911b01b8d17697404038033f Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:30:00 -0400 Subject: [PATCH 28/57] Update Product design rituals (#23205) - We no longer commit to 6 weeks: https://github.com/fleetdm/fleet/pull/22706 - Description for confirm and celebrate is outdated --- handbook/product-design/product-design.rituals.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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" - From 9d69f01c2b1c8388945abcf34e24a50c085a358e Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 24 Oct 2024 16:39:32 -0500 Subject: [PATCH 29/57] Website: Add website visit reason to page view records. (#23154) Closes: #23107 Changes: - Updated the custom hook to send `adAttributionString` to Fleet website page view records if that value was set in a logged-in user's session <30m before they visit a page. --- website/api/hooks/custom/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) 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}`); From c38e53d1c6699a7fe88a580a8d007c08bd531837 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 24 Oct 2024 16:41:43 -0500 Subject: [PATCH 30/57] Website: Add organization to questionnaire responses (#23167) Closes: https://github.com/fleetdm/confidential/issues/8238 Changes: - Updated save-questionnaire-progress to prepend a "org-acording-to-fleetdm.com" response to the questionnaire answers reported to the CRM --- website/api/controllers/save-questionnaire-progress.js | 3 +++ 1 file changed, 3 insertions(+) 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, From 6cbc98cb55e5e27ecbf477808b6735e77c744394 Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:42:19 -0500 Subject: [PATCH 31/57] Update open-positions.yml (#23191) --- handbook/company/open-positions.yml | 53 ----------------------------- 1 file changed, 53 deletions(-) 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. From 0a612a06b4f54a687097eb9a57a8f16243a9f274 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Thu, 24 Oct 2024 16:42:30 -0500 Subject: [PATCH 32/57] Remove pending MDM device from Fleet (#23067) #22331 # Demo of fix # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> --- changes/22331-remove-pending-devices | 1 + server/datastore/mysql/apple_mdm.go | 131 +++++-- server/datastore/mysql/apple_mdm_test.go | 6 +- server/datastore/mysql/hosts.go | 11 +- server/datastore/mysql/hosts_test.go | 4 +- .../20241022140321_AddStatusToHostMDM.go | 39 +++ .../mysql/migrations/tables/migration.go | 25 +- server/datastore/mysql/schema.sql | 7 +- server/fleet/datastore.go | 14 +- server/mdm/apple/apple_mdm.go | 77 ++++- server/mock/datastore_mock.go | 36 +- server/service/integration_mdm_dep_test.go | 327 ++++++++++++++++++ server/service/integration_mdm_test.go | 11 +- server/worker/macos_setup_assistant.go | 6 +- 14 files changed, 622 insertions(+), 73 deletions(-) create mode 100644 changes/22331-remove-pending-devices create mode 100644 server/datastore/mysql/migrations/tables/20241022140321_AddStatusToHostMDM.go 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/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 d669a302bcbd..e5968c54211f 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -790,15 +790,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) @@ -870,6 +862,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 1a5513c5bde8..24a52ef4c8e3 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -7612,7 +7612,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) @@ -7745,7 +7745,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/schema.sql b/server/datastore/mysql/schema.sql index 3395c33f607f..db3384cc8fec 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`) @@ -1085,9 +1088,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=323 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=324 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,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,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,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,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 8da56903196d..415334b6c831 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 d43aa82f2c62..66d60f95368e 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) @@ -2355,12 +2359,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 @@ -5650,18 +5660,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/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 4a35a623f3d2..fd37517c4677 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" ) @@ -745,6 +747,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 @@ -832,6 +842,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) @@ -1160,6 +1214,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 26b19f62a267..2807ef03cce2 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() { 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") } } From d694d0572ff118296982b945dd12a0640c967027 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 24 Oct 2024 17:00:01 -0500 Subject: [PATCH 33/57] Msp dash: follow-up changes to software page after QA (#23210) https://github.com/fleetdm/fleet/issues/21928#issuecomment-2436371970 Changes: - updated the edit-software endpoint to make sure that text values related to software installers (pre-install query, install script, post-install script, uninstall script) are updated for all teams when software is edited, and to delete software on the fleet instance when the installer is replaced and all teams are removed. - updated the ace editor component to emit an input event when text is pasted inside of it. - Updated the error messages in the upload software modal - Fixed an issue where the edit software endpoint would return a 400 response when all teams are removed from a software installer when a new installer package is provided. --- .../api/controllers/software/edit-software.js | 61 +++++++++++++++---- .../js/components/ace-editor.component.js | 2 +- .../assets/js/pages/software/software.page.js | 18 +++--- .../views/pages/software/software.ejs | 2 +- 4 files changed, 60 insertions(+), 23 deletions(-) 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: `
-
{{value}}
+
{{value}}
`, 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/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 From f916871b370c37bc4c66dc9d556fe4f9aed25507 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 24 Oct 2024 17:00:27 -0500 Subject: [PATCH 34/57] Website: Add links to responsibilities headings on department handbook pages. (#23159) Closes: #22995 Changes: - Updated the handbook's page script to add an unordered list of links to headings for responsibilities on department handbook pages. --- website/assets/js/pages/handbook/basic-handbook.page.js | 7 +++++++ 1 file changed, 7 insertions(+) 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); + } }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ From dff82d9e1c1cacca3fe6309f63a340f4378fd36a Mon Sep 17 00:00:00 2001 From: Sam Pfluger <108141731+Sampfluger88@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:05:00 -0500 Subject: [PATCH 35/57] Update custom.js (#23211) --- website/config/custom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/config/custom.js b/website/config/custom.js index 3947acfc9dfa..51438306de02 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'], From 0ddef77b1579a9dc598376a838e1811e428a9975 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 24 Oct 2024 17:23:32 -0500 Subject: [PATCH 36/57] Website: Fix typo in maintainer GitHub username. (#23214) Changes: - Removed an @ symbol from a maintainer's GitHub username in the website's custom configuration --- website/config/custom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'], From 10508e9be9119339a8156309a475253c1864429e Mon Sep 17 00:00:00 2001 From: Noah Talerman <47070608+noahtalerman@users.noreply.github.com> Date: Thu, 24 Oct 2024 18:26:39 -0400 Subject: [PATCH 37/57] Puppet module guide: GitOps users (#23017) Looks like we forgot to update the guide when we shipped this user story: - #15337 --- articles/puppet-module.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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). From 6e84647deae2aa86b814ee635ec3fb875a6bd7e5 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 24 Oct 2024 18:15:47 -0500 Subject: [PATCH 38/57] Website: update "Managed cloud for growing deployments" step of start questionnaire (#23163) Closes: https://github.com/fleetdm/confidential/issues/7956 Changes: - Updated the link presented to users on the "Managed cloud for growing deployments" step of the get started questionnaire --- website/assets/styles/pages/start.less | 4 ++-- website/views/pages/start.ejs | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) 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/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.

From c873866d6db85485d49be6a9484590c430f941d5 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 24 Oct 2024 18:29:11 -0500 Subject: [PATCH 39/57] Website: add Vanta card to /integrations (#23166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: #22514 Changes: - added a card for the Fleet » Vanta integration to the /integrations page. --- website/assets/images/logo-vanta-82x28@2x.png | Bin 0 -> 2856 bytes website/views/pages/integrations.ejs | 14 ++++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 website/assets/images/logo-vanta-82x28@2x.png 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 0000000000000000000000000000000000000000..6ed1374ad113eeffa40d6055dfa8fd414366e4bb GIT binary patch literal 2856 zcmV+@3)l3CP) zbtHxF3D(6ftg$T3c8?`n$daZopYLASpue%t|-ef_!&55Qq$?D`B( zRK|D058C+aF?=P+PlNw-g@hfjLitiHZVwzrf?(K&dxE4N zXM^Jd95!FuoT#AMaGgY`@c%RISG2wl2_?ldxF^W?P2h(evU=PT=jq=mA^)8GdtTP} zAwa`5_TDz!6DA5t{v0X>d+5fr%wJ$m2$hPo6?DPDA_)DLaF3`b{~&*Eb$O%9>2c^l zZ{ja&-!sixV||-aJju^$GTh^y|J_QvNxw%>*S|$g^lO3Ne+d5P+VaM(QffQ*03i<1 zwfX>a<~C(6(jQRzfE{&9`BVob6~GQ~a-#!afb;44=xNrmO>fg{TwZMAT|ZGET6kWw zIERXE1rGlrRPSrGrS4CIfAxS#EOZm5hd}pSzr5H6DW1?sknVhRkN6Ny(GPrUk|njZ zGU$6jt>LL*@C^UA=S9bhM(qbul&tRtD_+0XKkS2nMao>CJ#z!l_UcRW960&O0_EWl zr7O;FG>H*E1zE|@bSF{jWmR8EfHzK$e?cz|_E;Dg5xdv0_F*5yu_9Yd(1rDenBzP!E+W(;NBEm)DDx#6pP>5AXZNj1_gX#?{3~%Xakr1(;9N zflB&2ku!NvfxI>+GAFleMj1=V{M;z>2$}Vvs7ZE&Tfe%n68cS2N|p#_w1v6)64M$QkTINJ1;B7iG8vF zRh2QpZqiE9gC`4x)2WZ?MFAzJ5vrPs&q=hHh z1g0J~!Gf%0t`KVCx1!Inop-K&;HZ>P0N27(TX~&{gd8gI0vHHPuS9-dC*xa+q7U%R zq1D6#Dnk7?jLs8~g`)eSR~<$@U=p8`e5I}3NDYggp*K&N>!>}4T!kbnfjy=R4A6&q zX5;gif*Ao*pDYXdr`8^|TSdoU+JP0AWmd>U=*Zb9WN}LOK9iuv^T-mB&viCD-a&tB z>+JfNiX7NER&3;-upm@eY}q+7=T1KtO?4Iw*+5PPcYfQeUH#GqHu=E}ftJY}y#@8c z4p6IwUEt9!{8kwf%L4jqE94JP6V#aMcuP)yR*n)Cv%~h!m1@C);P|K}nKxv>^ScZF zuR?{461A*hJDvK-gEKQ@it@>#nv%%eSp&M4BI~Y9!40||XkFeU@6G=vFr<|P*GhWa z1btb831a1lA-oxE>N!AzP+2%UJ?;ts>{@j(a+#lmvq zEQ#oWDh}T6g1eTvQ6UL3<-+9rkB@9o8YSPqD-LGzNDGBr0}7%r8gEe<98LP;doT7) zM+;WO2@F5%YvHbB&QeHPj8$KfG{l&F#BoltoBq6Nnb~+M@d5)Uu?bfflPFC^KNZ|P z+UXy_y=5OcvyKt}Si6LBaeC+8tP2D#R9!)BiO< z&+mrJ7g&+bQb@w0;KEC1;%$PNB!6An*c=7JkI5Wg4x|W&%EtA5}G4&Phzk~wS0CakK-$JX#5{yU@0zHf>~^^O{$I+(54OkGQe*TElx z&P=KWht9R7byyVQ9w_8k=HZ>noUM@2tx&;5+kDtKCpjI$Qr6K>-W3(@9)Ov>&(}vC zxCTc>w9JFyK1Uf*=Fe|S=IkMxhzc%P#5u{VU5z2&oFK<9Guq1`mASpTU4x8puf1eMvI6R!HtVtdjC=RxsnQ z{HeHbf|=GqU&fphWp1Pu)XFbnvc1;4(4!R(&s1YDPnmP^!&>Gw?JeXO^AuSSzDo}S z6wJ&kSBGtVU-W%cFv}94^5tgUcNTNWo=r`tU``vVQ02DCO|jUO2Ij+&Ik#dyuM?C~ zNY<`oSq`!xn9-*xW**D1bi#5+hg}N3=*^5$sgRwBuMd7@9ZO4%*$++>DK2jl08?q; zmn8F23aK4iZg3N!3n>bMnHY=0TD?9aEueVmEXzrs;?bJt)unZX*@5$ z8jXC5JWGDncnHkN3R0xyjdxkgYYE>Exsx7R<}_iE{A`{w7f=$t&Q`%I?(tb00OYs5 zabh=JHQp^jzYX!cDhu1GlV#l~u;DI5o1$ga)-FRrR`ZfME#Igj^U?}wRd9jRh&K5vI#ww5 zTHj3K83o#Akn3L1Ik6Qd>r=9*z$@At-4BM(p+uWOILz!Ek>#{lI5y#Mv@)Gon1{?s z8PA4C+f?XF@7d=hM$Xa-sT*S!T-cPE#hm%wQ@sEkc&i7eH-*Y4=rG3$6@1 zEa
+
+
+ Vanta +
+
+

+ Send information about Fleet users and enrolled hosts to Vanta. +

+
+ Learn more + Get started +
+
+
From f16c941d89c18c5191f6a0fb9d503bb9aa49bb76 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Fri, 25 Oct 2024 10:01:18 -0300 Subject: [PATCH 40/57] Fix `PATCH /api/latest/fleet/config` to not clear VPP token/team associations when not set (#23175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #23174 PS: Make sure to render the diff without whitespace changes. ![Screenshot 2024-10-24 at 10 58 47 AM](https://github.com/user-attachments/assets/fb36c995-206c-4a15-8bf2-6b375c5cc565) - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- .../23174-fix-patch-config-vpp-associations | 1 + cmd/fleet/serve_test.go | 13 +- cmd/fleetctl/gitops_test.go | 107 +++++++++++- ee/server/service/vpp.go | 54 +++--- server/service/appconfig.go | 159 +++++++++--------- server/service/client.go | 6 +- server/service/integration_mdm_test.go | 58 ++++++- 7 files changed, 280 insertions(+), 118 deletions(-) create mode 100644 changes/23174-fix-patch-config-vpp-associations 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/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index 5294e302e88d..3fa8e4714029 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -352,10 +352,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 +665,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 +1127,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/gitops_test.go b/cmd/fleetctl/gitops_test.go index 22f34287b0b9..3d0b5eddbece 100644 --- a/cmd/fleetctl/gitops_test.go +++ b/cmd/fleetctl/gitops_test.go @@ -1014,6 +1014,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)} @@ -1027,7 +1071,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 @@ -1098,6 +1143,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 } @@ -1152,19 +1200,50 @@ 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) { return []*fleet.ABMToken{}, 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( ` @@ -1180,6 +1259,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: `, @@ -1189,9 +1273,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: @@ -1202,6 +1283,8 @@ name: ${TEST_TEAM_NAME} team_settings: secrets: [{"secret":"${TEST_SECRET}"}] software: + app_store_apps: + - app_store_id: '1' `, ) require.NoError(t, err) @@ -1237,12 +1320,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) @@ -1257,6 +1346,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/ee/server/service/vpp.go b/ee/server/service/vpp.go index d1f13bd55589..f818188daa8e 100644 --- a/ee/server/service/vpp.go +++ b/ee/server/service/vpp.go @@ -85,6 +85,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 { @@ -128,35 +134,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/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 247ba1b93546..3c168ed26b2c 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -400,7 +400,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...) @@ -1289,6 +1288,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 diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 2807ef03cce2..56bc4394d87e 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -10563,13 +10563,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{} @@ -10581,9 +10612,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 @@ -11679,7 +11724,6 @@ func (s *integrationMDMTestSuite) TestSCEPProxy() { pkiMessage, err = scep.ParsePKIMessage(body, scep.WithCACerts(certs)) require.NoError(t, err) assert.Equal(t, scep.CertRep, pkiMessage.MessageType) - } type noopCertDepot struct{ depot.Depot } From 52e3fc09f93e7ac7f660af0d218adb98b884c49a Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:34:19 -0400 Subject: [PATCH 41/57] Fleet UI: Fix tabbing software view all hosts link, software status (#23144) --- .../SoftwarePackageCard/_styles.scss | 9 ++++++++- .../SoftwareTitles/SoftwareTable/_styles.scss | 14 -------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/_styles.scss index f4c5f6a9e0b7..2de1944fa03e 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwarePackageCard/_styles.scss @@ -62,7 +62,7 @@ } } - &__status-title{ + &__status-title { display: flex; flex-direction: column; align-items: center; @@ -71,6 +71,13 @@ &__status-count { font-weight: normal; + + // When tabbing + &:focus-visible { + overflow: initial; + outline: 2px solid $ui-vibrant-blue-25; + box-shadow: inset 0 0 0 1px $ui-vibrant-blue-10; + } } &__actions-wrapper { diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss index 8e413e481868..d6ae84038c3c 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/_styles.scss @@ -78,20 +78,6 @@ &__data-table-block { .data-table-block { .data-table__table { - // for showing and hiding "view all hosts" link on hover - tr { - .software-link { - opacity: 0; - transition: opacity 250ms; - } - - &:hover { - .software-link { - opacity: 1; - } - } - } - thead { .name__header { width: $col-md; From 3636fe2f79215e9019b827e25af0ffdb8e640a9e Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:35:13 -0400 Subject: [PATCH 42/57] Fleet UI: Fix select query not scrolling (#23208) --- .../modals/SelectQueryModal/_styles.scss | 23 +++++++++++-------- .../components/AddPolicyModal/_styles.scss | 1 + 2 files changed, 15 insertions(+), 9 deletions(-) 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/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss index c134d42992cf..2257d1cac474 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss @@ -1,3 +1,4 @@ +// Similar to SelectQueryModal .add-policy-modal { height: 90%; overflow: hidden; From 62fdfb68c158c4b13a47a96d69c08c7bbb59dd55 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 25 Oct 2024 09:11:43 -0500 Subject: [PATCH 43/57] Ignore teams files for non-premium gitops users. (#23194) #21715 This fix is needed for https://github.com/fleetdm/fleet-gitops CI to run successfully against a non-premium Fleet server. That CI test will cover this change. # Checklist for submitter - [x] Manual QA for all new/changed functionality --- cmd/fleetctl/gitops.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 From 2711cc5d73a048d8a5dbebeaf12b2ee6a1fd10ac Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:17:31 -0400 Subject: [PATCH 44/57] Fleet UI: Ability to Esc out of SQL/YML editors (#23201) --- frontend/components/Editor/Editor.tsx | 15 +++++++++++++++ frontend/components/FleetAce/FleetAce.tsx | 12 ++++++++++++ frontend/components/YamlAce/YamlAce.jsx | 16 +++++++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) 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/FleetAce/FleetAce.tsx b/frontend/components/FleetAce/FleetAce.tsx index b0422d4cde21..09eafd1a3bb6 100644 --- a/frontend/components/FleetAce/FleetAce.tsx +++ b/frontend/components/FleetAce/FleetAce.tsx @@ -202,6 +202,18 @@ const FleetAce = ({ const onLoadHandler = (editor: IAceEditor) => { 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/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} /> ); From 6bc0b5dcd9214c6e3ff94fe657947aeccbdad352 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 25 Oct 2024 09:43:32 -0500 Subject: [PATCH 45/57] Updated OpenTelemetry dependencies to latest. (#23186) #23183 Updated OpenTelemetry libraries as part of research into error logging for orbit. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - [x] Manual QA for all new/changed functionality --- changes/23183-opentelemetry | 3 + go.mod | 54 ++++++++-------- go.sum | 111 +++++++++++++++----------------- server/datastore/mysql/mysql.go | 23 ++++++- 4 files changed, 103 insertions(+), 88 deletions(-) create mode 100644 changes/23183-opentelemetry 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/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/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) } From 2a756088f14e6ca73cc36f56bb68ad2bd1327c01 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:02:52 -0400 Subject: [PATCH 46/57] Fleet UI: Accessibility button actions, tabbing (#22916) --- .../ActionsDropdown/ActionsDropdown.tests.tsx | 92 +++++++ .../ActionsDropdown/ActionsDropdown.tsx | 252 ++++++++++++++++++ .../components/ActionsDropdown/_styles.scss | 6 + frontend/components/ActionsDropdown/index.ts | 1 + .../components/FileUploader/FileUploader.tsx | 22 +- frontend/components/Modal/Modal.tsx | 2 +- .../DropdownCell/DropdownCell.tests.tsx | 33 --- .../DataTable/DropdownCell/DropdownCell.tsx | 37 --- .../DataTable/DropdownCell/_styles.scss | 87 ------ .../DataTable/DropdownCell/index.ts | 1 - .../TeamsDropdown/TeamsDropdown.tsx | 1 + frontend/components/buttons/Button/Button.tsx | 9 +- .../components/buttons/Button/_styles.scss | 7 +- .../buttons/DropdownButton/_styles.scss | 2 +- .../DropdownOptionTooltipWrapper/_styles.scss | 5 +- .../PackQueriesTableConfig.tsx | 10 +- .../components/top_nav/UserMenu/UserMenu.tsx | 2 +- .../components/top_nav/UserMenu/_styles.scss | 16 ++ .../SoftwarePackageCard.tsx | 16 +- .../Integrations/IntegrationsTableConfig.tsx | 10 +- .../AppleBusinessManagerTableConfig.tsx | 4 +- .../components/VppTable/VppTableConfig.tsx | 4 +- .../components/IdpSection/IdpSection.tsx | 1 + .../UsersPage/UsersPageTableConfig.tsx | 10 +- .../TeamManagementPage/TeamTableConfig.tsx | 2 +- .../UsersTable/UsersTableConfig.tsx | 10 +- .../HostStatusWebhookPreviewModal.tsx | 2 +- .../CustomLabelGroupHeading.tsx | 2 +- .../components/FilterPill/_styles.scss | 5 - .../LabelFilterSelect/LabelFilterSelect.tsx | 16 +- .../DeleteHostModal/DeleteHostModal.tsx | 8 +- .../HostActionsDropdown.tests.tsx | 18 +- .../HostActionsDropdown.tsx | 7 +- .../RunScriptModal/ScriptsTableConfig.tsx | 8 +- .../Software/HostSoftwareTableConfig.tsx | 4 +- frontend/styles/var/colors.ts | 5 + frontend/styles/var/mixins.scss | 7 + frontend/styles/var/padding.ts | 19 ++ 38 files changed, 503 insertions(+), 240 deletions(-) create mode 100644 frontend/components/ActionsDropdown/ActionsDropdown.tests.tsx create mode 100644 frontend/components/ActionsDropdown/ActionsDropdown.tsx create mode 100644 frontend/components/ActionsDropdown/_styles.scss create mode 100644 frontend/components/ActionsDropdown/index.ts delete mode 100644 frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tests.tsx delete mode 100644 frontend/components/TableContainer/DataTable/DropdownCell/DropdownCell.tsx delete mode 100644 frontend/components/TableContainer/DataTable/DropdownCell/_styles.scss delete mode 100644 frontend/components/TableContainer/DataTable/DropdownCell/index.ts create mode 100644 frontend/styles/var/padding.ts 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/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} > {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/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; } -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/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/UserManagementPage/components/UsersTable/UsersTableConfig.tsx b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTableConfig.tsx index bb6788b012ae..482b0b491a04 100644 --- a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTableConfig.tsx +++ b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTableConfig.tsx @@ -12,7 +12,7 @@ import { IDropdownOption } from "interfaces/dropdownOption"; import { generateRole, generateTeam, greyCell } from "utilities/helpers"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; import { COLORS } from "styles/var/colors"; -import DropdownCell from "../../../../../components/TableContainer/DataTable/DropdownCell"; +import ActionsDropdown from "../../../../../components/ActionsDropdown"; interface IHeaderProps { column: { @@ -33,7 +33,7 @@ interface ICellProps extends IRowProps { }; } -interface IDropdownCellProps extends IRowProps { +interface IActionsDropdownProps extends IRowProps { cell: { value: IDropdownOption[]; }; @@ -45,7 +45,7 @@ interface IDataColumn { accessor: string; Cell: | ((props: ICellProps) => 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 = ({ />
-
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