diff --git a/.github/ISSUE_TEMPLATE/story.md b/.github/ISSUE_TEMPLATE/story.md index 8f47cd1c8411..e9ff1a48d436 100644 --- a/.github/ISSUE_TEMPLATE/story.md +++ b/.github/ISSUE_TEMPLATE/story.md @@ -39,7 +39,7 @@ What else should contributors [keep in mind](https://fleetdm.com/handbook/compan ## Changes ### Product -- [ ] UI changes: TODO +- [ ] UI changes: TODO - [ ] CLI (fleetctl) usage changes: TODO - [ ] YAML changes: TODO - [ ] REST API changes: TODO diff --git a/.github/scripts/dogfood-policy-updater-latest-macos.sh b/.github/scripts/dogfood-policy-updater-latest-macos.sh index 6cc8f729ab57..cb3366e6b966 100644 --- a/.github/scripts/dogfood-policy-updater-latest-macos.sh +++ b/.github/scripts/dogfood-policy-updater-latest-macos.sh @@ -103,6 +103,33 @@ if [ "$policy_version_number" != "$latest_macos_version" ]; then fi echo "Pull request created successfully." + + # Extract the pull request number from the response + pr_number=$(echo "$pr_response" | jq -r '.number') + if [ -z "$pr_number" ] || [ "$pr_number" == "null" ]; then + echo "Error: Failed to retrieve pull request number." + exit 1 + fi + + echo "Adding reviewers to PR #$pr_number..." + + # Prepare the reviewers data payload + reviewers_data=$(jq -n --arg r1 "harrisonravazzolo" '{reviewers: [$r1]}') + + # Request reviewers for the pull request + review_response=$(curl -s -X POST \ + -H "Authorization: token $DOGFOOD_AUTOMATION_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + -d "$reviewers_data" \ + "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/pulls/$pr_number/requested_reviewers") + + + if echo "$review_response" | grep -q "errors"; then + echo "Error: Failed to add reviewers. Response: $review_response" + exit 1 + fi + + echo "Reviewers added successfully." else echo "No updates needed; the version is the same." fi diff --git a/.github/workflows/generate-desktop-targets.yml b/.github/workflows/generate-desktop-targets.yml index e9e17de8a9e1..b561d38f9e07 100644 --- a/.github/workflows/generate-desktop-targets.yml +++ b/.github/workflows/generate-desktop-targets.yml @@ -19,7 +19,7 @@ defaults: shell: bash env: - FLEET_DESKTOP_VERSION: 1.39.0 + FLEET_DESKTOP_VERSION: 1.39.1 permissions: contents: write diff --git a/articles/articles/preventing-mistakes-with-gitops.md b/articles/articles/preventing-mistakes-with-gitops.md new file mode 100644 index 000000000000..34d6e5c0c808 --- /dev/null +++ b/articles/articles/preventing-mistakes-with-gitops.md @@ -0,0 +1,94 @@ +## Introduction + +All SysAdmins have been there. It’s Friday afternoon - you make a few wrong clicks in your MDM. All of a sudden, devices that were in a specific team granting users access to your internal network have a configuration profile revoked. Just as you’re ready to sign off for the weekend you start getting Slack messages and realize your mistake. 😳 + +It’s a tough lesson to learn. + +But, what if there was a way, through GitOps and change management best practices, you could avoid all this? A modern change management methodology typically reserved for developers has now arrived for your IT team. πŸ›¬ + +## What is β€œGitOps”? + +GitOps is an operational framework that takes best practices used for application development such as version control, collaboration, compliance, and continuous integration / development, and applies them to device management automation. Key attributes of this system: + +- Configurations are declaritively defined in a repo (such as GitHub or GitLab) +- Configurations represent a single source of truth for system state +- Automated synchronization between git and live infrastructure +- Continuous reconciliation to maintain system state +- Immutable configuration is managed through pull requests and code reviews + +The ultimate goals of this approach? Improve reliability, reduce errors, and enable consistent, auditable management of your device infrastructure. + +## Getting Started + +Fleet publishes a starter template that we recommend checking out (available for both GitHub and GitLab.) + +> In this article we will be using GitHub but the general principles are the same. + +Clone the starter repo: `https://github.com/fleetdm/fleet-gitops` and create your own repo to which you will push code. + +> In a production environment, it is best to protect the `main` branch and only allow merging after a code review is conducted. It can be modified if needed, but, by default the apply action will run whenever code is committed to `main`. + +An important benefit of GitOps is the ability to store all your environment secrets in GitHub - encrypted and protected from view. With the correct configuration, this prevents tampering and leaks. + +Add the `FLEET_URL` and `FLEET_API_TOKEN` secrets to your new repository's secrets. If you’re working out of the template, also add `FLEET_GLOBAL_ENROLL_SECRET`, `FLEET_WORKSTATIONS_ENROLL_SECRET` and `FLEET_WORKSTATIONS_CANARY_ENROLL_SECRET`. + +This can be adjusted depending on how you want to leverage Teams and team names. + +## A Typical GitOps Workflow + +We will start with a traditional workflow to demonstrate the process used to commit changes to your Fleet instance. In this example we are adding a passcode policy for Macs by setting the minimum length to 12 characters. + +> For all examples in this article we will be using the GitHub Desktop app to do commits. Using `git` in the terminal will of course also work. Use whatever you’re most comfortable with. + +![gif-1](../website/assets/images/articles/preventing-mistakes-1-1423x771@2x.gif) + +Here, after making changes to the `passcode.json` file, it has been added to the Team we are configuring under the `macos_settings` section. + +![gif-2](../website/assets/images/articles/preventing-mistakes-2-960x540@2x.gif) + +GitHub Desktop will automatically pick up changes. You can review each file and make commit comments. If all looks good, push your changes to the working branch. + +![gif-3](../website/assets/images/articles/preventing-mistakes-3-1423x771@2x.gif) + +We create a PR to bring this change into the `main` production branch. In this example, branch protections are off so I can merge right to `main` but further on in the article this will change. + +## GitOps: The way it was meant to be + +Another benefit of a GitOps approach is the ability for members of a team to review changes before they are applied in production. This encourages collaboration while ensuring all modifications to state are following best practices and compliance. In addition, if something breaks (which is inevitable) you have a β€˜snapshot’ or point in time with a known working state to which you can easily roll back. + +![gif-4](../website/assets/images/articles/preventing-mistakes-4-960x540@2x.gif) + +The newest version of macOS is released and an engineer on your team wants to push a change to require an update of all hosts in the Workstations team. The IT engineer creates a branch to work from and makes the necessary changes, including setting a new target version and deadline. + +``` +macos_updates: + deadline: "2025-02-15" + minimum_version: "15.4.1" +``` + +Merging is blocked until a member of the team reviews and approves the changes. + +![gif-5](../website/assets/images/articles/preventing-mistakes-5-960x540@2x.gif) + +Our IT manager is listed as the approver for these changes. The approver is notified of a pending PR for review. Is there a problem with some of the changes? Our engineer accidentally put in a version string that is not yet available. This will cause issues for our users when they try to update. The fix? Tag the engineer with some feedback and request changes to be made and re-committed. + +![Pr Approval](../website/assets/images/articles/pr-approval-921x475@2x.jpg) + +After our engineer has updated code from the review, the approver can do a final review, approve and let the engineer merge this branch into `main` to trigger the apply workflow. This will push the changes into the production environment. ✨ + +![Pr Approval](../website/assets/images/articles/pr-approval-2-933x483@2x.jpg) + +## Conclusion + +By adopting GitOps for device management, your team's work becomes observable, reversible and repeatable while automating your device configurations. Instead of making changes manually and risking unintended consequences, you gain a reliable, auditable workflow where every modification is reviewed, approved, and tracked. + +This approach reduces human error and fosters teamwork. Whether you're enforcing security policies, managing OS updates, or deploying configuration changes, GitOps ensures consistency and control helping you avoid those last-minute Friday afternoon mishaps. πŸ˜₯ + +Want to know more about Fleet's comprehensive MDM platform in code? Visit fleetdm.com and use the 'Talk to an engineer' [link](https://fleetdm.com/contact). + + + + + + + diff --git a/articles/fleet-software-attestation.md b/articles/fleet-software-attestation.md index 13300251d724..31c72ef9814a 100644 --- a/articles/fleet-software-attestation.md +++ b/articles/fleet-software-attestation.md @@ -1,14 +1,34 @@ # Fleet software attestation -As of version 4.63.0 Fleet added [SLSA attestations](https://slsa.dev/) to our released binaries and container images. This includes the `fleet` and `fleetctl` server software, the fleetd (Orbit, osquery, and Fleet Desktop) agent for hosts. +As of version 4.63.0 Fleet added [SLSA attestations](https://slsa.dev/) to our released binaries and container images. This includes the Fleet server, [fleetctl](https://fleetdm.com/docs/get-started/anatomy#fleetctl) command-line tool (CLI), and Fleet's agent (specifically the [Orbit](https://fleetdm.com/docs/get-started/anatomy#fleetd) component). ## What is software attestation? A software attestation is a cryptographically-signed statement provided by a software creator that certifies the build process and provenance of one or more software _artifacts_ (which might be files, container images, or other outputs). In other words, it's a promise to our users that the software we're providing was built by us, using a process that they can trust and verify. We utilize the SLSA framework for attestations which you can read more about [here](https://slsa.dev/). After each release, attestations are added to https://github.com/fleetdm/fleet/attestations. -## Verifying our release artifacts +## Verifying a release -Any product of a Fleet release can be _verified_ to prove that it was indeed created by Fleet, using the `gh` command line tool from Github. See the [`gh attestation verify`](https://cli.github.com/manual/gh_attestation_verify) docs for more info. +Any Fleet release can be _verified_ to prove that it was indeed created by Fleet, using the `gh` command line tool from Github. See the [`gh attestation verify`](https://cli.github.com/manual/gh_attestation_verify) docs for more info. + +After downloading the [Fleet binary](https://github.com/fleetdm/fleet/releases), here's how to verify: + +``` +gh attestation verify --owner fleetdm /path/to/fleet +``` + +Verify the [fleetctl binary](https://github.com/fleetdm/fleet/releases) (CLI): + +``` +gh attestation verify --owner fleetdm fleetdm /path/to/fleetctl +``` + +After, installing Fleet's agent (fleetd) on a macOS host, run this command on the host to verify: + +``` +gh attestation verify --owner fleetdm /usr/local/bin/orbit +``` + +TODO: Filepath for Windows and Linux diff --git a/articles/how-to-configure-logging-destinations.md b/articles/how-to-configure-logging-destinations.md index a64b6b437cb2..cd626690af74 100644 --- a/articles/how-to-configure-logging-destinations.md +++ b/articles/how-to-configure-logging-destinations.md @@ -78,13 +78,7 @@ Sumo Logic supports data ingestion via HTTP, making it a reliable choice for log #### For Splunk -Splunk is a powerful platform for searching, monitoring, and analyzing machine-generated big data. Here’s how to integrate it with Firehose: - - - -1. **Set up Firehose**: Use the AWS guide to configure your Firehose delivery stream for Splunk as a destination. The process involves specifying the Splunk endpoint and authentication details. Detailed instructions are available in the [AWS Firehose documentation](https://docs.aws.amazon.com/firehose/latest/dev/create-destination.html?icmpid=docs_console_unmapped#create-destination-splunk). -2. **Configure Splunk**: Follow the [Splunk documentation](https://docs.splunk.com/Documentation/AddOns/released/Firehose/RequestFirehose) to ensure Splunk is set to receive data from Firehose. This step involves setting up the necessary inputs and configuring Splunk to handle incoming data. -3. **Firehose to Splunk configuration**: Finalize the setup by configuring Firehose to send data to Splunk, following the guidelines in the [Splunk documentation](https://docs.splunk.com/Documentation/AddOns/released/Firehose/ConfigureFirehose). +Splunk is a powerful platform for searching, monitoring, and analyzing machine-generated big data. Learn how to connect Fleet to Splunk [here](https://fleetdm.com/guides/log-destinations#splunk). ### Conclusion diff --git a/articles/install-fleet-maintained-apps-on-macos-hosts.md b/articles/install-fleet-maintained-apps-on-macos-hosts.md index 511f9a125f63..87d77bb75e53 100644 --- a/articles/install-fleet-maintained-apps-on-macos-hosts.md +++ b/articles/install-fleet-maintained-apps-on-macos-hosts.md @@ -22,7 +22,7 @@ Fleet maintains these [celebrity apps](https://github.com/fleetdm/fleet/blob/mai - Post-install script - Uninstall scripts -These scripts are auto-generated based on the app's Homebrew Cask formula, but you can modify them. Modifying these scripts allows you to tailor the app installation process to your organization's needs, such as automating additional setup tasks or custom configurations post-installation. +If you find that a script doesn't work as expected, please file a [bug](https://github.com/fleetdm/fleet/issues/new?template=bug-report.md). When scripts are fixed, after upgrading Fleet, they are automatically updated for you unless you edited any of the scripts. ## Install the app diff --git a/articles/lock-wipe-hosts.md b/articles/lock-wipe-hosts.md index a3faa8dab13a..fe5f3143ab50 100644 --- a/articles/lock-wipe-hosts.md +++ b/articles/lock-wipe-hosts.md @@ -24,6 +24,8 @@ Currently, there's no **Lock** button for iOS and iPadOS. If an iOS or iPadOS ho 3. Click the **Actions** dropdown, then click **Wipe**. 4. Confirm that you want to wipe the device in the dialog. The host will now be marked with a "Wipe pending" badge. Once the wipe command is acknowledged by the host, the badge will update to "Wiped". +> When wiping and re-installing the operating system (OS) for a host, also delete it from Fleet before you re-enroll it. If you re-enroll without deleting, a new disk encryption key won’t be escrowed. + ## Unlock a host 1. Navigate to the **Hosts** page by clicking the "Hosts" tab in the main navigation header. Find the device you want to unlock. You can search by name, hostname, UUID, serial number, or private IP address in the search box in the upper right corner. diff --git a/articles/log-destinations.md b/articles/log-destinations.md index ddec0c9b0c61..c92eff5234c5 100644 --- a/articles/log-destinations.md +++ b/articles/log-destinations.md @@ -53,13 +53,38 @@ Snowflake provides instructions on setting up the destination tables and IAM rol ## Splunk -To send logs to Splunk, you must first configure Fleet to send logs to [Amazon Kinesis Data Firehose (Firehose)](#amazon-kinesis-data-firehose). This is because you'll enable Firehose to forward logs directly to Splunk. - -With Fleet configured to send logs to Firehose, you then want to load the data from Firehose into Splunk. AWS provides instructions on how to enable Firehose to forward directly to Splunk [here in the AWS documentation](https://docs.aws.amazon.com/firehose/latest/dev/create-destination.html#create-destination-splunk). - -If you're using Fleet's [terraform reference architecture](https://github.com/fleetdm/fleet/blob/main/infrastructure/dogfood/terraform/aws), you want to replace the S3 destination with a Splunk destination. Hashicorp provides instructions on how to send Firehose data to Splunk [here in the Terraform documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_firehose_delivery_stream#splunk-destination). - -Splunk provides instructions on how to prepare the Splunk platform for Firehose data [here in the Splunk documentation](https://docs.splunk.com/Documentation/AddOns/latest/Firehose/ConfigureFirehose). +How to send logs to Splunk: + +1. Follow [Splunk's instructions](https://docs.splunk.com/Documentation/AddOns/latest/Firehose/ConfigureFirehose) to prepare the Splunk for Firehose data. + +2. Follow these [AWS instructions](https://docs.aws.amazon.com/firehose/latest/dev/create-destination.html#create-destination-splunk) on how to enable Firehose to forward directly to Splunk. + +3. In your [`main.tf` file](https://github.com/fleetdm/fleet-terraform/blob/main/addons/logging-destination-firehose/main.tf), replace your S3 destination (`aws_kinesis_firehose_delivery_stream`) with a Splunk destination: + +```hcl +resource "aws_kinesis_firehose_delivery_stream" "test_stream" { + name = "terraform-kinesis-firehose-test-stream" + destination = "splunk" + + splunk_configuration { + hec_endpoint = "https://http-inputs-mydomain.splunkcloud.com:443" + hec_token = "51D4DA16-C61B-4F5F-8EC7-ED4301342A4A" + hec_acknowledgment_timeout = 600 + hec_endpoint_type = "Event" + s3_backup_mode = "FailedEventsOnly" + + s3_configuration { + role_arn = aws_iam_role.firehose.arn + bucket_arn = aws_s3_bucket.bucket.arn + buffering_size = 10 + buffering_interval = 400 + compression_format = "GZIP" + } + } +} +``` + +For the latest configuration go to HashiCorp's Terraform docs [here](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_firehose_delivery_stream#splunk-destination). ## Amazon Kinesis Data Streams diff --git a/articles/vulnerability-processing.md b/articles/vulnerability-processing.md index 114a7c345866..7a15b1ac44ea 100644 --- a/articles/vulnerability-processing.md +++ b/articles/vulnerability-processing.md @@ -25,6 +25,8 @@ Currently, only software names with all ASCII characters are supported. Vulnerab For Ubuntu Linux, kernel vulnerabilities with known variants (ie. `-generic`) are detected using OVAL. Custom kernels (unknown variants) are detected using NVD. +If you find that Fleet is incorrectly marking software as vulnerable (false positive) or missing a vulnerability (false negative), please file a [bug](https://github.com/fleetdm/fleet/issues/new?template=bug-report.md). When false positives are fixed, it may take an hour for the false positive to dissapear after upgrading Fleet. + ## Sources Fleet combines multiple sources to get accurate and up-to-date CVE information: diff --git a/changes/24222-update-versions b/changes/24222-update-versions new file mode 100644 index 000000000000..449761fe6602 --- /dev/null +++ b/changes/24222-update-versions @@ -0,0 +1,2 @@ +- Adds a daily job that keeps the App Store app version displayed in Fleet in sync with the actual + latest version. \ No newline at end of file diff --git a/changes/26177-max-conc b/changes/26177-max-conc new file mode 100644 index 000000000000..1cab5f65b05b --- /dev/null +++ b/changes/26177-max-conc @@ -0,0 +1,2 @@ +- Prevented an invalid `FLEET_VULNERABILITIES_MAX_CONCURRENCY` value from causing deadlocks during + vulnerability processing. \ No newline at end of file diff --git a/changes/26178-fix-2 b/changes/26178-fix-2 new file mode 100644 index 000000000000..ef0c2716d8ec --- /dev/null +++ b/changes/26178-fix-2 @@ -0,0 +1,2 @@ +- Fixed an issue with increased resource usage during vulnerabilities processing by adding a + database index. \ No newline at end of file diff --git a/changes/26283-software-stuck-pending b/changes/26283-software-stuck-pending new file mode 100644 index 000000000000..030ea3a8e10e --- /dev/null +++ b/changes/26283-software-stuck-pending @@ -0,0 +1 @@ +- Fixed a bug where new fleetd could not install software from old fleet server diff --git a/changes/issue-25656-change-host-modal-copy b/changes/issue-25656-change-host-modal-copy new file mode 100644 index 000000000000..8f98de606cec --- /dev/null +++ b/changes/issue-25656-change-host-modal-copy @@ -0,0 +1,2 @@ +- changed the copy for the delete and transfer host modal to be more clear about the disk encryption + key behavior diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index be7a4ab7744e..5b106e1cac59 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -21,6 +21,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" + "github.com/fleetdm/fleet/v4/server/mdm/apple/vpp" "github.com/fleetdm/fleet/v4/server/mdm/assets" "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep" @@ -102,17 +103,33 @@ func cronVulnerabilities( return fmt.Errorf("scanning vulnerabilities: %w", err) } - start := time.Now() - level.Info(logger).Log("msg", "updating vulnerability host counts") - if err := ds.UpdateVulnerabilityHostCounts(ctx, config.MaxConcurrency); err != nil { - return fmt.Errorf("updating vulnerability host counts: %w", err) + if err := updateVulnHostCounts(ctx, ds, logger, config.MaxConcurrency); err != nil { + return err } - level.Info(logger).Log("msg", "vulnerability host counts updated", "took", time.Since(start).Seconds()) + } return nil } +func updateVulnHostCounts(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, maxConcurrency int) error { + // Prevent invalid values for max concurrency + if maxConcurrency <= 0 { + level.Info(logger).Log("msg", "invalid maxConcurrency value provided, setting value to 1", "providedValue", maxConcurrency) + maxConcurrency = 1 + } + + start := time.Now() + level.Info(logger).Log("msg", "updating vulnerability host counts") + + if err := ds.UpdateVulnerabilityHostCounts(ctx, maxConcurrency); err != nil { + return fmt.Errorf("updating vulnerability host counts: %w", err) + } + level.Info(logger).Log("msg", "vulnerability host counts updated", "took", time.Since(start).Seconds()) + + return nil +} + func scanVulnerabilities( ctx context.Context, ds fleet.Datastore, @@ -1478,3 +1495,26 @@ func newMaintainedAppSchedule( return s, nil } + +func newRefreshVPPAppVersionsSchedule( + ctx context.Context, + instanceID string, + ds fleet.Datastore, + logger kitlog.Logger, +) (*schedule.Schedule, error) { + const ( + name = string(fleet.CronRefreshVPPAppVersions) + defaultInterval = 1 * time.Hour + ) + + logger = kitlog.With(logger, "cron", name) + s := schedule.New( + ctx, name, instanceID, defaultInterval, ds, ds, + schedule.WithLogger(logger), + schedule.WithJob("refresh_vpp_app_version", func(ctx context.Context) error { + return vpp.RefreshVersions(ctx, ds) + }), + ) + + return s, nil +} diff --git a/cmd/fleet/prepare.go b/cmd/fleet/prepare.go index 5ef841686525..6a487dafaa9a 100644 --- a/cmd/fleet/prepare.go +++ b/cmd/fleet/prepare.go @@ -60,27 +60,11 @@ To setup Fleet infrastructure, use one of the available commands. return case fleet.SomeMigrationsCompleted: if !noPrompt { - fmt.Printf("################################################################################\n"+ - "# WARNING:\n"+ - "# This will perform Fleet database migrations. Please back up your data before\n"+ - "# continuing.\n"+ - "#\n"+ - "# Missing migrations: tables=%v, data=%v.\n"+ - "#\n"+ - "# Press Enter to continue, or Control-c to exit.\n"+ - "################################################################################\n", - status.MissingTable, status.MissingData) + printMissingMigrationsPrompt(status.MissingTable, status.MissingData) bufio.NewScanner(os.Stdin).Scan() } case fleet.UnknownMigrations: - fmt.Printf("################################################################################\n"+ - "# WARNING:\n"+ - "# Your Fleet database has unrecognized migrations. This could happen when\n"+ - "# running an older version of Fleet on a newer migrated database.\n"+ - "#\n"+ - "# Unknown migrations: tables=%v, data=%v.\n"+ - "################################################################################\n", - status.UnknownTable, status.UnknownData) + printUnknownMigrationsMessage(status.UnknownTable, status.UnknownData) if dev { os.Exit(1) } @@ -104,3 +88,41 @@ To setup Fleet infrastructure, use one of the available commands. prepareCmd.AddCommand(dbCmd) return prepareCmd } + +func printUnknownMigrationsMessage(tables []int64, data []int64) { + fmt.Printf("################################################################################\n"+ + "# WARNING:\n"+ + "# Your Fleet database has unrecognized migrations. This could happen when\n"+ + "# running an older version of Fleet on a newer migrated database.\n"+ + "#\n"+ + "# Unknown migrations: %s.\n"+ + "################################################################################\n", + tablesAndDataToString(tables, data)) +} + +func printMissingMigrationsPrompt(tables []int64, data []int64) { + fmt.Printf("################################################################################\n"+ + "# WARNING:\n"+ + "# This will perform Fleet database migrations. Please back up your data before\n"+ + "# continuing.\n"+ + "#\n"+ + "# Missing migrations: %s.\n"+ + "#\n"+ + "# Press Enter to continue, or Control-c to exit.\n"+ + "################################################################################\n", + tablesAndDataToString(tables, data)) +} + +func tablesAndDataToString(tables, data []int64) string { + switch { + case len(tables) > 0 && len(data) == 0: + // Most common case + return fmt.Sprintf("tables=%v", tables) + case len(tables) == 0 && len(data) == 0: + return "unknown" + case len(tables) == 0 && len(data) > 0: + return fmt.Sprintf("data=%v", data) + default: + return fmt.Sprintf("tables=%v, data=%v", tables, data) + } +} diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index ad82cb364730..c5b3898df8e3 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -43,6 +43,7 @@ import ( "github.com/fleetdm/fleet/v4/server/live_query" "github.com/fleetdm/fleet/v4/server/logging" "github.com/fleetdm/fleet/v4/server/mail" + android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/cryptoutil" microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" @@ -52,6 +53,7 @@ import ( "github.com/fleetdm/fleet/v4/server/pubsub" "github.com/fleetdm/fleet/v4/server/service" "github.com/fleetdm/fleet/v4/server/service/async" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/service/redis_key_value" "github.com/fleetdm/fleet/v4/server/service/redis_lock" "github.com/fleetdm/fleet/v4/server/service/redis_policy_set" @@ -236,44 +238,18 @@ the way that the Fleet server works. case fleet.AllMigrationsCompleted: // OK case fleet.UnknownMigrations: - fmt.Printf("################################################################################\n"+ - "# WARNING:\n"+ - "# Your Fleet database has unrecognized migrations. This could happen when\n"+ - "# running an older version of Fleet on a newer migrated database.\n"+ - "#\n"+ - "# Unknown migrations: tables=%v, data=%v.\n"+ - "################################################################################\n", - migrationStatus.UnknownTable, migrationStatus.UnknownData) + printUnknownMigrationsMessage(migrationStatus.UnknownTable, migrationStatus.UnknownData) if dev { os.Exit(1) } case fleet.SomeMigrationsCompleted: - fmt.Printf("################################################################################\n"+ - "# WARNING:\n"+ - "# Your Fleet database is missing required migrations. This is likely to cause\n"+ - "# errors in Fleet.\n"+ - "#\n"+ - "# Missing migrations: tables=%v, data=%v.\n"+ - "#\n"+ - "# Run `%s prepare db` to perform migrations.\n"+ - "#\n"+ - "# To run the server without performing migrations:\n"+ - "# - Set environment variable FLEET_UPGRADES_ALLOW_MISSING_MIGRATIONS=1, or,\n"+ - "# - Set config updates.allow_missing_migrations to true, or,\n"+ - "# - Use command line argument --upgrades_allow_missing_migrations=true\n"+ - "################################################################################\n", - migrationStatus.MissingTable, migrationStatus.MissingData, os.Args[0]) + tables, data := migrationStatus.MissingTable, migrationStatus.MissingData + printMissingMigrationsWarning(tables, data) if !config.Upgrades.AllowMissingMigrations { os.Exit(1) } case fleet.NoMigrationsCompleted: - fmt.Printf("################################################################################\n"+ - "# ERROR:\n"+ - "# Your Fleet database is not initialized. Fleet cannot start up.\n"+ - "#\n"+ - "# Run `%s prepare db` to initialize the database.\n"+ - "################################################################################\n", - os.Args[0]) + printDatabaseNotInitializedError() os.Exit(1) } @@ -311,7 +287,8 @@ the way that the Fleet server works. os.Exit(1) } } else { - if err := ds.ApplyEnrollSecrets(cmd.Context(), nil, []*fleet.EnrollSecret{{Secret: config.Packaging.GlobalEnrollSecret}}); err != nil { + if err := ds.ApplyEnrollSecrets(cmd.Context(), nil, + []*fleet.EnrollSecret{{Secret: config.Packaging.GlobalEnrollSecret}}); err != nil { level.Debug(logger).Log("err", err, "msg", "failed to apply enroll secrets") } } @@ -350,6 +327,7 @@ the way that the Fleet server works. } level.Info(logger).Log("component", "redis", "mode", redisPool.Mode()) + unCachedDS := ds ds = cached_mysql.New(ds) var dsOpts []mysqlredis.Option if license.DeviceCount > 0 && config.License.EnforceHostLimit { @@ -478,7 +456,8 @@ the way that the Fleet server works. if config.GeoIP.DatabasePath != "" { maxmind, err := fleet.NewMaxMindGeoIP(logger, config.GeoIP.DatabasePath) if err != nil { - level.Error(logger).Log("msg", "failed to initialize maxmind geoip, check database path", "database_path", config.GeoIP.DatabasePath, "error", err) + level.Error(logger).Log("msg", "failed to initialize maxmind geoip, check database path", "database_path", + config.GeoIP.DatabasePath, "error", err) } else { geoIP = maxmind } @@ -527,7 +506,8 @@ the way that the Fleet server works. // reconcile Apple Business Manager configuration environment variables with the database if config.MDM.IsAppleAPNsSet() || config.MDM.IsAppleSCEPSet() { if len(config.Server.PrivateKey) == 0 { - initFatal(errors.New("inserting MDM APNs and SCEP assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + initFatal(errors.New("inserting MDM APNs and SCEP assets"), + "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") } // first we'll check if the APNs and SCEP assets are already in the database and @@ -543,7 +523,8 @@ the way that the Fleet server works. toInsert[fleet.MDMAssetAPNSCert] = struct{}{} toInsert[fleet.MDMAssetAPNSKey] = struct{}{} default: - level.Warn(logger).Log("msg", "Your server already has stored APNs certificates. Fleet will ignore any certificates provided via environment variables when this happens.") + level.Warn(logger).Log("msg", + "Your server already has stored APNs certificates. Fleet will ignore any certificates provided via environment variables when this happens.") } // check DB for SCEP assets @@ -555,14 +536,17 @@ the way that the Fleet server works. toInsert[fleet.MDMAssetCACert] = struct{}{} toInsert[fleet.MDMAssetCAKey] = struct{}{} default: - level.Warn(logger).Log("msg", "Your server already has stored SCEP certificates. Fleet will ignore any certificates provided via environment variables when this happens.") + level.Warn(logger).Log("msg", + "Your server already has stored SCEP certificates. Fleet will ignore any certificates provided via environment variables when this happens.") } if len(toInsert) > 0 { if !config.MDM.IsAppleAPNsSet() { - initFatal(errors.New("Apple APNs MDM configuration must be provided when Apple SCEP is provided"), "validate Apple MDM") + initFatal(errors.New("Apple APNs MDM configuration must be provided when Apple SCEP is provided"), + "validate Apple MDM") } else if !config.MDM.IsAppleSCEPSet() { - initFatal(errors.New("Apple SCEP MDM configuration must be provided when Apple APNs is provided"), "validate Apple MDM") + initFatal(errors.New("Apple SCEP MDM configuration must be provided when Apple APNs is provided"), + "validate Apple MDM") } // parse the APNs and SCEP assets from the config @@ -603,7 +587,8 @@ the way that the Fleet server works. // reconcile Apple Business Manager configuration environment variables with the database if config.MDM.IsAppleBMSet() { if len(config.Server.PrivateKey) == 0 { - initFatal(errors.New("inserting MDM ABM assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + initFatal(errors.New("inserting MDM ABM assets"), + "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") } appleBM, err := config.MDM.AppleBM() @@ -618,9 +603,11 @@ the way that the Fleet server works. case err != nil: initFatal(err, "reading ABM assets from database") case !found: - toInsert = append(toInsert, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMKey, Value: appleBM.KeyPEM}, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMCert, Value: appleBM.CertPEM}) + toInsert = append(toInsert, fleet.MDMConfigAsset{Name: fleet.MDMAssetABMKey, Value: appleBM.KeyPEM}, + fleet.MDMConfigAsset{Name: fleet.MDMAssetABMCert, Value: appleBM.CertPEM}) default: - level.Warn(logger).Log("msg", "Your server already has stored ABM certificates and token. Fleet will ignore any certificates provided via environment variables when this happens.") + level.Warn(logger).Log("msg", + "Your server already has stored ABM certificates and token. Fleet will ignore any certificates provided via environment variables when this happens.") } if len(toInsert) > 0 { @@ -636,7 +623,8 @@ the way that the Fleet server works. // apple_mdm_dep_profile_assigner cron and backfilled if _, err := ds.InsertABMToken(context.Background(), &fleet.ABMToken{ EncryptedToken: appleBM.EncryptedToken, - RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), // 2000-01-01 is our "zero value" for time + RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, + time.UTC), // 2000-01-01 is our "zero value" for time }); err != nil { initFatal(err, "save ABM token") } @@ -759,6 +747,15 @@ the way that the Fleet server works. if err != nil { initFatal(err, "initializing service") } + androidSvc, err := android_service.NewService( + ctx, + logger, + mysql.NewAndroidDS(unCachedDS), + ds, + ) + if err != nil { + initFatal(err, "initializing android service") + } var softwareInstallStore fleet.SoftwareInstallerStore var bootstrapPackageStore fleet.MDMBootstrapPackageStore @@ -767,7 +764,8 @@ the way that the Fleet server works. profileMatcher := apple_mdm.NewProfileMatcher(redisPool) if config.S3.SoftwareInstallersBucket != "" { if config.S3.BucketsAndPrefixesMatch() { - level.Warn(logger).Log("msg", "the S3 buckets and prefixes for carves and software installers appear to be identical, this can cause issues") + level.Warn(logger).Log("msg", + "the S3 buckets and prefixes for carves and software installers appear to be identical, this can cause issues") } // Extract the CloudFront URL signer before creating the S3 stores. config.S3.ValidateCloudFrontURL(initFatal) @@ -811,7 +809,9 @@ the way that the Fleet server works. softwareInstallStore = fleet.FailingSoftwareInstallerStore{} } else { softwareInstallStore = store - level.Info(logger).Log("msg", "using local filesystem software installer store, this is not suitable for production use", "directory", installerDir) + level.Info(logger).Log("msg", + "using local filesystem software installer store, this is not suitable for production use", "directory", + installerDir) } } @@ -992,6 +992,12 @@ the way that the Fleet server works. }); err != nil { initFatal(err, "failed to register maintained apps schedule") } + + if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) { + return newRefreshVPPAppVersionsSchedule(ctx, instanceID, ds, logger) + }); err != nil { + initFatal(err, "failed to register refresh vpp app versions schedule") + } } if license.IsPremium() && config.Activity.EnableAuditLog { @@ -1069,7 +1075,8 @@ the way that the Fleet server works. frontendHandler = service.WithMDMEnrollmentMiddleware(svc, httpLogger, frontendHandler) - apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore) + apiHandler = service.MakeHandler(svc, config, httpLogger, limiterStore, + []endpoint_utils.HandlerRoutesFunc{android_service.GetRoutes(svc, androidSvc)}) setupRequired, err := svc.SetupRequired(baseCtx) if err != nil { @@ -1142,7 +1149,8 @@ the way that the Fleet server works. initFatal(err, "inserting SCEP challenge") } - level.Warn(logger).Log("msg", "Your server already has stored a SCEP challenge. Fleet will ignore this value provided via environment variables when this happens.") + level.Warn(logger).Log("msg", + "Your server already has stored a SCEP challenge. Fleet will ignore this value provided via environment variables when this happens.") } } if err := service.RegisterAppleMDMProtocolServices( @@ -1204,7 +1212,8 @@ the way that the Fleet server works. } if (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/software/package")) || - (req.Method == http.MethodPatch && strings.HasSuffix(req.URL.Path, "/package") && strings.Contains(req.URL.Path, "/fleet/software/titles/")) || + (req.Method == http.MethodPatch && strings.HasSuffix(req.URL.Path, "/package") && strings.Contains(req.URL.Path, + "/fleet/software/titles/")) || (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/bootstrap")) { var zeroTime time.Time rc := http.NewResponseController(rw) @@ -1348,6 +1357,34 @@ the way that the Fleet server works. return serveCmd } +func printDatabaseNotInitializedError() { + fmt.Printf("################################################################################\n"+ + "# ERROR:\n"+ + "# Your Fleet database is not initialized. Fleet cannot start up.\n"+ + "#\n"+ + "# Run `%s prepare db` to initialize the database.\n"+ + "################################################################################\n", + os.Args[0]) +} + +func printMissingMigrationsWarning(tables []int64, data []int64) { + fmt.Printf("################################################################################\n"+ + "# WARNING:\n"+ + "# Your Fleet database is missing required migrations. This is likely to cause\n"+ + "# errors in Fleet.\n"+ + "#\n"+ + "# Missing migrations: %s.\n"+ + "#\n"+ + "# Run `%s prepare db` to perform migrations.\n"+ + "#\n"+ + "# To run the server without performing migrations:\n"+ + "# - Set environment variable FLEET_UPGRADES_ALLOW_MISSING_MIGRATIONS=1, or,\n"+ + "# - Set config updates.allow_missing_migrations to true, or,\n"+ + "# - Use command line argument --upgrades_allow_missing_migrations=true\n"+ + "################################################################################\n", + tablesAndDataToString(tables, data), os.Args[0]) +} + func initLicense(config configpkg.FleetConfig, devLicense, devExpiredLicense bool) (*fleet.LicenseInfo, error) { if devLicense { // This license key is valid for development only diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go index f18bfe391e90..2d32c659abf5 100644 --- a/cmd/fleet/serve_test.go +++ b/cmd/fleet/serve_test.go @@ -598,6 +598,51 @@ func TestScanVulnerabilities(t *testing.T) { require.Equal(t, 1, webhookCount) } +func TestUpdateVulnHostCounts(t *testing.T) { + logger := kitlog.NewNopLogger() + logger = level.NewFilter(logger, level.AllowDebug()) + + ctx := context.Background() + + ds := new(mock.Store) + + testCases := []struct { + desc string + maxConcurrency int + expectedMaxConcurrency int + }{ + { + desc: "invalid max concurrency count: 0", + maxConcurrency: 0, + expectedMaxConcurrency: 1, + }, + { + desc: "invalid max concurrency count: < 0", + maxConcurrency: -1, + expectedMaxConcurrency: 1, + }, + { + desc: "valid max concurrency count", + maxConcurrency: 10, + expectedMaxConcurrency: 10, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + var gotMaxConcurrency int + ds.UpdateVulnerabilityHostCountsFunc = func(ctx context.Context, maxRoutines int) error { + gotMaxConcurrency = maxRoutines + return nil + } + + err := updateVulnHostCounts(ctx, ds, logger, tc.maxConcurrency) + require.NoError(t, err) + + require.Equal(t, tc.expectedMaxConcurrency, gotMaxConcurrency) + }) + } +} + func TestScanVulnerabilitiesMkdirFailsIfVulnPathIsFile(t *testing.T) { logger := kitlog.NewNopLogger() logger = level.NewFilter(logger, level.AllowDebug()) diff --git a/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml b/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml index aa64529ff40d..ec2aaf7435c0 100644 --- a/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml +++ b/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml @@ -778,6 +778,8 @@ spec: CE506065-7C0E-434E-8B8C-12E164116C94 PayloadVersion 1 + DisableFDEAutoLogin + com.apple.login.mcx.DisableAutoLoginClient diff --git a/docs/Configuration/agent-configuration.md b/docs/Configuration/agent-configuration.md index 867d9478a14d..3647f7c50aed 100644 --- a/docs/Configuration/agent-configuration.md +++ b/docs/Configuration/agent-configuration.md @@ -56,7 +56,7 @@ To see a description for all available settings, first [enroll your host](https: osquery > SELECT name, default_value, value, description FROM osquery_flags; ``` -> Running the interactive osquery shell loads a standalone instance of osquery, with a default configuration rather than the one set in `Agent options`. If you'd like to verify that your hosts are running with the latest settings set in `options`, run the query as a live query through Fleet. +> Running the interactive osquery shell loads a standalone instance of osquery, with a default configuration rather than the one set in agent options. If you'd like to verify that your hosts are running with the latest settings set in `options`, run the query as a live query through Fleet. > If you revoked an old enroll secret, the `command_line_flags` won't update for hosts that enrolled to Fleet using this old enroll secret. This is because fleetd uses the enroll secret to receive new flags from Fleet. For these hosts, all existing features will work as expected. diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md index 9d1f483cfb53..006e8cd3836f 100644 --- a/docs/Configuration/yaml-files.md +++ b/docs/Configuration/yaml-files.md @@ -501,6 +501,7 @@ The `sso_settings` section lets you define single sign-on (SSO) settings. Learn - `enable_sso` (default: `false`) - `idp_name` is the human-friendly name for the identity provider that will provide single sign-on authentication (default: `""`). +- `idp_image_url` is an optional link to an image such as a logo for the identity provider. (default: `""`). - `entity_id` is the entity ID: a Uniform Resource Identifier (URI) that you use to identify Fleet when configuring the identity provider. It must exactly match the Entity ID field used in identity provider configuration (default: `""`). - `metadata` is the metadata (in XML format) provided by the identity provider. (default: `""`) - `metadata_url` is the URL that references the identity provider metadata. Only one of `metadata` or `metadata_url` is required (default: `""`). @@ -516,6 +517,7 @@ org_settings: sso_settings: enable_sso: true idp_name: Okta + idp_image_url: https://www.okta.com/favicon.ico entity_id: https://example.okta.com metadata: $SSO_METADATA enable_jit_provisioning: true # Available in Fleet Premium diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md index 42fc777d8878..0e61fad1f09a 100644 --- a/docs/REST API/rest-api.md +++ b/docs/REST API/rest-api.md @@ -10495,43 +10495,41 @@ _Available in Fleet Premium_ ```json { - "teams": [ - { - "name": "workstations", - "id": 1, - "user_count": 0, - "host_count": 0, - "agent_options": { - "config": { - "options": { - "pack_delimiter": "/", - "logger_tls_period": 10, - "distributed_plugin": "tls", - "disable_distributed": false, - "logger_tls_endpoint": "/api/v1/osquery/log", - "distributed_interval": 10, - "distributed_tls_max_attempts": 3 - }, - "decorators": { - "load": [ - "SELECT uuid AS host_uuid FROM system_info;", - "SELECT hostname AS hostname FROM system_info;" - ] - } + "team": { + "name": "workstations", + "id": 1, + "user_count": 0, + "host_count": 0, + "agent_options": { + "config": { + "options": { + "pack_delimiter": "/", + "logger_tls_period": 10, + "distributed_plugin": "tls", + "disable_distributed": false, + "logger_tls_endpoint": "/api/v1/osquery/log", + "distributed_interval": 10, + "distributed_tls_max_attempts": 3 }, - "overrides": {}, - "command_line_flags": {} - }, - "webhook_settings": { - "failing_policies_webhook": { - "enable_failing_policies_webhook": false, - "destination_url": "", - "policy_ids": null, - "host_batch_size": 0 + "decorators": { + "load": [ + "SELECT uuid AS host_uuid FROM system_info;", + "SELECT hostname AS hostname FROM system_info;" + ] } + }, + "overrides": {}, + "command_line_flags": {} + }, + "webhook_settings": { + "failing_policies_webhook": { + "enable_failing_policies_webhook": false, + "destination_url": "", + "policy_ids": null, + "host_batch_size": 0 } } - ] + } } ``` diff --git a/frontend/components/TargetLabelSelector/TargetLabelSelector.tsx b/frontend/components/TargetLabelSelector/TargetLabelSelector.tsx index d4fe8c646841..6e1703747764 100644 --- a/frontend/components/TargetLabelSelector/TargetLabelSelector.tsx +++ b/frontend/components/TargetLabelSelector/TargetLabelSelector.tsx @@ -156,7 +156,7 @@ interface ITargetLabelSelectorProps { customTargetOptions: IDropdownOption[]; selectedLabels: Record; labels: ILabelSummary[]; - /** set this prop to show a help text. If it is encluded then it will override + /** set this prop to show a help text. If it is included then it will override * the selected options defined `helpText` */ dropdownHelpText?: ReactNode; diff --git a/frontend/pages/ManageControlsPage/Scripts/Scripts.tsx b/frontend/pages/ManageControlsPage/Scripts/Scripts.tsx index 38582004f053..566f726e705a 100644 --- a/frontend/pages/ManageControlsPage/Scripts/Scripts.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/Scripts.tsx @@ -37,9 +37,7 @@ interface IScriptsProps { const Scripts = ({ router, currentPage, teamIdForApi }: IScriptsProps) => { const { isPremiumTier } = useContext(AppContext); const [showDeleteScriptModal, setShowDeleteScriptModal] = useState(false); - const [showScriptDetailsModal, setShowScriptDetailsModal] = useState(false); const [showEditScripsModal, setShowEditScriptModal] = useState(false); - const [goBackToScriptDetails, setGoBackToScriptDetails] = useState(false); // Used for onCancel in delete modal const selectedScript = useRef(null); @@ -86,13 +84,7 @@ const Scripts = ({ router, currentPage, teamIdForApi }: IScriptsProps) => { const onClickScript = (script: IScript) => { selectedScript.current = script; - setShowScriptDetailsModal(true); - }; - - const onCancelScriptDetails = () => { - selectedScript.current = null; - setShowScriptDetailsModal(false); - setGoBackToScriptDetails(false); + setShowEditScriptModal(true); }; const onEditScript = (script: IScript) => { @@ -112,12 +104,7 @@ const Scripts = ({ router, currentPage, teamIdForApi }: IScriptsProps) => { const onCancelDelete = () => { setShowDeleteScriptModal(false); - - if (goBackToScriptDetails) { - setShowScriptDetailsModal(true); - } else { - selectedScript.current = null; - } + selectedScript.current = null; }; const onDeleteScript = () => { @@ -200,21 +187,6 @@ const Scripts = ({ router, currentPage, teamIdForApi }: IScriptsProps) => { onDone={onDeleteScript} /> )} - {showScriptDetailsModal && selectedScript.current && ( - { - setShowScriptDetailsModal(false); - setShowDeleteScriptModal(true); - setGoBackToScriptDetails(true); - }} - runScriptHelpText - /> - )} {showEditScripsModal && selectedScript.current && (
To run this script on a host, go to the{" "} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp/SoftwareVppForm/SoftwareVppForm.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp/SoftwareVppForm/SoftwareVppForm.tsx index 4612d1b22ecd..3e09c1183600 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp/SoftwareVppForm/SoftwareVppForm.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp/SoftwareVppForm/SoftwareVppForm.tsx @@ -15,6 +15,7 @@ import TargetLabelSelector from "components/TargetLabelSelector"; import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon"; import { CUSTOM_TARGET_OPTIONS, + generateHelpText, generateSelectedLabels, getCustomTarget, getTargetType, @@ -224,6 +225,9 @@ const SoftwareVppForm = ({ onSelectCustomTarget={onSelectCustomTargetOption} onSelectLabel={onSelectLabel} labels={labels || []} + dropdownHelpText={ + generateHelpText("manual", formData.customTarget) // maps to manual install help text + } /> {renderSelfServiceContent(softwareVppForEdit.platform)}
@@ -253,6 +257,9 @@ const SoftwareVppForm = ({ onSelectCustomTarget={onSelectCustomTargetOption} onSelectLabel={onSelectLabel} labels={labels || []} + dropdownHelpText={ + generateHelpText("manual", formData.customTarget) // maps to manual install help text + } /> {renderSelfServiceContent( ("selectedApp" in formData && diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx index b40247373c98..9c6d13b51cb9 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx @@ -321,25 +321,35 @@ const SoftwareInstallerCard = ({ ); }; - const versionInfo = version ? ( - {version} - ) : ( - - Fleet couldn't read the version from {name}.{" "} - - - } - > - Version (unknown) - - ); + let versionInfo = {version}; + + if (installerType === "vpp") { + versionInfo = ( + Updated every hour.}> + {version} + + ); + } + + if (installerType === "package" && !version) { + versionInfo = ( + + Fleet couldn't read the version from {name}.{" "} + + + } + > + Version (unknown) + + ); + } const renderDetails = () => { return !uploadedAt ? ( diff --git a/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tests.tsx b/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tests.tsx new file mode 100644 index 000000000000..9569b52a29b7 --- /dev/null +++ b/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tests.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { noop } from "lodash"; +import { render, screen } from "@testing-library/react"; + +import DeleteHostModal from "./DeleteHostModal"; + +describe("DeleteHostModal", () => { + it("renders the number of hosts selected", () => { + render( + + ); + expect(screen.getByText("3 hosts")).toBeVisible(); + }); + + it("renders the host name when only the host name is provided", () => { + render( + + ); + expect(screen.getByText("Host1")).toBeVisible(); + }); + + it("renders the number of hosts selected with '+' after when select all matching hosts is true", () => { + render( + + ); + expect(screen.getByText("3+ hosts")).toBeVisible(); + }); + + it("renders the host count with '+' and an additional warning when there are more than 500 hosts and select all matching hosts is true", () => { + render( + + ); + expect(screen.getByText("3+ hosts")).toBeVisible(); + expect( + screen.getByText( + "When deleting a large volume of hosts, it may take some time for this change to be reflected in the UI." + ) + ).toBeVisible(); + }); +}); diff --git a/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tsx b/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tsx index 90651cb292c2..8f11661fa6fc 100644 --- a/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tsx +++ b/frontend/pages/hosts/components/DeleteHostModal/DeleteHostModal.tsx @@ -47,25 +47,26 @@ const DeleteHostModal = ({ } return hostName; }; - const largeVolumeText = (): string => { - if ( - selectedHostIds && - isAllMatchingHostsSelected && - hostsCount && - hostsCount >= 500 - ) { - return " When deleting a large volume of hosts, it may take some time for this change to be reflected in the UI."; - } - return ""; - }; + + const hasManyHosts = + selectedHostIds && + isAllMatchingHostsSelected && + hostsCount && + hostsCount >= 500; return ( <>

This will remove the record of {hostText()} and associated data - (e.g. unlock PINs).{largeVolumeText()} + such as unlock PINs and disk encryption keys.

+ {hasManyHosts && ( +

+ When deleting a large volume of hosts, it may take some time for + this change to be reflected in the UI. +

+ )}
  • macOS, Windows, or Linux hosts will re-appear unless Fleet's diff --git a/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tests.tsx b/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tests.tsx new file mode 100644 index 000000000000..81cdd5485c65 --- /dev/null +++ b/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tests.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { noop } from "lodash"; +import { render, screen } from "@testing-library/react"; + +import TransferHostModal from "./TransferHostModal"; + +describe("TransferHostModal", () => { + it("renders the correct message when more than one host is being transfered", () => { + render( + + ); + + expect( + screen.getByText( + "The hosts' disk encryption keys are deleted if they're transferred to a team with disk encryption turned off." + ) + ).toBeVisible(); + }); + + it("render the correct message when one host is being transfered", () => { + render( + + ); + + expect( + screen.getByText( + "The host's disk encryption key is deleted if it's transferred to a team with disk encryption turned off." + ) + ).toBeVisible(); + }); +}); diff --git a/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tsx b/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tsx index 5993415e4c21..c75e160cfdf6 100644 --- a/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tsx +++ b/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tsx @@ -65,43 +65,58 @@ const TransferHostModal = ({ return [NO_TEAM_OPTION, ...teamOptions]; }; + const diskEncryptionMsg = ( + <> + The {multipleHosts ? "hosts'" : "host's"} disk encryption{" "} + {multipleHosts ? "keys are" : "key is"} deleted if{" "} + {multipleHosts ? "they're" : "it's"} transferred to a team with disk + encryption turned off. + + ); + return ( - - - {isGlobalAdmin ? ( -

    - Team not here?{" "} - - Create a team - -

    - ) : null} -
    - - -
    - + <> +

    {diskEncryptionMsg}

    +
    + + {isGlobalAdmin ? ( +

    + Team not here?{" "} + + Create a team + +

    + ) : null} +
    + + +
    + +
    ); }; diff --git a/handbook/company/product-groups.md b/handbook/company/product-groups.md index 7fd2c455d204..e4e17fc68a1e 100644 --- a/handbook/company/product-groups.md +++ b/handbook/company/product-groups.md @@ -469,6 +469,8 @@ Once reproduced, QA documents the reproduction steps in the description and move QA has reproduced the issue successfully. It should now be transferred to product design if it is a released bug, or engineering if it is unreleased. +If the bugs is missing reproduction steps, it's removed from the drafting board and sent back to the "inbox" state. + Remove the β€œ:reproduce” label, add the following labels: 1. The relevant product group (e.g. `#g-mdm`, `#g-orchestration`, `#g-software`). Make your best guess, and product design will change if necessary. diff --git a/handbook/company/why-this-way.md b/handbook/company/why-this-way.md index 6331a85de790..5626270a939f 100644 --- a/handbook/company/why-this-way.md +++ b/handbook/company/why-this-way.md @@ -143,6 +143,33 @@ Besides the exceptions above, Fleet does not use any other repositories. Other > _**Tip:** In addition to the built-in search available for the public handbook on fleetdm.com, you can also [search any public AND non-public content, including issue templates, at the same time](https://github.com/search?q=org%3Afleetdm+path%3A.github%2FISSUE_TEMPLATE+path%3Ahandbook%2F+path%3Adocs%2F+foo&type=code)._ +## Why be intentional about infrastructure? + +Our infrastructure is simple to prioritize [results](https://fleetdm.com/handbook/company#results), spend less, avoid preemptive structure, choose "boring" solutions, and reuse systems whenever possible. Adding infrastructure slows us down by adding complexity and surface area to maintain. + +All new infrastructure at Fleet is first approved by the E-group. Currently approved infrastructure dependencies when deploying Fleet are maintained in the [references architecture documentation](https://fleetdm.com/docs/deploy/reference-architectures). + +Additional infrastructure: + +1. **HTTP server at [fleetdm.com](https://fleetdm.com/)**. When a public HTTP server is required to broker information, [Digital Experience](https://fleetdm.com/handbook/digital-experience) adds the functionality to the existing fleetdm.com HTTP server. The fleetdm.com web server is hosted at [Heroku](https://heroku.com/). + +2. **Managed Cloud**. All Managed Cloud [customer environments](https://docs.google.com/spreadsheets/d/1nGgy7Gx1Y3sYHinL8kFWnhejghV1QDtv9uQgKu91F9E/edit?usp=sharing) and Fleet's dogfooding environments are hosted at [AWS](https://aws.amazon.com). + +3. **Dashboards**. Additional product dashboards such as the [vulnerability dashboard](https://github.com/fleetdm/fleet/tree/main/ee/vulnerability-dashboard) and [bulk operations dashboard](https://github.com/fleetdm/fleet/tree/main/ee/bulk-operations-dashboard) are deployed to [Heroku](https://heroku.com) on an as-needed basis per customer. + +4. **Development and QA instances**. Long-lived Fleet instances used to support CI/CD pipelines and quality assurance processes are hosted at [Render](https://render.com/). + +5. **CI/CD pipelines**. All CI/CD pipelines supporting Fleet's infrastructure are hosted as GitHub workflows in both [our public](https://github.com/fleetdm/fleet/actions) and [private](https://github.com/fleetdm/confidential/actions) repositories. + +6. **[Terraform submodules](https://github.com/fleetdm/fleet-terraform**. Submodules provided by Fleet to enable configuration of services required to securely scale Fleet to tens of thousands of hosts. These services require privileged access to cloud resources, and their composition and configuration is unique for each deployment. + +7. **Domain name registrar**. All Fleet domain names are registered with [NameCheap](https://www.namecheap.com). + +8. **DNS**. All domain DNS records and caching rules are hosted with [Cloudflare](https://www.cloudflare.com/). + +9. **Object storage**. All object storage dependencies necessary to operate a fleetdm.com instance (download.fleetdm.com, updates.fleetdm.com), are hosted in R2 buckets at [Cloudflare](https://www.cloudflare.com). + + ## Why not continuously generate REST API reference docs from javadoc-style code comments? Here are a few of the drawbacks that we have experienced when generating docs via tools like Swagger or OpenAPI, and some of the advantages of doing it by hand with Markdown. diff --git a/handbook/customer-success/customer-success.rituals.yml b/handbook/customer-success/customer-success.rituals.yml index 2c493ccf7472..ae62f82a9c11 100644 --- a/handbook/customer-success/customer-success.rituals.yml +++ b/handbook/customer-success/customer-success.rituals.yml @@ -6,7 +6,7 @@ moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "zayhanlon" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Enables automation of GitHub issues - labels: [ "#g-customer-success" ] # label to be applied to issue + labels: [ ":help-customers" ] # label to be applied to issue repo: "confidential" - task: "Process new requests" diff --git a/handbook/demand/demand.rituals.yml b/handbook/demand/demand.rituals.yml index d235607fd6d8..4cce691b16e7 100644 --- a/handbook/demand/demand.rituals.yml +++ b/handbook/demand/demand.rituals.yml @@ -13,7 +13,7 @@ moreInfoUrl: "https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible" #URL used to highlight "description:" test in table dri: "mikermcneil" # DRI for ritual (assignee if autoIssue) (TODO display GitHub proflie pic instead of name or title) autoIssue: # Β« Enable automation of GitHub issues - labels: [ "#g-demand" ] # label to be applied to issue + labels: [ ":help-marketing" ] # label to be applied to issue repo: "confidential" - task: "Settle event strategy" @@ -73,9 +73,7 @@ dri: "Drew-P-drawers" - task: "Check ongoing events" - startedOn: "2024-10-21" - frequency: "Daily" description: "Check event issues and complete steps." moreInfoUrl: "https://fleetdm.com/handbook/engineering#book-an-event" diff --git a/handbook/digital-experience/security.md b/handbook/digital-experience/security.md index a9c71c039fbf..1b76b9eba96b 100644 --- a/handbook/digital-experience/security.md +++ b/handbook/digital-experience/security.md @@ -362,7 +362,7 @@ Because they are the only type of Two-Factor Authentication (2FA) that protects phishing, we will make them **mandatory for everyone** soon. See the [Google Workspace security -section](https://fleetdm.com/handbook/digital-experience/security#google-workspace-security-authentication) for more +section](https://fleetdm.com/handbook/digital-experience/security#2-step-verification) for more information on the security of different types of 2FA. diff --git a/handbook/product-design/README.md b/handbook/product-design/README.md index 87be84776e74..6ccda126c0af 100644 --- a/handbook/product-design/README.md +++ b/handbook/product-design/README.md @@ -37,9 +37,11 @@ If a customer/prospect request is missing a Gong snippet or requires additional As soon as we've addressed the next quarter's objectives, the Head of Product Design cancels the daily meeting. -### Product design check in +### Triage new bugs -The Head of Product Design summarizes the current week's design reviews to discuss with the CEO. +Product Designers are responsible for triaging all new reproduced bugs. + +Learn more [here](https://fleetdm.com/handbook/company/product-groups#in-product-drafting). ### Drafting @@ -146,17 +148,6 @@ At Fleet, features are placed behind feature flags if the changes could affect F > Fleet's feature flag guidelines is borrowed from GitLab's ["When to use feature flags" section](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags) of their handbook. Check out [GitLab's "Feature flags only when needed" video](https://www.youtube.com/watch?v=DQaGqyolOd8) for an explanation of the costs of introducing feature flags. -### View Fleet usage statistics - -In order to understand the usage of the Fleet product, we [collect statistics](https://fleetdm.com/docs/using-fleet/usage-statistics) from installations where this functionality is enabled. - -Fleeties can view these statistics in the Google spreadsheet [Fleet -usage](https://docs.google.com/spreadsheets/d/1Mh7Vf4kJL8b5TWlHxcX7mYwaakZMg_ZGNLY3kl1VI-c/edit#gid=0) -available in Google Drive. - -Some of the data is forwarded to [Datadog](https://us5.datadoghq.com/dashboard/7pb-63g-xty/usage-statistics?from_ts=1682952132131&to_ts=1685630532131&live=true) and is available to Fleeties. - - ### Prepare reference docs for release Every change to how Fleet is used is reflected live on the website in reference documentation **at release day** (REST API, config surface, tables, and other already-existing docs under /docs/using-fleet). diff --git a/handbook/sales/README.md b/handbook/sales/README.md index 5389189bf265..a3f241d748d5 100644 --- a/handbook/sales/README.md +++ b/handbook/sales/README.md @@ -63,6 +63,16 @@ Every week, the sales manager will review the necessary opportunities with inter If no opportunities meet these criteria, the meeting is used to discuss the oldest opportunities and close any that are stalled. +### Send a subscription quote + +Reseller partners occasionally reach out and ask Fleet for a quote on behalf of customers. Use the following steps to provide a subscription quote: +1. Navigate to the [Google Docs template gallery](https://docs.google.com/document/u/0/?ftv=1&tgif=d) and make a copy of the "TEMPLATE - 3EYE - Subscription quote" document. +2. Assign the "Quote #" by combining the "ISO date" (YYYYMMDD) and "Total amount" (e.g. "YYYYMMDD-000000" or "20250212-128400"). +3. Make sure the "Customer", "Customer contact", "Total term", "Effective dates", "Billing frequency/timing", and "Payment terms" fields are correctly reflected on the subscription quote +4. Insert the correct "Quantity", "Total list price", "Distributor price", and "Effective price" in the table. If a discount is applied to the quote, insert the appropriate ISO date in the "**Discount.**" term at the bottom of the page. +5. Insert the total effective price in the "Total amount (USD)" field and send the quote. + + ### Send an order form In order to be transparent, Fleet sends order forms within 30 days of opportunity creation in most cases. All quotes and purchase orders must be approved by the CRO and 🌐 [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) before being sent to the prospect or customer. Often, the CRO will request legal review of any unique terms required. To prepare and send a subscription order form the Fleet owner of the opportunity (usually AE or CSM) will: diff --git a/infrastructure/loadtesting/terraform/readme.md b/infrastructure/loadtesting/terraform/readme.md index 802b362f8635..854ade8018f2 100644 --- a/infrastructure/loadtesting/terraform/readme.md +++ b/infrastructure/loadtesting/terraform/readme.md @@ -13,8 +13,8 @@ These are set via [variables](https://github.com/fleetdm/fleet/blob/main/infrast # When first applying. Assuming tag exists terraform apply -var tag=hosts-5k-test -var fleet_containers=5 -var db_instance_type=db.t4g.medium -var redis_instance_type=cache.t4g.small -# When adding loadtest containers. -terraform apply -var tag=hosts-5k-test -var fleet_containers=5 -var db_instance_type=db.t4g.medium -var redis_instance_type=cache.t4g.small -var -var loadtest_containers=10 +# When adding loadtest containers. +terraform apply -var tag=hosts-5k-test -var fleet_containers=5 -var db_instance_type=db.t4g.medium -var redis_instance_type=cache.t4g.small -var -var loadtest_containers=10 ``` ### Deploying your code to the loadtesting environment @@ -64,7 +64,7 @@ export TF_VAR_fleet_config='{"FLEET_DEV_MDM_APPLE_DISABLE_PUSH":"1","FLEET_DEV_M ``` - The above is needed because the newline characters in the certificate/key/token files. -- The value set in `FLEET_MDM_APPLE_SCEP_CHALLENGE` must match whatever you set in `osquery-perf`'s `mdm_scep_challenge` argument. +- The value set in `FLEET_MDM_APPLE_SCEP_CHALLENGE` must match whatever you set in `osquery-perf`'s `mdm_scep_challenge` argument. - The above `export TF_VAR_fleet_config=...` command was tested on `bash`. It did not work in `zsh`. - Note that we are also setting `FLEET_DEV_MDM_APPLE_DISABLE_PUSH=1`. We don't want to generate push notifications against fake UUIDs (otherwise it may cause Apple to rate limit due to invalid requests). - Note that we are also setting `FLEET_DEV_MDM_APPLE_DISABLE_DEVICE_INFO_CERT_VERIFY=1` to skip verification of Apple certificates for OTA enrollments. @@ -86,6 +86,8 @@ With the variable `loadtest_containers` you can specify how many containers of 5 ### Monitoring the infrastructure +This [document](https://docs.google.com/document/d/1V6QtFzcGDsLnn2PIvGin74DAxdAN_3likjxSssOMMQI/edit?tab=t.0) covers the load test key metrics to capture or keep an eye on. Results are collected in [this spreadsheet](https://docs.google.com/spreadsheets/d/1FOF0ykFVoZ7DJSTfrveip0olfyRQsY9oT1uXCCZmuKc/edit?gid=0#gid=0) for release-specific load tests. + There are a few main places of interest to monitor the load and resource usage: * The Application Performance Monitoring (APM) dashboard: access it on your Fleet load-testing URL on port `:5601` and path `/app/apm`, e.g. `https://loadtest.fleetdm.com:5601/app/apm`. Note to do this without the VPN you will need to add your public IP Address to the load balancer for TCP Port 5601. At the time of this writing, [this](https://us-east-2.console.aws.amazon.com/vpc/home?region=us-east-2#SecurityGroup:groupId=sg-0e67d910a662720f8) will take you directly to the security group for the load balancer if logged into the Load Testing account. @@ -180,4 +182,4 @@ provider "docker" { host = "unix:///Users/foobar/.docker/run/docker.sock" } [...] -``` \ No newline at end of file +``` diff --git a/orbit/CHANGELOG.md b/orbit/CHANGELOG.md index f074881d287d..4c6be53bfb25 100644 --- a/orbit/CHANGELOG.md +++ b/orbit/CHANGELOG.md @@ -1,3 +1,7 @@ +## Orbit 1.39.1 (Feb 12, 2025) + +* Fixed a bug where fleetd could not install software from an old fleet server. + ## Orbit 1.39.0 (Feb 07, 2025) * Fixed a bug where the `SystemDrive` environment variable was not being passed to osqueryd. diff --git a/orbit/TUF.md b/orbit/TUF.md index c3f3790c08d2..c33743227851 100644 --- a/orbit/TUF.md +++ b/orbit/TUF.md @@ -7,8 +7,8 @@ Following are the currently deployed versions of fleetd components on the `stabl | Component\OS | macOS | Linux | Windows | Linux (arm64) | |--------------|--------------|--------|---------|---------------| -| orbit | 1.38.1 | 1.38.1 | 1.38.1 | 1.38.1 | -| desktop | 1.38.1 | 1.38.1 | 1.38.1 | 1.38.1 | +| orbit | 1.39.1 | 1.39.1 | 1.39.1 | 1.39.1 | +| desktop | 1.39.1 | 1.39.1 | 1.39.1 | 1.39.1 | | osqueryd | 5.15.0 | 5.15.0 | 5.15.0 | 5.15.0 | | nudge | 1.1.10.81462 | - | - | - | | swiftDialog | 2.1.0 | - | - | - | @@ -18,8 +18,8 @@ Following are the currently deployed versions of fleetd components on the `stabl | Component\OS | macOS | Linux | Windows | Linux (arm64) | |--------------|--------|--------|---------|---------------| -| orbit | 1.38.1 | 1.38.1 | 1.38.1 | 1.38.1 | -| desktop | 1.38.1 | 1.38.1 | 1.38.1 | 1.38.1 | +| orbit | 1.39.1 | 1.39.1 | 1.39.1 | 1.39.1 | +| desktop | 1.39.1 | 1.39.1 | 1.39.1 | 1.39.1 | | osqueryd | 5.15.0 | 5.15.0 | 5.15.0 | 5.15.0 | | nudge | - | - | - | - | | swiftDialog | - | - | - | - | diff --git a/package.json b/package.json index f8e87872d2a7..105a063798a6 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "core-js": "3.25.1", "date-fns": "3.6.0", "date-fns-tz": "3.1.3", - "dompurify": "3.1.3", + "dompurify": "3.2.4", "es6-object-assign": "1.1.0", "es6-promise": "4.2.8", "file-saver": "1.3.8", diff --git a/server/archtest/archtest.go b/server/archtest/archtest.go new file mode 100644 index 000000000000..0078bed0b898 --- /dev/null +++ b/server/archtest/archtest.go @@ -0,0 +1,199 @@ +package archtest + +import ( + "container/list" + "go/build" + "regexp" + "slices" + "strings" + + "golang.org/x/tools/go/packages" +) + +// PackageTest is an architecture test to check package dependencies. +// It is used to ensure that packages do not depend on each other in a way that increases coupling and maintainability. +// Based on https://github.com/matthewmcnew/archtest +type PackageTest struct { + t TestingT + pkgs []string + includeRegex *regexp.Regexp + ignorePkgs map[string]struct{} + withTests bool +} + +// PackageTest will ignore dependency on this package. +const thisPackage = "github.com/fleetdm/fleet/v4/server/archtest" + +type TestingT interface { + Errorf(format string, args ...any) +} + +func NewPackageTest(t TestingT, packageName ...string) *PackageTest { + return &PackageTest{t: t, pkgs: packageName} +} + +// OnlyInclude sets a regex to filter the packages to include in the dependency check. +// This significantly speeds up the dependency check by only importing the packages that match the regex. +func (pt *PackageTest) OnlyInclude(regex *regexp.Regexp) *PackageTest { + pt.includeRegex = regex + return pt +} + +func (pt *PackageTest) IgnorePackages(pkgs ...string) *PackageTest { + if pt.ignorePkgs == nil { + pt.ignorePkgs = make(map[string]struct{}, len(pkgs)) + } + for _, p := range pt.expandPackages(pkgs) { + pt.ignorePkgs[p] = struct{}{} + } + return pt +} + +func (pt *PackageTest) WithTests() *PackageTest { + pt.withTests = true + return pt +} + +func (pt *PackageTest) ShouldNotDependOn(pkgs ...string) { + expandedPackages := pt.expandPackages(pkgs) + for dep := range pt.findDependencies(pt.pkgs) { + if dep.isDependencyOn(expandedPackages) { + pt.t.Errorf("Error: package dependency not allowed. Dependency chain:\n%s", dep) + } + } +} + +type packageDependency struct { + name string + parent *packageDependency + xTest bool +} + +func (pd *packageDependency) String() string { + result, _ := pd.chain() + return result +} + +func (pd *packageDependency) chain() (string, int) { + name := pd.name + if pd.xTest { + name += "_test" + } + if pd.parent == nil { + return name + "\n", 1 + } + + pc, numberOfTabs := pd.parent.chain() + return pc + strings.Repeat("\t", numberOfTabs) + name + "\n", numberOfTabs + 1 +} + +func (pd *packageDependency) isDependencyOn(pkgs []string) bool { + if pd.parent == nil { + return false + } + return slices.Contains(pkgs, pd.name) +} + +// asXTest marks returns a copy of package dependency marked as external test. +func (pd packageDependency) asXTest() *packageDependency { + pd.xTest = true + return &pd +} + +func (pt PackageTest) findDependencies(pkgs []string) <-chan *packageDependency { + c := make(chan *packageDependency) + go func() { + defer close(c) + + importCache := map[string]struct{}{} + for _, p := range pt.expandPackages(pkgs) { + pt.read(c, &packageDependency{name: p, parent: nil}, importCache) + } + }() + return c +} + +func (pt *PackageTest) read(pChan chan<- *packageDependency, topDependency *packageDependency, cache map[string]struct{}) { + queue := list.New() + queue.PushBack(topDependency) + for queue.Len() > 0 { + front := queue.Front() + queue.Remove(front) + dep, _ := (front.Value).(*packageDependency) + + if pt.skip(cache, dep) { + continue + } + + cache[dep.name] = struct{}{} + pChan <- dep + + pkg, err := build.Default.Import(dep.name, ".", build.ImportMode(0)) + if err != nil { + pt.t.Errorf("Error reading: %s", dep.name) + continue + } + if pkg.Goroot { + continue + } + + for _, importPath := range pkg.Imports { + queue.PushBack(&packageDependency{name: importPath, parent: dep}) + } + + if pt.withTests { + for _, i := range pkg.TestImports { + queue.PushBack(&packageDependency{name: i, parent: dep}) + } + + // XTestImports are packages with _test suffix that are in the same directory as the package. + for _, i := range pkg.XTestImports { + queue.PushBack(&packageDependency{name: i, parent: dep.asXTest()}) + } + } + } +} + +func (pt *PackageTest) skip(cache map[string]struct{}, dep *packageDependency) bool { + if _, seen := cache[dep.name]; seen { + return true + } + + if _, ignore := pt.ignorePkgs[dep.name]; ignore || dep.name == "C" || dep.name == thisPackage { + return true + } + + if pt.includeRegex != nil && !pt.includeRegex.MatchString(dep.name) { + return true + } + return false +} + +func (pt PackageTest) expandPackages(pkgs []string) []string { + if !needExpansion(pkgs) { + return pkgs + } + + loadedPkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName}, pkgs...) + if err != nil { + pt.t.Errorf("Error reading: %s, err: %s", pkgs, err) + return nil + } + if len(loadedPkgs) == 0 { + pt.t.Errorf("Error reading: %s, did not match any packages", pkgs) + return nil + + } + + packagePaths := make([]string, 0, len(loadedPkgs)) + for _, p := range loadedPkgs { + packagePaths = append(packagePaths, p.PkgPath) + } + return packagePaths +} + +func needExpansion(packages []string) bool { + return slices.ContainsFunc(packages, func(p string) bool { + return strings.Contains(p, "...") + }) +} diff --git a/server/archtest/archtest_test.go b/server/archtest/archtest_test.go new file mode 100644 index 000000000000..ddc43c32b361 --- /dev/null +++ b/server/archtest/archtest_test.go @@ -0,0 +1,202 @@ +package archtest + +import ( + "fmt" + "strings" + "testing" +) + +const packagePrefix = "github.com/fleetdm/fleet/v4/server/archtest/test_files/" + +func TestPackage_ShouldNotDependOn(t *testing.T) { + + t.Run("Succeeds on non dependencies", func(t *testing.T) { + mockT := new(testingT) + NewPackageTest(mockT, packagePrefix+"testpackage"). + ShouldNotDependOn(packagePrefix + "nodependency") + + assertNoError(t, mockT) + }) + + t.Run("Fails on dependencies", func(t *testing.T) { + mockT := new(testingT) + NewPackageTest(mockT, packagePrefix+"testpackage"). + ShouldNotDependOn(packagePrefix + "dependency") + + assertError(t, mockT, + packagePrefix+"testpackage", + packagePrefix+"dependency") + }) + + t.Run("Supports testing against packages in the go root", func(t *testing.T) { + mockT := new(testingT) + NewPackageTest(mockT, packagePrefix+"testpackage"). + ShouldNotDependOn("crypto") + + assertError(t, mockT, + packagePrefix+"testpackage", + "crypto") + }) + + t.Run("Fails on transative dependencies", func(t *testing.T) { + mockT := new(testingT) + NewPackageTest(mockT, packagePrefix+"testpackage"). + ShouldNotDependOn(packagePrefix + "transative") + + assertError(t, mockT, + packagePrefix+"testpackage", + packagePrefix+"dependency", + packagePrefix+"transative") + }) + + t.Run("Supports multiple packages at once", func(t *testing.T) { + mockT := new(testingT) + NewPackageTest(mockT, packagePrefix+"dontdependonanything", + packagePrefix+"testpackage"). + ShouldNotDependOn(packagePrefix+"nodependency", + packagePrefix+"dependency") + + assertError(t, mockT, + packagePrefix+"testpackage", + packagePrefix+"dependency") + }) + + t.Run("Supports wildcard matching", func(t *testing.T) { + mockT := new(testingT) + NewPackageTest(mockT, packagePrefix+"..."). + ShouldNotDependOn(packagePrefix + "nodependency") + + assertNoError(t, mockT) + + NewPackageTest(mockT, packagePrefix+"testpackage/nested/..."). + ShouldNotDependOn(packagePrefix + "...") + + assertError(t, mockT, packagePrefix+"testpackage/nested/dep", + packagePrefix+"nesteddependency") + }) + + t.Run("Supports checking imports in test files", func(t *testing.T) { + mockT := new(testingT) + + NewPackageTest(mockT, packagePrefix+"testpackage/..."). + ShouldNotDependOn(packagePrefix + "testfiledeps/testonlydependency") + + assertNoError(t, mockT) + + NewPackageTest(mockT, packagePrefix+"testpackage/..."). + WithTests(). + ShouldNotDependOn(packagePrefix + "testfiledeps/testonlydependency") + + assertError(t, mockT, + packagePrefix+"testpackage/nested/dep", + packagePrefix+"testfiledeps/testonlydependency", + ) + }) + + t.Run("Supports checking imports from test packages", func(t *testing.T) { + mockT := new(testingT) + + NewPackageTest(mockT, packagePrefix+"testpackage/..."). + ShouldNotDependOn(packagePrefix + "testfiledeps/testpkgdependency") + + assertNoError(t, mockT) + + NewPackageTest(mockT, packagePrefix+"testpackage/..."). + WithTests(). + ShouldNotDependOn(packagePrefix + "testfiledeps/testpkgdependency") + + assertError(t, mockT, + packagePrefix+"testpackage/nested/dep_test", + packagePrefix+"testfiledeps/testpkgdependency", + ) + }) + + t.Run("Supports Ignoring packages", func(t *testing.T) { + mockT := new(testingT) + + NewPackageTest(mockT, packagePrefix+"testpackage/nested/dep"). + IgnorePackages(packagePrefix + "testpackage/nested/dep"). + ShouldNotDependOn(packagePrefix + "nesteddependency") + + assertNoError(t, mockT) + }) + + t.Run("Ignored packages ignore ignored transitive packages", func(t *testing.T) { + mockT := new(testingT) + + NewPackageTest(mockT, packagePrefix+"testpackage"). + IgnorePackages("github.com/this/is/verifying/multiple/exclusions", packagePrefix+"..."). + IgnorePackages("github.com/this/is/verifying/chaining"). + ShouldNotDependOn(packagePrefix + "transative") + + assertNoError(t, mockT) + }) + + t.Run("Fails on packages that do not exist", func(t *testing.T) { + mockT := new(testingT) + NewPackageTest(mockT, packagePrefix+"dontexist/sorry"). + ShouldNotDependOn(packagePrefix + "dependency") + + assertError(t, mockT) + + mockT = new(testingT) + NewPackageTest(mockT, "DONT__WORK"). + ShouldNotDependOn(packagePrefix + "dependency") + + assertError(t, mockT) + + mockT = new(testingT) + NewPackageTest(mockT, packagePrefix+"dontexist/..."). + ShouldNotDependOn(packagePrefix + "dependency") + + assertError(t, mockT) + }) +} + +func assertNoError(t *testing.T, mockT *testingT) { + t.Helper() + if mockT.errored() { + t.Fatalf("archtest should not have failed but, %+v", mockT.message()) + } +} + +func assertError(t *testing.T, mockT *testingT, dependencyTrace ...string) { + t.Helper() + if !mockT.errored() { + t.Fatal("archtest did not fail on dependency") + } + + if dependencyTrace == nil { + return + } + + s := strings.Builder{} + s.WriteString("Error: package dependency not allowed. Dependency chain:\n") + for i, v := range dependencyTrace { + s.WriteString(strings.Repeat("\t", i)) + s.WriteString(v + "\n") + } + + if mockT.message() != s.String() { + t.Errorf("expected %s got error message: %s", s.String(), mockT.message()) + } +} + +type testingT struct { + errors [][]interface{} +} + +func (t *testingT) Errorf(format string, args ...any) { + t.errors = append(t.errors, append([]interface{}{format}, args...)) +} + +func (t testingT) errored() bool { + return len(t.errors) != 0 +} + +func (t *testingT) message() string { + if len(t.errors[0]) == 1 { + return t.errors[0][0].(string) + } + return fmt.Sprintf(t.errors[0][0].(string), t.errors[0][1:]...) +} diff --git a/server/archtest/test_files/dependency/dependency.go b/server/archtest/test_files/dependency/dependency.go new file mode 100644 index 000000000000..e25a8a2331eb --- /dev/null +++ b/server/archtest/test_files/dependency/dependency.go @@ -0,0 +1,10 @@ +package dependency + +import "fmt" +import "github.com/fleetdm/fleet/v4/server/archtest/test_files/transative" + +const Item = "depend on me" + +func SomeMethod() { + fmt.Println(transative.NowYouDependOnMe) +} diff --git a/server/archtest/test_files/dontdependonanything/nope.go b/server/archtest/test_files/dontdependonanything/nope.go new file mode 100644 index 000000000000..7a89bf429a66 --- /dev/null +++ b/server/archtest/test_files/dontdependonanything/nope.go @@ -0,0 +1,4 @@ +package dontdependonanything + +func Nothing() { +} diff --git a/server/archtest/test_files/nesteddependency/dependency.go b/server/archtest/test_files/nesteddependency/dependency.go new file mode 100644 index 000000000000..3977d1fc6eec --- /dev/null +++ b/server/archtest/test_files/nesteddependency/dependency.go @@ -0,0 +1,10 @@ +package nesteddependency + +import "fmt" +import "github.com/fleetdm/fleet/v4/server/archtest/test_files/transative" + +const Item = "depend on me" + +func SomeMethod() { + fmt.Println(transative.NowYouDependOnMe) +} diff --git a/server/archtest/test_files/nodependency/nobodywantsme.go b/server/archtest/test_files/nodependency/nobodywantsme.go new file mode 100644 index 000000000000..abc91a2bb19c --- /dev/null +++ b/server/archtest/test_files/nodependency/nobodywantsme.go @@ -0,0 +1,5 @@ +package nodependency + +func SomeGreatOne() { + +} diff --git a/server/archtest/test_files/testfiledeps/testonlydependency/testhelpers.go b/server/archtest/test_files/testfiledeps/testonlydependency/testhelpers.go new file mode 100644 index 000000000000..b50d31366287 --- /dev/null +++ b/server/archtest/test_files/testfiledeps/testonlydependency/testhelpers.go @@ -0,0 +1,5 @@ +package testonlydependency + +func OohNoBadCode() { + +} diff --git a/server/archtest/test_files/testfiledeps/testpkgdependency/testhelpers.go b/server/archtest/test_files/testfiledeps/testpkgdependency/testhelpers.go new file mode 100644 index 000000000000..de9573783543 --- /dev/null +++ b/server/archtest/test_files/testfiledeps/testpkgdependency/testhelpers.go @@ -0,0 +1,5 @@ +package testpkgdependency + +func OohNoBadCode() { + +} diff --git a/server/archtest/test_files/testpackage/nested/dep/depends.go b/server/archtest/test_files/testpackage/nested/dep/depends.go new file mode 100644 index 000000000000..82f21c4aa89c --- /dev/null +++ b/server/archtest/test_files/testpackage/nested/dep/depends.go @@ -0,0 +1,11 @@ +package dep + +import ( + "fmt" + + "github.com/fleetdm/fleet/v4/server/archtest/test_files/nesteddependency" +) + +func init() { + fmt.Println(nesteddependency.Item) +} diff --git a/server/archtest/test_files/testpackage/nested/dep/depends_seperate_pkg_test.go b/server/archtest/test_files/testpackage/nested/dep/depends_seperate_pkg_test.go new file mode 100644 index 000000000000..8f08b70861b4 --- /dev/null +++ b/server/archtest/test_files/testpackage/nested/dep/depends_seperate_pkg_test.go @@ -0,0 +1,11 @@ +package dep_test + +import ( + "testing" + + "github.com/fleetdm/fleet/v4/server/archtest/test_files/testfiledeps/testpkgdependency" +) + +func Test(t *testing.T) { + testpkgdependency.OohNoBadCode() +} diff --git a/server/archtest/test_files/testpackage/nested/dep/depends_test.go b/server/archtest/test_files/testpackage/nested/dep/depends_test.go new file mode 100644 index 000000000000..480026b3fb34 --- /dev/null +++ b/server/archtest/test_files/testpackage/nested/dep/depends_test.go @@ -0,0 +1,11 @@ +package dep + +import ( + "testing" + + "github.com/fleetdm/fleet/v4/server/archtest/test_files/testfiledeps/testonlydependency" +) + +func TestDoIBreakYou(t *testing.T) { + testonlydependency.OohNoBadCode() +} diff --git a/server/archtest/test_files/testpackage/testpackage.go b/server/archtest/test_files/testpackage/testpackage.go new file mode 100644 index 000000000000..edd93f13c2f0 --- /dev/null +++ b/server/archtest/test_files/testpackage/testpackage.go @@ -0,0 +1,12 @@ +package testpackage + +import ( + "crypto" // for test + "fmt" + + "github.com/fleetdm/fleet/v4/server/archtest/test_files/dependency" +) + +func What(_ crypto.Decrypter) { + fmt.Println(dependency.Item) +} diff --git a/server/archtest/test_files/transative/dependency.go b/server/archtest/test_files/transative/dependency.go new file mode 100644 index 000000000000..348bd5460950 --- /dev/null +++ b/server/archtest/test_files/transative/dependency.go @@ -0,0 +1,3 @@ +package transative + +const NowYouDependOnMe string = "careful" diff --git a/server/datastore/mysql/migrations/tables/20250213104005_AddAndroidEnterpriseTable.go b/server/datastore/mysql/migrations/tables/20250213104005_AddAndroidEnterpriseTable.go new file mode 100644 index 000000000000..600fc325804a --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20250213104005_AddAndroidEnterpriseTable.go @@ -0,0 +1,27 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20250213104005, Down_20250213104005) +} + +func Up_20250213104005(tx *sql.Tx) error { + _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS android_enterprises ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + signup_name VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + enterprise_id VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + created_at DATETIME(6) NULL DEFAULT NOW(6), + updated_at DATETIME(6) NULL DEFAULT NOW(6) ON UPDATE NOW(6))`) + if err != nil { + return fmt.Errorf("failed to create android_enterprise table: %w", err) + } + return nil +} + +func Down_20250213104005(_ *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20250214205657_AddCVEIndex.go b/server/datastore/mysql/migrations/tables/20250214205657_AddCVEIndex.go new file mode 100644 index 000000000000..9a771c8dede1 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20250214205657_AddCVEIndex.go @@ -0,0 +1,22 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20250214205657, Down_20250214205657) +} + +func Up_20250214205657(tx *sql.Tx) error { + _, err := tx.Exec(`ALTER TABLE software_cve ADD INDEX idx_software_cve_cve (cve);`) + if err != nil { + return fmt.Errorf("failed to add index to software_cve.cve: %w", err) + } + return nil +} + +func Down_20250214205657(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/mysql.go b/server/datastore/mysql/mysql.go index 8c36d1116919..cf6e06e44034 100644 --- a/server/datastore/mysql/mysql.go +++ b/server/datastore/mysql/mysql.go @@ -26,6 +26,8 @@ import ( "github.com/fleetdm/fleet/v4/server/datastore/mysql/migrations/tables" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/goose" + "github.com/fleetdm/fleet/v4/server/mdm/android" + android_mysql "github.com/fleetdm/fleet/v4/server/mdm/android/mysql" nano_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" scep_depot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" "github.com/go-kit/log" @@ -291,6 +293,11 @@ func New(config config.MysqlConfig, c clock.Clock, opts ...DBOption) (*Datastore return ds, nil } +func NewAndroidDS(ds fleet.Datastore) android.Datastore { + mysqlDs := ds.(*Datastore) + return android_mysql.New(mysqlDs.logger, mysqlDs.primary, mysqlDs.replica) +} + type itemToWrite struct { ctx context.Context errCh chan error diff --git a/server/datastore/mysql/operating_systems.go b/server/datastore/mysql/operating_systems.go index 73160b28e468..e5dbf94d03f8 100644 --- a/server/datastore/mysql/operating_systems.go +++ b/server/datastore/mysql/operating_systems.go @@ -46,6 +46,11 @@ func (ds *Datastore) UpdateHostOperatingSystem(ctx context.Context, hostID uint, if !updateNeeded { return nil } + + const maxDisplayVersionLength = 10 // per DB schema + if len(hostOS.DisplayVersion) > maxDisplayVersionLength { + return ctxerr.Errorf(ctx, "host OS display version too long: %s", hostOS.DisplayVersion) + } return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { os, err := getOrGenerateOperatingSystemDB(ctx, tx, hostOS) if err != nil { diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 9469c3fb2f8c..5013fa61399b 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -56,6 +56,17 @@ CREATE TABLE `aggregated_stats` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `android_enterprises` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `signup_name` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `enterprise_id` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `created_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `app_config_json` ( `id` int unsigned NOT NULL DEFAULT '1', `json_value` json NOT NULL, @@ -1128,9 +1139,9 @@ CREATE TABLE `migration_status_tables` ( `is_applied` tinyint(1) NOT NULL, `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=352 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=354 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'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,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'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,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` ( @@ -1820,7 +1831,8 @@ CREATE TABLE `software_cve` ( `software_id` bigint unsigned DEFAULT NULL, `resolved_in_version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `unq_software_id_cve` (`software_id`,`cve`) + UNIQUE KEY `unq_software_id_cve` (`software_id`,`cve`), + KEY `idx_software_cve_cve` (`cve`) ) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index 227597a2586f..00f7cb5891c2 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -2592,7 +2592,10 @@ last_vpp_install AS ( SELECT COUNT(*) AS count_installer_labels, COUNT(lm.label_id) AS count_host_labels, - SUM(CASE WHEN lbl.created_at IS NOT NULL AND :host_label_updated_at >= lbl.created_at THEN 1 ELSE 0 END) as count_host_updated_after_labels + SUM(CASE + WHEN lbl.created_at IS NOT NULL AND lbl.label_membership_type = 0 AND :host_label_updated_at >= lbl.created_at THEN 1 + WHEN lbl.created_at IS NOT NULL AND lbl.label_membership_type = 1 THEN 1 + ELSE 0 END) as count_host_updated_after_labels FROM vpp_app_team_labels vatl LEFT OUTER JOIN labels lbl @@ -2775,7 +2778,10 @@ last_vpp_install AS ( SELECT COUNT(*) AS count_installer_labels, COUNT(lm.label_id) AS count_host_labels, - SUM(CASE WHEN lbl.created_at IS NOT NULL AND :host_label_updated_at >= lbl.created_at THEN 1 ELSE 0 END) as count_host_updated_after_labels + SUM(CASE + WHEN lbl.created_at IS NOT NULL AND lbl.label_membership_type = 0 AND :host_label_updated_at >= lbl.created_at THEN 1 + WHEN lbl.created_at IS NOT NULL AND lbl.label_membership_type = 1 THEN 1 + ELSE 0 END) as count_host_updated_after_labels FROM vpp_app_team_labels vatl LEFT OUTER JOIN labels lbl diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index a5f570ccb132..9ca46aec75f6 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -2144,7 +2144,9 @@ func (ds *Datastore) isSoftwareLabelScoped(ctx context.Context, softwareID, host COUNT(lm.label_id) AS count_host_labels, SUM(CASE WHEN - lbl.created_at IS NOT NULL AND (SELECT label_updated_at FROM hosts WHERE id = :host_id) >= lbl.created_at THEN 1 + lbl.created_at IS NOT NULL AND lbl.label_membership_type = 0 AND (SELECT label_updated_at FROM hosts WHERE id = :host_id) >= lbl.created_at THEN 1 + WHEN + lbl.created_at IS NOT NULL AND lbl.label_membership_type = 1 THEN 1 ELSE 0 END) as count_host_updated_after_labels @@ -2223,21 +2225,14 @@ FROM ( COUNT(*) AS count_installer_labels, COUNT(lm.label_id) AS count_host_labels, SUM( - CASE WHEN lbl.created_at IS NOT NULL - AND( - SELECT - label_updated_at FROM hosts - WHERE - id = h.id) >= lbl.created_at THEN - 1 - ELSE - 0 - END) AS count_host_updated_after_labels + CASE + WHEN lbl.created_at IS NOT NULL AND lbl.label_membership_type = 0 AND (SELECT label_updated_at FROM hosts WHERE id = h.id) >= lbl.created_at THEN 1 + WHEN lbl.created_at IS NOT NULL AND lbl.label_membership_type = 1 THEN 1 + ELSE 0 END) AS count_host_updated_after_labels FROM %[1]s_labels sil LEFT OUTER JOIN labels lbl ON lbl.id = sil.label_id - LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id - AND lm.host_id = h.id + LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id AND lm.host_id = h.id WHERE sil.%[1]s_id = ? AND sil.exclude = 1 diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go index daa80d5d3f6c..1176fbc0beef 100644 --- a/server/datastore/mysql/software_test.go +++ b/server/datastore/mysql/software_test.go @@ -5686,7 +5686,7 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) { } // Add a new label and apply it to the installer. There are no hosts with this label. - label4, err := ds.NewLabel(ctx, &fleet.Label{Name: "label4" + t.Name()}) + label4, err := ds.NewLabel(ctx, &fleet.Label{Name: "label4" + t.Name(), LabelMembershipType: fleet.LabelMembershipTypeDynamic}) require.NoError(t, err) err = setOrUpdateSoftwareInstallerLabelsDB(ctx, ds.writer(ctx), installerID3, fleet.LabelIdentsWithScope{ @@ -5706,6 +5706,11 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) { require.NoError(t, err) require.True(t, scoped) + // TODO(JVE): dump labels table and check type + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + DumpTable(t, q, "labels") + return nil + }) // installer3 is not in scope yet, because label is "exclude any" and host doesn't have results scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID3, host.ID) require.NoError(t, err) diff --git a/server/datastore/mysql/vpp.go b/server/datastore/mysql/vpp.go index 138c30731a12..26afbc88bd10 100644 --- a/server/datastore/mysql/vpp.go +++ b/server/datastore/mysql/vpp.go @@ -301,7 +301,7 @@ func (ds *Datastore) getExistingLabels(ctx context.Context, vppAppTeamID uint) ( return &labels, nil case len(inclAny) > 0: - labels.LabelScope = fleet.LabelScopeExcludeAny + labels.LabelScope = fleet.LabelScopeIncludeAny labels.ByName = make(map[string]fleet.LabelIdent, len(inclAny)) for _, l := range inclAny { labels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID} @@ -523,6 +523,12 @@ WHERE return appSet, nil } +func (ds *Datastore) InsertVPPApps(ctx context.Context, apps []*fleet.VPPApp) error { + return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { + return insertVPPApps(ctx, tx, apps) + }) +} + func insertVPPApps(ctx context.Context, tx sqlx.ExtContext, apps []*fleet.VPPApp) error { stmt := ` INSERT INTO vpp_apps @@ -1623,3 +1629,23 @@ func (ds *Datastore) GetIncludedHostIDMapForVPPAppTx(ctx context.Context, tx sql func (ds *Datastore) GetExcludedHostIDMapForVPPApp(ctx context.Context, vppAppTeamID uint) (map[uint]struct{}, error) { return ds.getExcludedHostIDMapForSoftware(ctx, vppAppTeamID, softwareTypeVPP) } + +func (ds *Datastore) GetAllVPPApps(ctx context.Context) ([]*fleet.VPPApp, error) { + query := ` +SELECT + adam_id, + title_id, + bundle_identifier, + icon_url, + name, + latest_version, + platform +FROM vpp_apps` + + var apps []*fleet.VPPApp + if err := sqlx.SelectContext(ctx, ds.reader(ctx), &apps, query); err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting all VPP apps") + } + + return apps, nil +} diff --git a/server/datastore/mysql/vpp_test.go b/server/datastore/mysql/vpp_test.go index 777dfe11ce60..a597dbe3f893 100644 --- a/server/datastore/mysql/vpp_test.go +++ b/server/datastore/mysql/vpp_test.go @@ -36,6 +36,7 @@ func TestVPP(t *testing.T) { {"GetOrInsertSoftwareTitleForVPPApp", testGetOrInsertSoftwareTitleForVPPApp}, {"DeleteVPPAssignedToPolicy", testDeleteVPPAssignedToPolicy}, {"TestVPPTokenTeamAssignment", testVPPTokenTeamAssignment}, + {"TestGetAllVPPApps", testGetAllVPPApps}, } for _, c := range cases { @@ -1829,4 +1830,95 @@ func testSetTeamVPPAppsWithLabels(t *testing.T, ds *Datastore) { _, ok := app2.VPPAppTeam.ValidatedLabels.ByName[l.LabelName] require.True(t, ok) } + + // switch label types + app1.VPPAppTeam = fleet.VPPAppTeam{VPPAppID: app1.VPPAppID, ValidatedLabels: &fleet.LabelIdentsWithScope{ + LabelScope: fleet.LabelScopeExcludeAny, + ByName: map[string]fleet.LabelIdent{ + label1.Name: { + LabelID: label1.ID, + LabelName: label1.Name, + }, + label2.Name: { + LabelID: label2.ID, + LabelName: label2.Name, + }, + }, + }} + + app2.VPPAppTeam = fleet.VPPAppTeam{VPPAppID: app2.VPPAppID, ValidatedLabels: &fleet.LabelIdentsWithScope{ + LabelScope: fleet.LabelScopeIncludeAny, + ByName: map[string]fleet.LabelIdent{ + label1.Name: { + LabelID: label1.ID, + LabelName: label1.Name, + }, + label2.Name: { + LabelID: label2.ID, + LabelName: label2.Name, + }, + }, + }} + + err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{ + app1.VPPAppTeam, + app2.VPPAppTeam, + }) + require.NoError(t, err) + + assigned, err = ds.GetAssignedVPPApps(ctx, &team.ID) + require.NoError(t, err) + require.Len(t, assigned, 2) + + app1Meta, err = ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team.ID, app1.TitleID) + require.NoError(t, err) + + app2Meta, err = ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team.ID, app2.TitleID) + require.NoError(t, err) + + require.Len(t, app1Meta.LabelsIncludeAny, 0) + require.Len(t, app1Meta.LabelsExcludeAny, 2) + for _, l := range app1Meta.LabelsExcludeAny { + _, ok := app1.VPPAppTeam.ValidatedLabels.ByName[l.LabelName] + require.True(t, ok) + } + + require.Len(t, app2Meta.LabelsExcludeAny, 0) + require.Len(t, app2Meta.LabelsIncludeAny, 2) + for _, l := range app2Meta.LabelsIncludeAny { + _, ok := app2.VPPAppTeam.ValidatedLabels.ByName[l.LabelName] + require.True(t, ok) + } +} + +func testGetAllVPPApps(t *testing.T, ds *Datastore) { + ctx := context.Background() + dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Org"+t.Name(), "Location"+t.Name()) + require.NoError(t, err) + tok1, err := ds.InsertVPPToken(ctx, dataToken) + assert.NoError(t, err) + _, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{}) + assert.NoError(t, err) + + app1 := &fleet.VPPApp{Name: "vpp_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b1"} + _, err = ds.InsertVPPAppWithTeam(ctx, app1, nil) + require.NoError(t, err) + + app2 := &fleet.VPPApp{Name: "vpp_app_2", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "2", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b2"} + _, err = ds.InsertVPPAppWithTeam(ctx, app2, nil) + require.NoError(t, err) + + app3 := &fleet.VPPApp{Name: "vpp_app_3", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "3", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b3"} + _, err = ds.InsertVPPAppWithTeam(ctx, app3, nil) + require.NoError(t, err) + + // this method doesn't pull the VPPAppTeamID + app1.AppTeamID = 0 + app2.AppTeamID = 0 + app3.AppTeamID = 0 + + apps, err := ds.GetAllVPPApps(ctx) + require.NoError(t, err) + + require.Equal(t, apps, []*fleet.VPPApp{app1, app2, app3}) } diff --git a/server/fleet/cron_schedules.go b/server/fleet/cron_schedules.go index b4a4f7afc8d7..6275ab417622 100644 --- a/server/fleet/cron_schedules.go +++ b/server/fleet/cron_schedules.go @@ -28,6 +28,9 @@ const ( CronCalendar CronScheduleName = "calendar" CronUninstallSoftwareMigration CronScheduleName = "uninstall_software_migration" CronMaintainedApps CronScheduleName = "maintained_apps" + // CronRefreshVPPAppVersions updates the versions of VPP apps in Fleet to the latest value. Runs + // every 1h. + CronRefreshVPPAppVersions CronScheduleName = "refresh_vpp_app_versions" ) type CronSchedulesService interface { diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 6477923e313b..2fc8f26a3c06 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1840,6 +1840,12 @@ type Datastore interface { SetTeamVPPApps(ctx context.Context, teamID *uint, appIDs []VPPAppTeam) error InsertVPPAppWithTeam(ctx context.Context, app *VPPApp, teamID *uint) (*VPPApp, error) + // GetAllVPPApps returns all the VPP apps in Fleet, across all teams. + GetAllVPPApps(ctx context.Context) ([]*VPPApp, error) + // InsertVPPApps inserts the given VPP apps in the database. + InsertVPPApps(ctx context.Context, apps []*VPPApp) error + + // InsertHostVPPSoftwareInstall(ctx context.Context, hostID uint, appID VPPAppID, commandUUID, associatedEventID string, selfService bool, policyID *uint) error InsertHostVPPSoftwareInstall(ctx context.Context, hostID uint, appID VPPAppID, commandUUID, associatedEventID string, opts HostSoftwareInstallOptions) error GetPastActivityDataForVPPAppInstall(ctx context.Context, commandResults *mdm.CommandResults) (*User, *ActivityInstalledAppStoreApp, error) diff --git a/server/fleet/errors.go b/server/fleet/errors.go index 5e6e024f2282..f04ceba4aa5a 100644 --- a/server/fleet/errors.go +++ b/server/fleet/errors.go @@ -643,3 +643,8 @@ func (e ConflictError) Error() string { func (e ConflictError) StatusCode() int { return http.StatusConflict } + +// Errorer interface is implemented by response structs to encode business logic errors +type Errorer interface { + Error() error +} diff --git a/server/mdm/android/android.go b/server/mdm/android/android.go new file mode 100644 index 000000000000..f55b93d3c248 --- /dev/null +++ b/server/mdm/android/android.go @@ -0,0 +1,30 @@ +package android + +type SignupDetails struct { + Url string `json:"url,omitempty"` + Name string `json:"name,omitempty"` +} + +type Enterprise struct { + ID uint `db:"id"` + SignupName string `db:"signup_name"` + EnterpriseID string `db:"enterprise_id"` +} + +func (e Enterprise) Name() string { + return "enterprises/" + e.EnterpriseID +} + +func (e Enterprise) IsValid() bool { + return e.EnterpriseID != "" +} + +type EnrollmentToken struct { + Value string `json:"value"` +} + +type Host struct { + HostID uint `db:"host_id"` + FleetEnterpriseID uint `db:"enterprise_id"` + DeviceID string `db:"device_id"` +} diff --git a/server/mdm/android/arch_test.go b/server/mdm/android/arch_test.go new file mode 100644 index 000000000000..bd64c83c2647 --- /dev/null +++ b/server/mdm/android/arch_test.go @@ -0,0 +1,37 @@ +package android_test + +import ( + "regexp" + "testing" + + "github.com/fleetdm/fleet/v4/server/archtest" +) + +// TestAllAndroidPackageDependencies checks that android packages are not dependent on other Fleet packages +// to maintain decoupling and modularity. +func TestAllAndroidPackageDependencies(t *testing.T) { + t.Parallel() + archtest.NewPackageTest(t, "github.com/fleetdm/fleet/v4/server/mdm/android..."). + OnlyInclude(regexp.MustCompile(`^github\.com/fleetdm/`)). + IgnorePackages( + "github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql", + "github.com/fleetdm/fleet/v4/server/service/externalsvc", // TODO(#26218): remove this dependency on Jira and Zendesk + "github.com/fleetdm/fleet/v4/server/service/middleware/auth", + "github.com/fleetdm/fleet/v4/server/service/middleware/authzcheck", + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils", + "github.com/fleetdm/fleet/v4/server/service/middleware/ratelimit", + ). + ShouldNotDependOn( + "github.com/fleetdm/fleet/v4/server/service...", + "github.com/fleetdm/fleet/v4/server/datastore...", + ) +} + +// TestAndroidPackageDependencies checks that android package is NOT dependent on ANY other Fleet packages +// to maintain decoupling and modularity. +func TestAndroidPackageDependencies(t *testing.T) { + t.Parallel() + archtest.NewPackageTest(t, "github.com/fleetdm/fleet/v4/server/mdm/android"). + OnlyInclude(regexp.MustCompile(`^github\.com/fleetdm/`)). + ShouldNotDependOn("github.com/fleetdm/fleet/v4/...") +} diff --git a/server/mdm/android/datastore.go b/server/mdm/android/datastore.go new file mode 100644 index 000000000000..4a30cfc40479 --- /dev/null +++ b/server/mdm/android/datastore.go @@ -0,0 +1,12 @@ +package android + +import ( + "context" +) + +type Datastore interface { + CreateEnterprise(ctx context.Context) (uint, error) + GetEnterpriseByID(ctx context.Context, ID uint) (*Enterprise, error) + UpdateEnterprise(ctx context.Context, enterprise *Enterprise) error + ListEnterprises(ctx context.Context) ([]*Enterprise, error) +} diff --git a/server/mdm/android/mysql/enterprises.go b/server/mdm/android/mysql/enterprises.go new file mode 100644 index 000000000000..2525b4758a0d --- /dev/null +++ b/server/mdm/android/mysql/enterprises.go @@ -0,0 +1,62 @@ +package mysql + +import ( + "context" + "database/sql" + "errors" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/mdm/android" + "github.com/jmoiron/sqlx" +) + +func (ds *Datastore) CreateEnterprise(ctx context.Context) (uint, error) { + stmt := `INSERT INTO android_enterprises (signup_name) VALUES ('')` + res, err := ds.Writer(ctx).ExecContext(ctx, stmt) + if err != nil { + return 0, ctxerr.Wrap(ctx, err, "inserting enterprise") + } + id, _ := res.LastInsertId() + return uint(id), nil // nolint:gosec // dismiss G115 +} + +func (ds *Datastore) GetEnterpriseByID(ctx context.Context, id uint) (*android.Enterprise, error) { + stmt := `SELECT id, signup_name, enterprise_id FROM android_enterprises WHERE id = ?` + var enterprise android.Enterprise + err := sqlx.GetContext(ctx, ds.reader(ctx), &enterprise, stmt, id) + switch { + case errors.Is(err, sql.ErrNoRows): + return nil, notFound("Android enterprise").WithID(id) + case err != nil: + return nil, ctxerr.Wrap(ctx, err, "selecting enterprise") + } + return &enterprise, nil +} + +func (ds *Datastore) UpdateEnterprise(ctx context.Context, enterprise *android.Enterprise) error { + if enterprise == nil || enterprise.ID == 0 { + return errors.New("missing enterprise ID") + } + stmt := `UPDATE android_enterprises + SET signup_name = ?, + enterprise_id = ? + WHERE id = ?` + res, err := ds.Writer(ctx).ExecContext(ctx, stmt, enterprise.SignupName, enterprise.EnterpriseID, enterprise.ID) + if err != nil { + return ctxerr.Wrap(ctx, err, "inserting enterprise") + } + if rows, _ := res.RowsAffected(); rows == 0 { + return notFound("Android enterprise").WithID(enterprise.ID) + } + return nil +} + +func (ds *Datastore) ListEnterprises(ctx context.Context) ([]*android.Enterprise, error) { + stmt := `SELECT id, signup_name, enterprise_id FROM android_enterprises` + var enterprises []*android.Enterprise + err := sqlx.SelectContext(ctx, ds.reader(ctx), &enterprises, stmt) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "selecting enterprises") + } + return enterprises, nil +} diff --git a/server/mdm/android/mysql/enterprises_test.go b/server/mdm/android/mysql/enterprises_test.go new file mode 100644 index 000000000000..4184a50d07ba --- /dev/null +++ b/server/mdm/android/mysql/enterprises_test.go @@ -0,0 +1,76 @@ +package mysql_test + +import ( + "context" + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/android" + "github.com/fleetdm/fleet/v4/server/mdm/android/mysql" + "github.com/fleetdm/fleet/v4/server/mdm/android/mysql/testing_utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEnterprise(t *testing.T) { + ds := testing_utils.CreateMySQLDS(t) + + cases := []struct { + name string + fn func(t *testing.T, ds *mysql.Datastore) + }{ + {"CreateGetEnterprise", testCreateGetEnterprise}, + {"UpdateEnterprise", testUpdateEnterprise}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + defer testing_utils.TruncateTables(t, ds) + + c.fn(t, ds) + }) + } +} + +func testCreateGetEnterprise(t *testing.T, ds *mysql.Datastore) { + _, err := ds.GetEnterpriseByID(testCtx(), 9999) + assert.True(t, fleet.IsNotFound(err)) + + id, err := ds.CreateEnterprise(testCtx()) + require.NoError(t, err) + assert.NotZero(t, id) + + result, err := ds.GetEnterpriseByID(testCtx(), id) + require.NoError(t, err) + assert.Equal(t, &android.Enterprise{ID: id}, result) +} + +func testUpdateEnterprise(t *testing.T, ds *mysql.Datastore) { + enterprise := &android.Enterprise{ + ID: 9999, // start with an invalid ID + SignupName: "signupUrls/C97372c91c6a85139", + EnterpriseID: "LC04bp524j", + } + err := ds.UpdateEnterprise(testCtx(), enterprise) + assert.Error(t, err) + + id, err := ds.CreateEnterprise(testCtx()) + require.NoError(t, err) + assert.NotZero(t, id) + + enterprise.ID = id + err = ds.UpdateEnterprise(testCtx(), enterprise) + require.NoError(t, err) + + result, err := ds.GetEnterpriseByID(testCtx(), enterprise.ID) + require.NoError(t, err) + assert.Equal(t, enterprise, result) + + enterprises, err := ds.ListEnterprises(testCtx()) + require.NoError(t, err) + assert.Len(t, enterprises, 1) + assert.Equal(t, enterprise, enterprises[0]) +} + +func testCtx() context.Context { + return context.Background() +} diff --git a/server/mdm/android/mysql/errors.go b/server/mdm/android/mysql/errors.go new file mode 100644 index 000000000000..a67a4f3ad0f4 --- /dev/null +++ b/server/mdm/android/mysql/errors.go @@ -0,0 +1,60 @@ +package mysql + +// TODO(26218): Refactor this to common_mysql + +import ( + "database/sql" + "fmt" +) + +type notFoundError struct { + ID uint + Name string + Message string + ResourceType string +} + +func notFound(kind string) *notFoundError { + return ¬FoundError{ + ResourceType: kind, + } +} + +func (e *notFoundError) Error() string { + if e.ID != 0 { + return fmt.Sprintf("%s %d was not found in the datastore", e.ResourceType, e.ID) + } + if e.Name != "" { + return fmt.Sprintf("%s %s was not found in the datastore", e.ResourceType, e.Name) + } + if e.Message != "" { + return fmt.Sprintf("%s %s was not found in the datastore", e.ResourceType, e.Message) + } + return fmt.Sprintf("%s was not found in the datastore", e.ResourceType) +} + +func (e *notFoundError) WithID(id uint) error { + e.ID = id + return e +} + +func (e *notFoundError) WithName(name string) error { + e.Name = name + return e +} + +func (e *notFoundError) WithMessage(msg string) error { + e.Message = msg + return e +} + +func (e *notFoundError) IsNotFound() bool { + return true +} + +// Is helps so that errors.Is(err, sql.ErrNoRows) returns true for an +// error of type *notFoundError, without having to wrap sql.ErrNoRows +// explicitly. +func (e *notFoundError) Is(other error) bool { + return other == sql.ErrNoRows +} diff --git a/server/mdm/android/mysql/mysql.go b/server/mdm/android/mysql/mysql.go new file mode 100644 index 000000000000..8abe51e20600 --- /dev/null +++ b/server/mdm/android/mysql/mysql.go @@ -0,0 +1,49 @@ +// Package mysql is a MySQL implementation of the android.Datastore interface. +package mysql + +import ( + "context" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxdb" + "github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/android" + "github.com/go-kit/log" + "github.com/jmoiron/sqlx" +) + +// Datastore is an implementation of android.Datastore interface backed by MySQL +type Datastore struct { + logger log.Logger + primary *sqlx.DB + replica fleet.DBReader // so it cannot be used to perform writes +} + +// New creates a new Datastore +func New(logger log.Logger, primary *sqlx.DB, replica fleet.DBReader) android.Datastore { + return &Datastore{ + logger: logger, + primary: primary, + replica: replica, + } +} + +// reader returns the DB instance to use for read-only statements, which is the +// replica unless the primary has been explicitly required via +// ctxdb.RequirePrimary. +func (ds *Datastore) reader(ctx context.Context) fleet.DBReader { + if ctxdb.IsPrimaryRequired(ctx) { + return ds.primary + } + return ds.replica +} + +// Writer returns the DB instance to use for write statements, which is always +// the primary. +func (ds *Datastore) Writer(_ context.Context) *sqlx.DB { + return ds.primary +} + +func (ds *Datastore) WithRetryTxx(ctx context.Context, fn common_mysql.TxFn) (err error) { + return common_mysql.WithRetryTxx(ctx, ds.Writer(ctx), fn, ds.logger) +} diff --git a/server/mdm/android/mysql/schema.sql b/server/mdm/android/mysql/schema.sql new file mode 100644 index 000000000000..8340d0283c76 --- /dev/null +++ b/server/mdm/android/mysql/schema.sql @@ -0,0 +1,14 @@ +/* + TODO(26218): Generate this file + */ + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `android_enterprises` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `signup_name` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `enterprise_id` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `created_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/server/mdm/android/mysql/testing_utils/testing_utils.go b/server/mdm/android/mysql/testing_utils/testing_utils.go new file mode 100644 index 000000000000..52bb19e40d37 --- /dev/null +++ b/server/mdm/android/mysql/testing_utils/testing_utils.go @@ -0,0 +1,308 @@ +package testing_utils + +// TODO(26218): Refactor this to remove duplication. + +import ( + "context" + "fmt" + "net/url" + "os" + "os/exec" + "path" + "runtime" + "strings" + "testing" + "time" + + "github.com/fleetdm/fleet/v4/server/config" + "github.com/fleetdm/fleet/v4/server/mdm/android/mysql" + "github.com/go-kit/log" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +const ( + testUsername = "root" + testPassword = "toor" + testAddress = "localhost:3307" + testReplicaDatabaseSuffix = "_replica" +) + +func connectMySQL(t testing.TB, testName string) *mysql.Datastore { + cfg := config.MysqlConfig{ + Username: testUsername, + Password: testPassword, + Database: testName, + Address: testAddress, + } + + dbWriter, err := newDB(&cfg) + require.NoError(t, err) + ds := mysql.New(log.NewLogfmtLogger(os.Stdout), dbWriter, dbWriter) + return ds.(*mysql.Datastore) +} + +func newDB(conf *config.MysqlConfig) (*sqlx.DB, error) { + driverName := "mysql" + + dsn := generateMysqlConnectionString(*conf) + db, err := sqlx.Open(driverName, dsn) + if err != nil { + return nil, err + } + + db.SetMaxIdleConns(conf.MaxIdleConns) + db.SetMaxOpenConns(conf.MaxOpenConns) + db.SetConnMaxLifetime(time.Second * time.Duration(conf.ConnMaxLifetime)) + + var dbError error + maxConnectionAttempts := 10 + for attempt := 0; attempt < maxConnectionAttempts; attempt++ { + dbError = db.Ping() + if dbError == nil { + // we're connected! + break + } + interval := time.Duration(attempt) * time.Second + fmt.Printf("could not connect to db: %v, sleeping %v\n", dbError, interval) + time.Sleep(interval) + } + + if dbError != nil { + return nil, dbError + } + return db, nil +} + +// generateMysqlConnectionString returns a MySQL connection string using the +// provided configuration. +func generateMysqlConnectionString(conf config.MysqlConfig) string { + params := url.Values{ + // using collation implicitly sets the charset too + // and it's the recommended way to do it per the + // driver documentation: + // https://github.com/go-sql-driver/mysql#charset + "collation": []string{"utf8mb4_unicode_ci"}, + "parseTime": []string{"true"}, + "loc": []string{"UTC"}, + "time_zone": []string{"'-00:00'"}, + "clientFoundRows": []string{"true"}, + "allowNativePasswords": []string{"true"}, + "group_concat_max_len": []string{"4194304"}, + "multiStatements": []string{"true"}, + } + if conf.TLSConfig != "" { + params.Set("tls", conf.TLSConfig) + } + if conf.SQLMode != "" { + params.Set("sql_mode", conf.SQLMode) + } + + dsn := fmt.Sprintf( + "%s:%s@%s(%s)/%s?%s", + conf.Username, + conf.Password, + conf.Protocol, + conf.Address, + conf.Database, + params.Encode(), + ) + + return dsn +} + +// initializeDatabase loads the dumped schema into a newly created database in +// MySQL. This is much faster than running the full set of migrations on each +// test. +func initializeDatabase(t testing.TB, testName string, opts *DatastoreTestOptions) *mysql.Datastore { + _, filename, _, _ := runtime.Caller(0) + base := path.Dir(filename) + schema, err := os.ReadFile(path.Join(base, "../schema.sql")) + if err != nil { + t.Error(err) + t.FailNow() + } + + // execute the schema for the test db, and once more for the replica db if + // that option is set. + dbs := []string{testName} + if opts.DummyReplica { + dbs = append(dbs, testName+testReplicaDatabaseSuffix) + } + for _, dbName := range dbs { + // Load schema from dumpfile + sqlCommands := fmt.Sprintf( + "DROP DATABASE IF EXISTS %s; CREATE DATABASE %s; USE %s; SET FOREIGN_KEY_CHECKS=0; %s;", + dbName, dbName, dbName, schema, + ) + + cmd := exec.Command( + "docker", "compose", "exec", "-T", "mysql_test", + // Command run inside container + "mysql", + "-u"+testUsername, "-p"+testPassword, + ) + cmd.Stdin = strings.NewReader(sqlCommands) + out, err := cmd.CombinedOutput() + if err != nil { + t.Error(err) + t.Error(string(out)) + t.FailNow() + } + } + if opts.RealReplica { + // Load schema from dumpfile + sqlCommands := fmt.Sprintf( + "DROP DATABASE IF EXISTS %s; CREATE DATABASE %s; USE %s; SET FOREIGN_KEY_CHECKS=0; %s;", + testName, testName, testName, schema, + ) + + cmd := exec.Command( + "docker", "compose", "exec", "-T", "mysql_replica_test", + // Command run inside container + "mysql", + "-u"+testUsername, "-p"+testPassword, + ) + cmd.Stdin = strings.NewReader(sqlCommands) + out, err := cmd.CombinedOutput() + if err != nil { + t.Error(err) + t.Error(string(out)) + t.FailNow() + } + } + + return connectMySQL(t, testName) +} + +// DatastoreTestOptions configures how the test datastore is created +// by CreateMySQLDSWithOptions. +type DatastoreTestOptions struct { + // DummyReplica indicates that a read replica test database should be created. + DummyReplica bool + + // RunReplication is the function to call to execute the replication of all + // missing changes from the primary to the replica. The function is created + // and set automatically by CreateMySQLDSWithOptions. The test is in full + // control of when the replication is executed. Only applies to DummyReplica. + // Note that not all changes to data show up in the information_schema + // update_time timestamp, so to work around that limitation, explicit table + // names can be provided to force their replication. + RunReplication func(forceTables ...string) + + // RealReplica indicates that the replica should be a real DB replica, with a dedicated connection. + RealReplica bool +} + +func createMySQLDSWithOptions(t testing.TB, opts *DatastoreTestOptions) *mysql.Datastore { + if _, ok := os.LookupEnv("MYSQL_TEST"); !ok { + t.Skip("MySQL tests are disabled") + } + + if opts == nil { + // so it is never nil in internal helper functions + opts = new(DatastoreTestOptions) + } + + if tt, ok := t.(*testing.T); ok && !opts.RealReplica { + tt.Parallel() + } + + if opts.RealReplica { + if _, ok := os.LookupEnv("MYSQL_REPLICA_TEST"); !ok { + t.Skip("MySQL replica tests are disabled. Set env var MYSQL_REPLICA_TEST=1 to enable.") + } + } + + pc, _, _, ok := runtime.Caller(2) + details := runtime.FuncForPC(pc) + if !ok || details == nil { + t.FailNow() + } + + cleanName := strings.ReplaceAll( + strings.TrimPrefix(details.Name(), "github.com/fleetdm/fleet/v4/"), "/", "_", + ) + cleanName = strings.ReplaceAll(cleanName, ".", "_") + if len(cleanName) > 60 { + // the later parts are more unique than the start, with the package names, + // so trim from the start. + cleanName = cleanName[len(cleanName)-60:] + } + ds := initializeDatabase(t, cleanName, opts) + t.Cleanup(func() { Close(ds) }) + return ds +} + +func Close(ds *mysql.Datastore) { + _ = ds.Writer(context.Background()).Close() +} + +func CreateMySQLDS(t testing.TB) *mysql.Datastore { + return createMySQLDSWithOptions(t, nil) +} + +func ExecAdhocSQL(tb testing.TB, ds *mysql.Datastore, fn func(q sqlx.ExtContext) error) { + tb.Helper() + err := fn(ds.Writer(context.Background())) + require.NoError(tb, err) +} + +// TruncateTables truncates the specified tables, in order, using ds.writer. +// Note that the order is typically not important because FK checks are +// disabled while truncating. If no table is provided, all tables (except +// those that are seeded by the SQL schema file) are truncated. +func TruncateTables(t testing.TB, ds *mysql.Datastore, tables ...string) { + // By setting DISABLE_TRUNCATE_TABLES a developer can troubleshoot tests + // by inspecting mysql tables. + if os.Getenv("DISABLE_TRUNCATE_TABLES") != "" { + return + } + + // those tables are seeded with the schema.sql and as such must not + // be truncated - a more precise approach must be used for those, e.g. + // delete where id > max before test, or something like that. + nonEmptyTables := map[string]bool{ + // put tables here that should not be truncated + } + ctx := context.Background() + + require.NoError(t, ds.WithRetryTxx(ctx, func(tx sqlx.ExtContext) error { + var skipSeeded bool + + if len(tables) == 0 { + skipSeeded = true + sql := ` + SELECT + table_name + FROM + information_schema.tables + WHERE + table_schema = database() AND + table_type = 'BASE TABLE' + ` + if err := sqlx.SelectContext(ctx, tx, &tables, sql); err != nil { + return err + } + } + + if _, err := tx.ExecContext(ctx, `SET FOREIGN_KEY_CHECKS=0`); err != nil { + return err + } + for _, tbl := range tables { + if nonEmptyTables[tbl] { + if skipSeeded { + continue + } + return fmt.Errorf("cannot truncate table %s, it contains seed data from schema.sql", tbl) + } + if _, err := tx.ExecContext(ctx, "TRUNCATE TABLE "+tbl); err != nil { + return err + } + } + if _, err := tx.ExecContext(ctx, `SET FOREIGN_KEY_CHECKS=1`); err != nil { + return err + } + return nil + })) +} diff --git a/server/mdm/android/service.go b/server/mdm/android/service.go new file mode 100644 index 000000000000..7b8076368c0c --- /dev/null +++ b/server/mdm/android/service.go @@ -0,0 +1,10 @@ +package android + +import "context" + +type Service interface { + EnterpriseSignup(ctx context.Context) (*SignupDetails, error) + EnterpriseSignupCallback(ctx context.Context, enterpriseID uint, enterpriseToken string) error + + CreateEnrollmentToken(ctx context.Context) (*EnrollmentToken, error) +} diff --git a/server/mdm/android/service/endpoint_utils.go b/server/mdm/android/service/endpoint_utils.go new file mode 100644 index 000000000000..ef69bd4dc5c9 --- /dev/null +++ b/server/mdm/android/service/endpoint_utils.go @@ -0,0 +1,244 @@ +package service + +// TODO(26218): Refactor this to remove duplication. + +import ( + "bufio" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strings" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/android" + "github.com/fleetdm/fleet/v4/server/service/middleware/auth" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" + "github.com/go-kit/kit/endpoint" + kithttp "github.com/go-kit/kit/transport/http" + "github.com/gorilla/mux" +) + +type handlerFunc func(ctx context.Context, request interface{}, svc android.Service) fleet.Errorer + +func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { + if e, ok := response.(fleet.Errorer); ok && e.Error() != nil { + endpoint_utils.EncodeError(ctx, e.Error(), w) + return nil + } + + if e, ok := response.(statuser); ok { + w.WriteHeader(e.Status()) + if e.Status() == http.StatusNoContent { + return nil + } + } + + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(response) +} + +// statuser allows response types to implement a custom +// http success status - default is 200 OK +type statuser interface { + Status() int +} + +// makeDecoder creates a decoder for the type for the struct passed on. If the +// struct has at least 1 json tag it'll unmarshall the body. If the struct has +// a `url` tag with value list_options it'll gather fleet.ListOptions from the +// URL (similarly for host_options, carve_options, user_options that derive +// from the common list_options). Note that these behaviors do not work for embedded structs. +// +// Finally, any other `url` tag will be treated as a path variable (of the form +// /path/{name} in the route's path) from the URL path pattern, and it'll be +// decoded and set accordingly. Variables can be optional by setting the tag as +// follows: `url:"some-id,optional"`. +// The "list_options" are optional by default and it'll ignore the optional +// portion of the tag. +// +// If iface implements the requestDecoder interface, it returns a function that +// calls iface.DecodeRequest(ctx, r) - i.e. the value itself fully controls its +// own decoding. +// +// If iface implements the bodyDecoder interface, it calls iface.DecodeBody +// after having decoded any non-body fields (such as url and query parameters) +// into the struct. +func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc { + if iface == nil { + return func(ctx context.Context, r *http.Request) (interface{}, error) { + return nil, nil + } + } + + t := reflect.TypeOf(iface) + if t.Kind() != reflect.Struct { + panic(fmt.Sprintf("makeDecoder only understands structs, not %T", iface)) + } + + return func(ctx context.Context, r *http.Request) (interface{}, error) { + v := reflect.New(t) + nilBody := false + buf := bufio.NewReader(r.Body) + var body io.Reader = buf + if _, err := buf.Peek(1); err == io.EOF { + nilBody = true + } else { + if r.Header.Get("content-encoding") == "gzip" { + gzr, err := gzip.NewReader(buf) + if err != nil { + return nil, endpoint_utils.BadRequestErr("gzip decoder error", err) + } + defer gzr.Close() + body = gzr + } + + req := v.Interface() + if err := json.NewDecoder(body).Decode(req); err != nil { + return nil, endpoint_utils.BadRequestErr("json decoder error", err) + } + v = reflect.ValueOf(req) + } + + fields := endpoint_utils.AllFields(v) + for _, fp := range fields { + field := fp.V + + urlTagValue, ok := fp.Sf.Tag.Lookup("url") + + var err error + if ok { + optional := false + urlTagValue, optional, err = endpoint_utils.ParseTag(urlTagValue) + if err != nil { + return nil, err + } + err = endpoint_utils.DecodeURLTagValue(r, field, urlTagValue, optional) + if err != nil { + return nil, err + } + continue + } + + _, jsonExpected := fp.Sf.Tag.Lookup("json") + if jsonExpected && nilBody { + return nil, badRequest("Expected JSON Body") + } + + err = endpoint_utils.DecodeQueryTagValue(r, fp) + if err != nil { + return nil, err + } + } + + return v.Interface(), nil + } +} + +func badRequest(msg string) error { + return &fleet.BadRequestError{Message: msg} +} + +type authEndpointer struct { + fleetSvc fleet.Service + svc android.Service + opts []kithttp.ServerOption + r *mux.Router + authFunc func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint + versions []string + customMiddleware []endpoint.Middleware +} + +func newUserAuthenticatedEndpointer(fleetSvc fleet.Service, svc android.Service, opts []kithttp.ServerOption, r *mux.Router, + versions ...string) *authEndpointer { + return &authEndpointer{ + fleetSvc: fleetSvc, + svc: svc, + opts: opts, + r: r, + authFunc: auth.AuthenticatedUser, + versions: versions, + } +} + +func newNoAuthEndpointer(svc android.Service, opts []kithttp.ServerOption, r *mux.Router, versions ...string) *authEndpointer { + return &authEndpointer{ + fleetSvc: nil, + svc: svc, + opts: opts, + r: r, + authFunc: auth.UnauthenticatedRequest, + versions: versions, + } +} + +var pathReplacer = strings.NewReplacer( + "/", "_", + "{", "_", + "}", "_", +) + +func getNameFromPathAndVerb(verb, path string) string { + prefix := strings.ToLower(verb) + "_" + return prefix + pathReplacer.Replace(strings.TrimPrefix(strings.TrimRight(path, "/"), "/api/_version_/fleet/")) +} + +func (e *authEndpointer) POST(path string, f handlerFunc, v interface{}) { + e.handleEndpoint(path, f, v, "POST") +} + +func (e *authEndpointer) GET(path string, f handlerFunc, v interface{}) { + e.handleEndpoint(path, f, v, "GET") +} + +func (e *authEndpointer) PUT(path string, f handlerFunc, v interface{}) { + e.handleEndpoint(path, f, v, "PUT") +} + +func (e *authEndpointer) PATCH(path string, f handlerFunc, v interface{}) { + e.handleEndpoint(path, f, v, "PATCH") +} + +func (e *authEndpointer) DELETE(path string, f handlerFunc, v interface{}) { + e.handleEndpoint(path, f, v, "DELETE") +} + +func (e *authEndpointer) HEAD(path string, f handlerFunc, v interface{}) { + e.handleEndpoint(path, f, v, "HEAD") +} + +func (e *authEndpointer) handlePathHandler(path string, pathHandler func(path string) http.Handler, verb string) { + versions := e.versions + versionedPath := strings.Replace(path, "/_version_/", fmt.Sprintf("/{fleetversion:(?:%s)}/", strings.Join(versions, "|")), 1) + nameAndVerb := getNameFromPathAndVerb(verb, path) + e.r.Handle(versionedPath, pathHandler(versionedPath)).Name(nameAndVerb).Methods(verb) +} + +func (e *authEndpointer) handleHTTPHandler(path string, h http.Handler, verb string) { + self := func(_ string) http.Handler { return h } + e.handlePathHandler(path, self, verb) +} + +func (e *authEndpointer) handleEndpoint(path string, f handlerFunc, v interface{}, verb string) { + e.handleHTTPHandler(path, e.makeEndpoint(f, v), verb) +} + +func (e *authEndpointer) makeEndpoint(f handlerFunc, v interface{}) http.Handler { + next := func(ctx context.Context, request interface{}) (interface{}, error) { + return f(ctx, request, e.svc), nil + } + endPt := e.authFunc(e.fleetSvc, next) + + // apply middleware in reverse order so that the first wraps the second + // wraps the third etc. + for i := len(e.customMiddleware) - 1; i >= 0; i-- { + mw := e.customMiddleware[i] + endPt = mw(endPt) + } + + return newServer(endPt, makeDecoder(v), e.opts) +} diff --git a/server/mdm/android/service/handler.go b/server/mdm/android/service/handler.go new file mode 100644 index 000000000000..a5b7ef06cb49 --- /dev/null +++ b/server/mdm/android/service/handler.go @@ -0,0 +1,49 @@ +package service + +import ( + "net/http" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/android" + "github.com/fleetdm/fleet/v4/server/service/middleware/authzcheck" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" + "github.com/go-kit/kit/endpoint" + kithttp "github.com/go-kit/kit/transport/http" + "github.com/gorilla/mux" +) + +func GetRoutes(fleetSvc fleet.Service, svc android.Service) endpoint_utils.HandlerRoutesFunc { + return func(r *mux.Router, opts []kithttp.ServerOption) { + attachFleetAPIRoutes(r, fleetSvc, svc, opts) + } +} + +func attachFleetAPIRoutes(r *mux.Router, fleetSvc fleet.Service, svc android.Service, opts []kithttp.ServerOption) { + + // user-authenticated endpoints + ue := newUserAuthenticatedEndpointer(fleetSvc, svc, opts, r, apiVersions()...) + + ue.GET("/api/_version_/fleet/android_enterprise/signup_url", androidEnterpriseSignupEndpoint, nil) + ue.GET("/api/_version_/fleet/android_enterprise/{id:[0-9]+}/enrollment_token", androidEnrollmentTokenEndpoint, + androidEnrollmentTokenRequest{}) + + // unauthenticated endpoints - most of those are either login-related, + // invite-related or host-enrolling. So they typically do some kind of + // one-time authentication by verifying that a valid secret token is provided + // with the request. + ne := newNoAuthEndpointer(svc, opts, r, apiVersions()...) + + // Android management + ne.GET("/api/_version_/fleet/android_enterprise/{id:[0-9]+}/connect", androidEnterpriseSignupCallbackEndpoint, + androidEnterpriseSignupCallbackRequest{}) + +} + +func apiVersions() []string { + return []string{"v1"} +} + +func newServer(e endpoint.Endpoint, decodeFn kithttp.DecodeRequestFunc, opts []kithttp.ServerOption) http.Handler { + e = authzcheck.NewMiddleware().AuthzCheck()(e) + return kithttp.NewServer(e, decodeFn, encodeResponse, opts...) +} diff --git a/server/mdm/android/service/service.go b/server/mdm/android/service/service.go new file mode 100644 index 000000000000..ecfe52c614cb --- /dev/null +++ b/server/mdm/android/service/service.go @@ -0,0 +1,118 @@ +package service + +import ( + "context" + "errors" + "fmt" + + "github.com/fleetdm/fleet/v4/server/authz" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/android" + kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" + "google.golang.org/api/androidmanagement/v1" +) + +type Service struct { + logger kitlog.Logger + authz *authz.Authorizer + mgmt *androidmanagement.Service + ds android.Datastore + fleetDS fleet.Datastore +} + +func NewService( + ctx context.Context, + logger kitlog.Logger, + ds android.Datastore, + fleetDS fleet.Datastore, +) (android.Service, error) { + authorizer, err := authz.NewAuthorizer() + if err != nil { + return nil, fmt.Errorf("new authorizer: %w", err) + } + + // mgmt, err := androidmanagement.NewService(ctx) + // if err != nil { + // return nil, ctxerr.Wrap(ctx, err, "creating android management service") + // } + return Service{ + logger: logger, + authz: authorizer, + mgmt: nil, + ds: ds, + fleetDS: fleetDS, + }, nil +} + +type androidResponse struct { + Err error `json:"error,omitempty"` +} + +func (r androidResponse) Error() error { return r.Err } + +type androidEnterpriseSignupResponse struct { + *android.SignupDetails + androidResponse +} + +func androidEnterpriseSignupEndpoint(ctx context.Context, _ interface{}, svc android.Service) fleet.Errorer { + result, err := svc.EnterpriseSignup(ctx) + if err != nil { + return androidResponse{Err: err} + } + return androidEnterpriseSignupResponse{SignupDetails: result} +} + +func (s Service) EnterpriseSignup(ctx context.Context) (*android.SignupDetails, error) { + s.authz.SkipAuthorization(ctx) + + // TODO: remove me + level.Warn(s.logger).Log("msg", "EnterpriseSignup called") + return nil, errors.New("not implemented") + +} + +type androidEnterpriseSignupCallbackRequest struct { + ID uint `url:"id"` + EnterpriseToken string `query:"enterpriseToken"` +} + +func androidEnterpriseSignupCallbackEndpoint(ctx context.Context, request interface{}, svc android.Service) fleet.Errorer { + req := request.(*androidEnterpriseSignupCallbackRequest) + err := svc.EnterpriseSignupCallback(ctx, req.ID, req.EnterpriseToken) + return androidResponse{Err: err} +} + +func (s Service) EnterpriseSignupCallback(ctx context.Context, id uint, enterpriseToken string) error { + s.authz.SkipAuthorization(ctx) + + // TODO: remove me + level.Warn(s.logger).Log("msg", "EnterpriseSignupCallback called", "id", id, "enterpriseToken", enterpriseToken) + return errors.New("not implemented") +} + +type androidEnrollmentTokenRequest struct { + EnterpriseID uint `url:"id"` +} + +type androidEnrollmentTokenResponse struct { + *android.EnrollmentToken + androidResponse +} + +func androidEnrollmentTokenEndpoint(ctx context.Context, request interface{}, svc android.Service) fleet.Errorer { + token, err := svc.CreateEnrollmentToken(ctx) + if err != nil { + return androidResponse{Err: err} + } + return androidEnrollmentTokenResponse{EnrollmentToken: token} +} + +func (s Service) CreateEnrollmentToken(ctx context.Context) (*android.EnrollmentToken, error) { + s.authz.SkipAuthorization(ctx) + + // TODO: remove me + level.Warn(s.logger).Log("msg", "CreateEnrollmentToken called") + return nil, errors.New("not implemented") +} diff --git a/server/mdm/apple/vpp/refresh.go b/server/mdm/apple/vpp/refresh.go new file mode 100644 index 000000000000..2ad72a237700 --- /dev/null +++ b/server/mdm/apple/vpp/refresh.go @@ -0,0 +1,44 @@ +package vpp + +import ( + "context" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/apple/itunes" +) + +// RefreshVersions updatest the LatestVersion fields for the VPP apps stored in Fleet. +func RefreshVersions(ctx context.Context, ds fleet.Datastore) error { + apps, err := ds.GetAllVPPApps(ctx) + if err != nil { + return err + } + + var adamIDs []string + appsByAdamID := make(map[string]*fleet.VPPApp) + for _, app := range apps { + adamIDs = append(adamIDs, app.AdamID) + appsByAdamID[app.AdamID] = app + } + + meta, err := itunes.GetAssetMetadata(adamIDs, &itunes.AssetMetadataFilter{Entity: "software"}) + if err != nil { + return err + } + + var appsToUpdate []*fleet.VPPApp + for _, adamID := range adamIDs { + if m, ok := meta[adamID]; ok { + if m.Version != appsByAdamID[adamID].LatestVersion { + appsByAdamID[adamID].LatestVersion = m.Version + appsToUpdate = append(appsToUpdate, appsByAdamID[adamID]) + } + } + } + + if err := ds.InsertVPPApps(ctx, appsToUpdate); err != nil { + return err + } + + return nil +} diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 6d81dc3cdde5..a71fb1d86bf0 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1161,6 +1161,10 @@ type SetTeamVPPAppsFunc func(ctx context.Context, teamID *uint, appIDs []fleet.V type InsertVPPAppWithTeamFunc func(ctx context.Context, app *fleet.VPPApp, teamID *uint) (*fleet.VPPApp, error) +type GetAllVPPAppsFunc func(ctx context.Context) ([]*fleet.VPPApp, error) + +type InsertVPPAppsFunc func(ctx context.Context, apps []*fleet.VPPApp) error + type InsertHostVPPSoftwareInstallFunc func(ctx context.Context, hostID uint, appID fleet.VPPAppID, commandUUID string, associatedEventID string, opts fleet.HostSoftwareInstallOptions) error type GetPastActivityDataForVPPAppInstallFunc func(ctx context.Context, commandResults *mdm.CommandResults) (*fleet.User, *fleet.ActivityInstalledAppStoreApp, error) @@ -2934,6 +2938,12 @@ type DataStore struct { InsertVPPAppWithTeamFunc InsertVPPAppWithTeamFunc InsertVPPAppWithTeamFuncInvoked bool + GetAllVPPAppsFunc GetAllVPPAppsFunc + GetAllVPPAppsFuncInvoked bool + + InsertVPPAppsFunc InsertVPPAppsFunc + InsertVPPAppsFuncInvoked bool + InsertHostVPPSoftwareInstallFunc InsertHostVPPSoftwareInstallFunc InsertHostVPPSoftwareInstallFuncInvoked bool @@ -7020,6 +7030,20 @@ func (s *DataStore) InsertVPPAppWithTeam(ctx context.Context, app *fleet.VPPApp, return s.InsertVPPAppWithTeamFunc(ctx, app, teamID) } +func (s *DataStore) GetAllVPPApps(ctx context.Context) ([]*fleet.VPPApp, error) { + s.mu.Lock() + s.GetAllVPPAppsFuncInvoked = true + s.mu.Unlock() + return s.GetAllVPPAppsFunc(ctx) +} + +func (s *DataStore) InsertVPPApps(ctx context.Context, apps []*fleet.VPPApp) error { + s.mu.Lock() + s.InsertVPPAppsFuncInvoked = true + s.mu.Unlock() + return s.InsertVPPAppsFunc(ctx, apps) +} + func (s *DataStore) InsertHostVPPSoftwareInstall(ctx context.Context, hostID uint, appID fleet.VPPAppID, commandUUID string, associatedEventID string, opts fleet.HostSoftwareInstallOptions) error { s.mu.Lock() s.InsertHostVPPSoftwareInstallFuncInvoked = true diff --git a/server/service/activities.go b/server/service/activities.go index 50ea2ff2fa12..1e42451341a5 100644 --- a/server/service/activities.go +++ b/server/service/activities.go @@ -34,7 +34,7 @@ type listActivitiesResponse struct { func (r listActivitiesResponse) Error() error { return r.Err } -func listActivitiesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listActivitiesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listActivitiesRequest) activities, metadata, err := svc.ListActivities(ctx, fleet.ListActivitiesOptions{ ListOptions: req.ListOptions, @@ -157,7 +157,7 @@ type listHostUpcomingActivitiesResponse struct { func (r listHostUpcomingActivitiesResponse) Error() error { return r.Err } -func listHostUpcomingActivitiesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listHostUpcomingActivitiesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listHostUpcomingActivitiesRequest) acts, meta, err := svc.ListHostUpcomingActivities(ctx, req.HostID, req.ListOptions) if err != nil { @@ -207,7 +207,7 @@ type listHostPastActivitiesRequest struct { ListOptions fleet.ListOptions `url:"list_options"` } -func listHostPastActivitiesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listHostPastActivitiesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listHostPastActivitiesRequest) acts, meta, err := svc.ListHostPastActivities(ctx, req.HostID, req.ListOptions) if err != nil { diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 0ed2314f9860..267f4fb14cdf 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -104,7 +104,7 @@ func (r appConfigResponse) MarshalJSON() ([]byte, error) { func (r appConfigResponse) Error() error { return r.Err } -func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { vc, ok := viewer.FromContext(ctx) if !ok { return nil, errors.New("could not fetch user") @@ -235,7 +235,7 @@ type modifyAppConfigRequest struct { json.RawMessage } -func modifyAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyAppConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyAppConfigRequest) appConfig, err := svc.ModifyAppConfig(ctx, req.RawMessage, fleet.ApplySpecOptions{ Force: req.Force, @@ -1420,7 +1420,7 @@ type applyEnrollSecretSpecResponse struct { func (r applyEnrollSecretSpecResponse) Error() error { return r.Err } -func applyEnrollSecretSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func applyEnrollSecretSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*applyEnrollSecretSpecRequest) err := svc.ApplyEnrollSecretSpec( ctx, req.Spec, fleet.ApplySpecOptions{ @@ -1478,7 +1478,7 @@ type getEnrollSecretSpecResponse struct { func (r getEnrollSecretSpecResponse) Error() error { return r.Err } -func getEnrollSecretSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getEnrollSecretSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { specs, err := svc.GetEnrollSecretSpec(ctx) if err != nil { return getEnrollSecretSpecResponse{Err: err}, nil @@ -1509,7 +1509,7 @@ type versionResponse struct { func (r versionResponse) Error() error { return r.Err } -func versionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func versionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { info, err := svc.Version(ctx) if err != nil { return versionResponse{Err: err}, nil @@ -1537,7 +1537,7 @@ type getCertificateResponse struct { func (r getCertificateResponse) Error() error { return r.Err } -func getCertificateEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getCertificateEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { chain, err := svc.CertificateChain(ctx) if err != nil { return getCertificateResponse{Err: err}, nil diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index b008800d3bc3..2d830e60083b 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -46,6 +46,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" nano_service "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/sso" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" @@ -97,7 +98,7 @@ type getMDMAppleCommandResultsResponse struct { func (r getMDMAppleCommandResultsResponse) Error() error { return r.Err } -func getMDMAppleCommandResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMAppleCommandResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMAppleCommandResultsRequest) results, err := svc.GetMDMAppleCommandResults(ctx, req.CommandUUID) if err != nil { @@ -199,7 +200,7 @@ type listMDMAppleCommandsResponse struct { func (r listMDMAppleCommandsResponse) Error() error { return r.Err } -func listMDMAppleCommandsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listMDMAppleCommandsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listMDMAppleCommandsRequest) results, err := svc.ListMDMAppleCommands(ctx, &fleet.MDMCommandListOptions{ ListOptions: req.ListOptions, @@ -337,7 +338,7 @@ func (newMDMAppleConfigProfileRequest) DecodeRequest(ctx context.Context, r *htt func (r newMDMAppleConfigProfileResponse) Error() error { return r.Err } -func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func newMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*newMDMAppleConfigProfileRequest) ff, err := req.Profile.Open() @@ -426,7 +427,7 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r newCP, err := svc.ds.NewMDMAppleConfigProfile(ctx, *cp) if err != nil { - var existsErr existsErrorInterface + var existsErr endpoint_utils.ExistsErrorInterface if errors.As(err, &existsErr) { msg := SameProfileNameUploadErrorMsg if re, ok := existsErr.(interface{ Resource() string }); ok { @@ -645,7 +646,7 @@ type listMDMAppleConfigProfilesResponse struct { func (r listMDMAppleConfigProfilesResponse) Error() error { return r.Err } -func listMDMAppleConfigProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listMDMAppleConfigProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listMDMAppleConfigProfilesRequest) cps, err := svc.ListMDMAppleConfigProfiles(ctx, req.TeamID) @@ -712,7 +713,7 @@ func (r getMDMAppleConfigProfileResponse) hijackRender(ctx context.Context, w ht r.fileReader.Close() } -func getMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMAppleConfigProfileRequest) cp, err := svc.GetMDMAppleConfigProfileByDeprecatedID(ctx, req.ProfileID) @@ -791,7 +792,7 @@ type deleteMDMAppleConfigProfileResponse struct { func (r deleteMDMAppleConfigProfileResponse) Error() error { return r.Err } -func deleteMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteMDMAppleConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteMDMAppleConfigProfileRequest) if err := svc.DeleteMDMAppleConfigProfileByDeprecatedID(ctx, req.ProfileID); err != nil { @@ -989,7 +990,7 @@ type getMDMAppleFileVaultSummaryResponse struct { func (r getMDMAppleFileVaultSummaryResponse) Error() error { return r.Err } -func getMdmAppleFileVaultSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMdmAppleFileVaultSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMAppleFileVaultSummaryRequest) fvs, err := svc.GetMDMAppleFileVaultSummary(ctx, req.TeamID) @@ -1026,7 +1027,7 @@ type getMDMAppleProfilesSummaryResponse struct { func (r getMDMAppleProfilesSummaryResponse) Error() error { return r.Err } -func getMDMAppleProfilesSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMAppleProfilesSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMAppleProfilesSummaryRequest) res := getMDMAppleProfilesSummaryResponse{} @@ -1088,7 +1089,7 @@ func (uploadAppleInstallerRequest) DecodeRequest(ctx context.Context, r *http.Re func (r uploadAppleInstallerResponse) Error() error { return r.Err } // Deprecated: Not in Use -func uploadAppleInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func uploadAppleInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uploadAppleInstallerRequest) ff, err := req.Installer.Open() if err != nil { @@ -1176,7 +1177,7 @@ type getAppleInstallerDetailsResponse struct { func (r getAppleInstallerDetailsResponse) Error() error { return r.Err } -func getAppleInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getAppleInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getAppleInstallerDetailsRequest) installer, err := svc.GetMDMAppleInstallerByID(ctx, req.ID) if err != nil { @@ -1209,7 +1210,7 @@ type deleteAppleInstallerDetailsResponse struct { func (r deleteAppleInstallerDetailsResponse) Error() error { return r.Err } -func deleteAppleInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteAppleInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteAppleInstallerDetailsRequest) if err := svc.DeleteMDMAppleInstaller(ctx, req.ID); err != nil { return deleteAppleInstallerDetailsResponse{Err: err}, nil @@ -1237,7 +1238,7 @@ type listMDMAppleDevicesResponse struct { func (r listMDMAppleDevicesResponse) Error() error { return r.Err } -func listMDMAppleDevicesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listMDMAppleDevicesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { devices, err := svc.ListMDMAppleDevices(ctx) if err != nil { return listMDMAppleDevicesResponse{Err: err}, nil @@ -1263,7 +1264,7 @@ type newMDMAppleDEPKeyPairResponse struct { func (r newMDMAppleDEPKeyPairResponse) Error() error { return r.Err } -func newMDMAppleDEPKeyPairEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func newMDMAppleDEPKeyPairEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { keyPair, err := svc.NewMDMAppleDEPKeyPair(ctx) if err != nil { return newMDMAppleDEPKeyPairResponse{ @@ -1308,7 +1309,7 @@ func (r enqueueMDMAppleCommandResponse) Error() error { return r.Err } // Deprecated: enqueueMDMAppleCommandEndpoint is now deprecated, replaced by // the platform-agnostic runMDMCommandEndpoint. It is still supported // indefinitely for backwards compatibility. -func enqueueMDMAppleCommandEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func enqueueMDMAppleCommandEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*enqueueMDMAppleCommandRequest) result, err := svc.EnqueueMDMAppleCommand(ctx, req.Command, req.DeviceIDs) if err != nil { @@ -1399,7 +1400,7 @@ func (r mdmAppleEnrollResponse) hijackRender(ctx context.Context, w http.Respons w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) if err := json.NewEncoder(w).Encode(r.SoftwareUpdateRequired); err != nil { - encodeError(ctx, ctxerr.New(ctx, "failed to encode software update required"), w) + endpoint_utils.EncodeError(ctx, ctxerr.New(ctx, "failed to encode software update required"), w) } return } @@ -1418,7 +1419,7 @@ func (r mdmAppleEnrollResponse) hijackRender(ctx context.Context, w http.Respons } } -func mdmAppleEnrollEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmAppleEnrollEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*mdmAppleEnrollRequest) sur, err := svc.CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx, req.MachineInfo) @@ -1616,7 +1617,7 @@ type mdmAppleCommandRemoveEnrollmentProfileResponse struct { func (r mdmAppleCommandRemoveEnrollmentProfileResponse) Error() error { return r.Err } -func mdmAppleCommandRemoveEnrollmentProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmAppleCommandRemoveEnrollmentProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*mdmAppleCommandRemoveEnrollmentProfileRequest) err := svc.EnqueueMDMAppleCommandRemoveEnrollmentProfile(ctx, req.HostID) if err != nil { @@ -1736,7 +1737,7 @@ func (r mdmAppleGetInstallerResponse) hijackRender(ctx context.Context, w http.R } } -func mdmAppleGetInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmAppleGetInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*mdmAppleGetInstallerRequest) installer, err := svc.GetMDMAppleInstallerByToken(ctx, req.Token) if err != nil { @@ -1765,7 +1766,7 @@ type mdmAppleHeadInstallerRequest struct { Token string `query:"token"` } -func mdmAppleHeadInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmAppleHeadInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*mdmAppleHeadInstallerRequest) installer, err := svc.GetMDMAppleInstallerDetailsByToken(ctx, req.Token) if err != nil { @@ -1798,7 +1799,7 @@ type listMDMAppleInstallersResponse struct { func (r listMDMAppleInstallersResponse) Error() error { return r.Err } -func listMDMAppleInstallersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listMDMAppleInstallersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { installers, err := svc.ListMDMAppleInstallers(ctx) if err != nil { return listMDMAppleInstallersResponse{ @@ -1846,7 +1847,7 @@ func (r deviceLockResponse) Error() error { return r.Err } func (r deviceLockResponse) Status() int { return http.StatusNoContent } -func deviceLockEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deviceLockEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deviceLockRequest) err := svc.MDMAppleDeviceLock(ctx, req.HostID) if err != nil { @@ -1879,7 +1880,7 @@ func (r deviceWipeResponse) Error() error { return r.Err } func (r deviceWipeResponse) Status() int { return http.StatusNoContent } -func deviceWipeEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deviceWipeEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deviceWipeRequest) err := svc.MDMAppleEraseDevice(ctx, req.HostID) if err != nil { @@ -1912,7 +1913,7 @@ type getHostProfilesResponse struct { func (r getHostProfilesResponse) Error() error { return r.Err } -func getHostProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostProfilesRequest) sums, err := svc.MDMListHostConfigurationProfiles(ctx, req.ID) if err != nil { @@ -1952,7 +1953,7 @@ func (r batchSetMDMAppleProfilesResponse) Error() error { return r.Err } func (r batchSetMDMAppleProfilesResponse) Status() int { return http.StatusNoContent } -func batchSetMDMAppleProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func batchSetMDMAppleProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchSetMDMAppleProfilesRequest) if err := svc.BatchSetMDMAppleProfiles(ctx, req.TeamID, req.TeamName, req.Profiles, req.DryRun, false); err != nil { return batchSetMDMAppleProfilesResponse{Err: err}, nil @@ -2105,7 +2106,7 @@ func (r preassignMDMAppleProfileResponse) Error() error { return r.Err } func (r preassignMDMAppleProfileResponse) Status() int { return http.StatusNoContent } -func preassignMDMAppleProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func preassignMDMAppleProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*preassignMDMAppleProfileRequest) if err := svc.MDMApplePreassignProfile(ctx, req.MDMApplePreassignProfilePayload); err != nil { return preassignMDMAppleProfileResponse{Err: err}, nil @@ -2137,7 +2138,7 @@ func (r matchMDMApplePreassignmentResponse) Error() error { return r.Err } func (r matchMDMApplePreassignmentResponse) Status() int { return http.StatusNoContent } -func matchMDMApplePreassignmentEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func matchMDMApplePreassignmentEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*matchMDMApplePreassignmentRequest) if err := svc.MDMAppleMatchPreassignment(ctx, req.ExternalHostIdentifier); err != nil { return matchMDMApplePreassignmentResponse{Err: err}, nil @@ -2172,7 +2173,7 @@ func (r updateMDMAppleSettingsResponse) Status() int { return http.StatusNoConte // This endpoint is required because the UI must allow maintainers (in addition // to admins) to update some MDM Apple settings, while the update config/update // team endpoints only allow write access to admins. -func updateMDMAppleSettingsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func updateMDMAppleSettingsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateMDMAppleSettingsRequest) if err := svc.UpdateMDMDiskEncryption(ctx, req.MDMAppleSettingsPayload.TeamID, req.MDMAppleSettingsPayload.EnableDiskEncryption); err != nil { return updateMDMAppleSettingsResponse{Err: err}, nil @@ -2282,7 +2283,7 @@ func (uploadBootstrapPackageRequest) DecodeRequest(ctx context.Context, r *http. func (r uploadBootstrapPackageResponse) Error() error { return r.Err } -func uploadBootstrapPackageEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func uploadBootstrapPackageEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uploadBootstrapPackageRequest) ff, err := req.Package.Open() if err != nil { @@ -2335,7 +2336,7 @@ func (r downloadBootstrapPackageResponse) hijackRender(ctx context.Context, w ht } } -func downloadBootstrapPackageEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func downloadBootstrapPackageEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*downloadBootstrapPackageRequest) pkg, err := svc.GetMDMAppleBootstrapPackageBytes(ctx, req.Token) if err != nil { @@ -2377,7 +2378,7 @@ type bootstrapPackageMetadataResponse struct { func (r bootstrapPackageMetadataResponse) Error() error { return r.Err } -func bootstrapPackageMetadataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func bootstrapPackageMetadataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*bootstrapPackageMetadataRequest) meta, err := svc.GetMDMAppleBootstrapPackageMetadata(ctx, req.TeamID, req.ForUpdate) switch { @@ -2412,7 +2413,7 @@ type deleteBootstrapPackageResponse struct { func (r deleteBootstrapPackageResponse) Error() error { return r.Err } -func deleteBootstrapPackageEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteBootstrapPackageEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteBootstrapPackageRequest) if err := svc.DeleteMDMAppleBootstrapPackage(ctx, &req.TeamID); err != nil { return deleteBootstrapPackageResponse{Err: err}, nil @@ -2443,7 +2444,7 @@ type getMDMAppleBootstrapPackageSummaryResponse struct { func (r getMDMAppleBootstrapPackageSummaryResponse) Error() error { return r.Err } -func getMDMAppleBootstrapPackageSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMAppleBootstrapPackageSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMAppleBootstrapPackageSummaryRequest) summary, err := svc.GetMDMAppleBootstrapPackageSummary(ctx, req.TeamID) if err != nil { @@ -2477,7 +2478,7 @@ type createMDMAppleSetupAssistantResponse struct { func (r createMDMAppleSetupAssistantResponse) Error() error { return r.Err } -func createMDMAppleSetupAssistantEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createMDMAppleSetupAssistantEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createMDMAppleSetupAssistantRequest) asst, err := svc.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{ TeamID: req.TeamID, @@ -2513,7 +2514,7 @@ type getMDMAppleSetupAssistantResponse struct { func (r getMDMAppleSetupAssistantResponse) Error() error { return r.Err } -func getMDMAppleSetupAssistantEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMAppleSetupAssistantEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMAppleSetupAssistantRequest) asst, err := svc.GetMDMAppleSetupAssistant(ctx, req.TeamID) if err != nil { @@ -2545,7 +2546,7 @@ type deleteMDMAppleSetupAssistantResponse struct { func (r deleteMDMAppleSetupAssistantResponse) Error() error { return r.Err } func (r deleteMDMAppleSetupAssistantResponse) Status() int { return http.StatusNoContent } -func deleteMDMAppleSetupAssistantEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteMDMAppleSetupAssistantEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteMDMAppleSetupAssistantRequest) if err := svc.DeleteMDMAppleSetupAssistant(ctx, req.TeamID); err != nil { return deleteMDMAppleSetupAssistantResponse{Err: err}, nil @@ -2580,7 +2581,7 @@ func (r updateMDMAppleSetupResponse) Status() int { return http.StatusNoContent // This endpoint is required because the UI must allow maintainers (in addition // to admins) to update some MDM Apple settings, while the update config/update // team endpoints only allow write access to admins. -func updateMDMAppleSetupEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func updateMDMAppleSetupEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateMDMAppleSetupRequest) if err := svc.UpdateMDMAppleSetup(ctx, req.MDMAppleSetupPayload); err != nil { return updateMDMAppleSetupResponse{Err: err}, nil @@ -2609,7 +2610,7 @@ type initiateMDMAppleSSOResponse struct { func (r initiateMDMAppleSSOResponse) Error() error { return r.Err } -func initiateMDMAppleSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func initiateMDMAppleSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { idpProviderURL, err := svc.InitiateMDMAppleSSO(ctx) if err != nil { return initiateMDMAppleSSOResponse{Err: err}, nil @@ -2668,7 +2669,7 @@ func (r callbackMDMAppleSSOResponse) hijackRender(ctx context.Context, w http.Re // message. func (r callbackMDMAppleSSOResponse) Error() error { return nil } -func callbackMDMAppleSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func callbackMDMAppleSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { auth := request.(fleet.Auth) redirectURL := svc.InitiateMDMAppleSSOCallback(ctx, auth) return callbackMDMAppleSSOResponse{redirectURL: redirectURL}, nil @@ -2688,7 +2689,7 @@ func (svc *Service) InitiateMDMAppleSSOCallback(ctx context.Context, auth fleet. type getManualEnrollmentProfileRequest struct{} -func getManualEnrollmentProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getManualEnrollmentProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { profile, err := svc.GetMDMManualEnrollmentProfile(ctx) if err != nil { return getDeviceMDMManualEnrollProfileResponse{Err: err}, nil @@ -4403,7 +4404,7 @@ type generateABMKeyPairResponse struct { func (r generateABMKeyPairResponse) Error() error { return r.Err } -func generateABMKeyPairEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func generateABMKeyPairEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { keyPair, err := svc.GenerateABMKeyPair(ctx) if err != nil { return generateABMKeyPairResponse{ @@ -4505,7 +4506,7 @@ type uploadABMTokenResponse struct { func (r uploadABMTokenResponse) Error() error { return r.Err } -func uploadABMTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func uploadABMTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uploadABMTokenRequest) ff, err := req.Token.Open() if err != nil { @@ -4546,7 +4547,7 @@ type deleteABMTokenResponse struct { func (r deleteABMTokenResponse) Error() error { return r.Err } func (r deleteABMTokenResponse) Status() int { return http.StatusNoContent } -func deleteABMTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteABMTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteABMTokenRequest) if err := svc.DeleteABMToken(ctx, req.TokenID); err != nil { return deleteABMTokenResponse{Err: err}, nil @@ -4574,7 +4575,7 @@ type listABMTokensResponse struct { func (r listABMTokensResponse) Error() error { return r.Err } -func listABMTokensEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listABMTokensEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { tokens, err := svc.ListABMTokens(ctx) if err != nil { return &listABMTokensResponse{Err: err}, nil @@ -4606,7 +4607,7 @@ type countABMTokensResponse struct { func (r countABMTokensResponse) Error() error { return r.Err } -func countABMTokensEndpoint(ctx context.Context, _ interface{}, svc fleet.Service) (errorer, error) { +func countABMTokensEndpoint(ctx context.Context, _ interface{}, svc fleet.Service) (fleet.Errorer, error) { tokenCount, err := svc.CountABMTokens(ctx) if err != nil { return &countABMTokensResponse{Err: err}, nil @@ -4642,7 +4643,7 @@ type updateABMTokenTeamsResponse struct { func (r updateABMTokenTeamsResponse) Error() error { return r.Err } -func updateABMTokenTeamsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func updateABMTokenTeamsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateABMTokenTeamsRequest) tok, err := svc.UpdateABMTokenTeams(ctx, req.TokenID, req.MacOSTeamID, req.IOSTeamID, req.IPadOSTeamID) @@ -4687,7 +4688,7 @@ func (renewABMTokenRequest) DecodeRequest(ctx context.Context, r *http.Request) // because we are in this method, we know that the path has 7 parts, e.g: // /api/latest/fleet/abm_tokens/19/renew - id, err := intFromRequest(r, "id") + id, err := endpoint_utils.IntFromRequest(r, "id") if err != nil { return nil, ctxerr.Wrap(ctx, err, "failed to parse abm token id") } @@ -4705,7 +4706,7 @@ type renewABMTokenResponse struct { func (r renewABMTokenResponse) Error() error { return r.Err } -func renewABMTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func renewABMTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*renewABMTokenRequest) ff, err := req.Token.Open() if err != nil { @@ -4737,7 +4738,7 @@ type getOTAProfileRequest struct { EnrollSecret string `query:"enroll_secret"` } -func getOTAProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getOTAProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getOTAProfileRequest) profile, err := svc.GetOTAProfile(ctx, req.EnrollSecret) if err != nil { @@ -4841,7 +4842,7 @@ func (r mdmAppleOTAResponse) hijackRender(ctx context.Context, w http.ResponseWr } } -func mdmAppleOTAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmAppleOTAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*mdmAppleOTARequest) xml, err := svc.MDMAppleProcessOTAEnrollment(ctx, req.Certificates, req.RootSigner, req.EnrollSecret, req.DeviceInfo) if err != nil { diff --git a/server/service/base_client.go b/server/service/base_client.go index a3bfbcaf8553..89ad6a961d6c 100644 --- a/server/service/base_client.go +++ b/server/service/base_client.go @@ -79,7 +79,7 @@ func (bc *baseClient) parseResponse(verb, path string, response *http.Response, if err := json.Unmarshal(b, &responseDest); err != nil { return fmt.Errorf("decode %s %s response: %w, body: %s", verb, path, err, b) } - if e, ok := responseDest.(errorer); ok { + if e, ok := responseDest.(fleet.Errorer); ok { if e.Error() != nil { return fmt.Errorf("%s %s error: %w", verb, path, e.Error()) } diff --git a/server/service/calendar.go b/server/service/calendar.go index d5a74bd35b1a..282ada629c49 100644 --- a/server/service/calendar.go +++ b/server/service/calendar.go @@ -7,6 +7,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/gorilla/mux" ) @@ -21,7 +22,7 @@ func (calendarWebhookRequest) DecodeRequest(_ context.Context, r *http.Request) var req calendarWebhookRequest eventUUID, ok := mux.Vars(r)["event_uuid"] if !ok { - return nil, errBadRoute + return nil, endpoint_utils.ErrBadRoute } unescaped, err := url.PathUnescape(eventUUID) if err != nil { @@ -41,7 +42,7 @@ type calendarWebhookResponse struct { func (r calendarWebhookResponse) Error() error { return r.Err } -func calendarWebhookEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func calendarWebhookEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*calendarWebhookRequest) err := svc.CalendarWebhook(ctx, req.eventUUID, req.googleChannelID, req.googleResourceState) if err != nil { diff --git a/server/service/campaigns.go b/server/service/campaigns.go index 5066aa59667a..7418fdcea3ef 100644 --- a/server/service/campaigns.go +++ b/server/service/campaigns.go @@ -30,7 +30,7 @@ type createDistributedQueryCampaignResponse struct { func (r createDistributedQueryCampaignResponse) Error() error { return r.Err } -func createDistributedQueryCampaignEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createDistributedQueryCampaignEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createDistributedQueryCampaignRequest) campaign, err := svc.NewDistributedQueryCampaign(ctx, req.QuerySQL, req.QueryID, req.Selected) if err != nil { @@ -185,7 +185,8 @@ type distributedQueryCampaignTargetsByIdentifiers struct { Hosts []string `json:"hosts"` } -func createDistributedQueryCampaignByIdentifierEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createDistributedQueryCampaignByIdentifierEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, + error) { req := request.(*createDistributedQueryCampaignByIdentifierRequest) campaign, err := svc.NewDistributedQueryCampaignByIdentifiers(ctx, req.QuerySQL, req.QueryID, req.Selected.Hosts, req.Selected.Labels) if err != nil { diff --git a/server/service/carves.go b/server/service/carves.go index ca7d71dd9f6d..5ca2e81f9278 100644 --- a/server/service/carves.go +++ b/server/service/carves.go @@ -29,7 +29,7 @@ type listCarvesResponse struct { func (r listCarvesResponse) Error() error { return r.Err } -func listCarvesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listCarvesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listCarvesRequest) carves, err := svc.ListCarves(ctx, req.ListOptions) if err != nil { @@ -66,7 +66,7 @@ type getCarveResponse struct { func (r getCarveResponse) Error() error { return r.Err } -func getCarveEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getCarveEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getCarveRequest) carve, err := svc.GetCarve(ctx, req.ID) if err != nil { @@ -100,7 +100,7 @@ type getCarveBlockResponse struct { func (r getCarveBlockResponse) Error() error { return r.Err } -func getCarveBlockEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getCarveBlockEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getCarveBlockRequest) data, err := svc.GetBlock(ctx, req.ID, req.BlockId) if err != nil { @@ -161,7 +161,7 @@ type carveBeginResponse struct { func (r carveBeginResponse) Error() error { return r.Err } -func carveBeginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func carveBeginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*carveBeginRequest) payload := fleet.CarveBeginPayload{ @@ -256,7 +256,7 @@ type carveBlockResponse struct { func (r carveBlockResponse) Error() error { return r.Err } -func carveBlockEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func carveBlockEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*carveBlockRequest) payload := fleet.CarveBlockPayload{ diff --git a/server/service/devices.go b/server/service/devices.go index f4453223662e..990dc6718f30 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -48,7 +48,7 @@ func (r devicePingResponse) hijackRender(ctx context.Context, w http.ResponseWri // NOTE: we're intentionally not reading the capabilities header in this // endpoint as is unauthenticated and we don't want to trust whatever comes in // there. -func devicePingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func devicePingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { svc.DisableAuthForPing(ctx) return devicePingResponse{}, nil } @@ -81,7 +81,7 @@ func (r *getFleetDesktopRequest) deviceAuthToken() string { // getFleetDesktopEndpoint is meant to be the only API endpoint used by Fleet Desktop. This // endpoint should not include any kind of identifying information about the host. -func getFleetDesktopEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getFleetDesktopEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { sum, err := svc.GetFleetDesktopSummary(ctx) if err != nil { return fleetDesktopResponse{Err: err}, nil @@ -123,7 +123,7 @@ type getDeviceHostResponse struct { func (r getDeviceHostResponse) Error() error { return r.Err } -func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getDeviceHostRequest) host, ok := hostctx.FromContext(ctx) if !ok { @@ -258,7 +258,7 @@ func (r *refetchDeviceHostRequest) deviceAuthToken() string { return r.Token } -func refetchDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func refetchDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) @@ -284,7 +284,7 @@ func (r *listDeviceHostDeviceMappingRequest) deviceAuthToken() string { return r.Token } -func listDeviceHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listDeviceHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) @@ -310,7 +310,7 @@ func (r *getDeviceMacadminsDataRequest) deviceAuthToken() string { return r.Token } -func getDeviceMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getDeviceMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) @@ -343,7 +343,7 @@ type listDevicePoliciesResponse struct { func (r listDevicePoliciesResponse) Error() error { return r.Err } -func listDevicePoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listDevicePoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) @@ -390,7 +390,7 @@ func (r transparencyURLResponse) hijackRender(ctx context.Context, w http.Respon func (r transparencyURLResponse) Error() error { return r.Err } -func transparencyURL(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func transparencyURL(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { config, err := svc.AppConfigObfuscated(ctx) if err != nil { return transparencyURLResponse{Err: err}, nil @@ -449,7 +449,7 @@ type fleetdErrorResponse struct{} func (r fleetdErrorResponse) Error() error { return nil } -func fleetdError(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func fleetdError(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*fleetdErrorRequest) err := svc.LogFleetdError(ctx, req.FleetdError) if err != nil { @@ -513,7 +513,7 @@ func (r getDeviceMDMManualEnrollProfileResponse) hijackRender(ctx context.Contex func (r getDeviceMDMManualEnrollProfileResponse) Error() error { return r.Err } -func getDeviceMDMManualEnrollProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getDeviceMDMManualEnrollProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { // this call ensures that the authentication was done, no need to actually // use the host if _, ok := hostctx.FromContext(ctx); !ok { @@ -592,7 +592,7 @@ func (r deviceMigrateMDMResponse) Error() error { return r.Err } func (r deviceMigrateMDMResponse) Status() int { return http.StatusNoContent } -func migrateMDMDeviceEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func migrateMDMDeviceEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) @@ -629,7 +629,7 @@ func (r triggerLinuxDiskEncryptionEscrowResponse) Error() error { return r.Err } func (r triggerLinuxDiskEncryptionEscrowResponse) Status() int { return http.StatusNoContent } -func triggerLinuxDiskEncryptionEscrowEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func triggerLinuxDiskEncryptionEscrowEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) @@ -668,7 +668,7 @@ type getDeviceSoftwareResponse struct { func (r getDeviceSoftwareResponse) Error() error { return r.Err } -func getDeviceSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getDeviceSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) diff --git a/server/service/endpoint_middleware_test.go b/server/service/endpoint_middleware_test.go index 8c988208be33..ffa0d0614a7c 100644 --- a/server/service/endpoint_middleware_test.go +++ b/server/service/endpoint_middleware_test.go @@ -10,6 +10,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/service/middleware/auth" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" kitlog "github.com/go-kit/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -186,7 +187,7 @@ func TestAuthenticatedHost(t *testing.T) { r := &testNodeKeyRequest{NodeKey: tt.nodeKey} _, err := endpoint(ctx, r) if tt.shouldErr { - assert.IsType(t, &osqueryError{}, err) + assert.IsType(t, &endpoint_utils.OsqueryError{}, err) } else { assert.Nil(t, err) } diff --git a/server/service/endpoint_utils.go b/server/service/endpoint_utils.go index b19363c0fb11..5c00105bc79c 100644 --- a/server/service/endpoint_utils.go +++ b/server/service/endpoint_utils.go @@ -6,10 +6,8 @@ import ( "context" "crypto/x509" "encoding/json" - "errors" "fmt" "io" - "net" "net/http" "net/url" "reflect" @@ -20,63 +18,14 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/service/middleware/auth" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/go-kit/kit/endpoint" kithttp "github.com/go-kit/kit/transport/http" "github.com/go-kit/log" "github.com/gorilla/mux" ) -type handlerFunc func(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) - -// parseTag parses a `url` tag and whether it's optional or not, which is an optional part of the tag -func parseTag(tag string) (string, bool, error) { - parts := strings.Split(tag, ",") - switch len(parts) { - case 0: - return "", false, fmt.Errorf("Error parsing %s: too few parts", tag) - case 1: - return tag, false, nil - case 2: - return parts[0], parts[1] == "optional", nil - default: - return "", false, fmt.Errorf("Error parsing %s: too many parts", tag) - } -} - -type fieldPair struct { - sf reflect.StructField - v reflect.Value -} - -// allFields returns all the fields for a struct, including the ones from embedded structs -func allFields(ifv reflect.Value) []fieldPair { - if ifv.Kind() == reflect.Ptr { - ifv = ifv.Elem() - } - if ifv.Kind() != reflect.Struct { - return nil - } - - var fields []fieldPair - - if !ifv.IsValid() { - return nil - } - - t := ifv.Type() - - for i := 0; i < ifv.NumField(); i++ { - v := ifv.Field(i) - - if v.Kind() == reflect.Struct && t.Field(i).Anonymous { - fields = append(fields, allFields(v)...) - continue - } - fields = append(fields, fieldPair{sf: ifv.Type().Field(i), v: v}) - } - - return fields -} +type handlerFunc func(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) // A value that implements requestDecoder takes control of decoding the request // as a whole - that is, it is responsible for decoding the body and any url @@ -145,7 +94,7 @@ func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc { if r.Header.Get("content-encoding") == "gzip" { gzr, err := gzip.NewReader(buf) if err != nil { - return nil, badRequestErr("gzip decoder error", err) + return nil, endpoint_utils.BadRequestErr("gzip decoder error", err) } defer gzr.Close() body = gzr @@ -154,22 +103,22 @@ func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc { if !isBodyDecoder { req := v.Interface() if err := json.NewDecoder(body).Decode(req); err != nil { - return nil, badRequestErr("json decoder error", err) + return nil, endpoint_utils.BadRequestErr("json decoder error", err) } v = reflect.ValueOf(req) } } - fields := allFields(v) + fields := endpoint_utils.AllFields(v) for _, fp := range fields { - field := fp.v + field := fp.V - urlTagValue, ok := fp.sf.Tag.Lookup("url") + urlTagValue, ok := fp.Sf.Tag.Lookup("url") - optional := false var err error if ok { - urlTagValue, optional, err = parseTag(urlTagValue) + optional := false + urlTagValue, optional, err = endpoint_utils.ParseTag(urlTagValue) if err != nil { return nil, err } @@ -203,109 +152,22 @@ func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc { field.Set(reflect.ValueOf(opts)) default: - switch field.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - v, err := intFromRequest(r, urlTagValue) - if err != nil { - if err == errBadRoute && optional { - continue - } - return nil, badRequestErr("intFromRequest", err) - } - field.SetInt(v) - - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - v, err := uintFromRequest(r, urlTagValue) - if err != nil { - if err == errBadRoute && optional { - continue - } - return nil, badRequestErr("uintFromRequest", err) - } - field.SetUint(v) - - case reflect.String: - v, err := stringFromRequest(r, urlTagValue) - if err != nil { - if err == errBadRoute && optional { - continue - } - return nil, badRequestErr("stringFromRequest", err) - } - field.SetString(v) - - default: - return nil, fmt.Errorf("unsupported type for field %s for 'url' decoding: %s", urlTagValue, field.Kind()) + err := endpoint_utils.DecodeURLTagValue(r, field, urlTagValue, optional) + if err != nil { + return nil, err } + continue } } - _, jsonExpected := fp.sf.Tag.Lookup("json") + _, jsonExpected := fp.Sf.Tag.Lookup("json") if jsonExpected && nilBody { return nil, badRequest("Expected JSON Body") } - queryTagValue, ok := fp.sf.Tag.Lookup("query") - - if ok { - queryTagValue, optional, err = parseTag(queryTagValue) - if err != nil { - return nil, err - } - queryVal := r.URL.Query().Get(queryTagValue) - // if optional and it's a ptr, leave as nil - if queryVal == "" { - if optional { - continue - } - return nil, badRequest(fmt.Sprintf("Param %s is required", fp.sf.Name)) - } - if field.Kind() == reflect.Ptr { - // create the new instance of whatever it is - field.Set(reflect.New(field.Type().Elem())) - field = field.Elem() - } - switch field.Kind() { - case reflect.String: - field.SetString(queryVal) - case reflect.Uint: - queryValUint, err := strconv.Atoi(queryVal) - if err != nil { - return nil, badRequestErr("parsing uint from query", err) - } - field.SetUint(uint64(queryValUint)) //nolint:gosec // dismiss G115 - case reflect.Float64: - queryValFloat, err := strconv.ParseFloat(queryVal, 64) - if err != nil { - return nil, badRequestErr("parsing float from query", err) - } - field.SetFloat(queryValFloat) - case reflect.Bool: - field.SetBool(queryVal == "1" || queryVal == "true") - case reflect.Int: - queryValInt := 0 - switch queryTagValue { - case "order_direction", "inherited_order_direction": - switch queryVal { - case "desc": - queryValInt = int(fleet.OrderDescending) - case "asc": - queryValInt = int(fleet.OrderAscending) - case "": - queryValInt = int(fleet.OrderAscending) - default: - return fleet.ListOptions{}, badRequest("unknown order_direction: " + queryVal) - } - default: - queryValInt, err = strconv.Atoi(queryVal) - if err != nil { - return nil, badRequestErr("parsing int from query", err) - } - } - field.SetInt(int64(queryValInt)) - default: - return nil, fmt.Errorf("Cant handle type for field %s %s", fp.sf.Name, field.Kind()) - } + err = endpoint_utils.DecodeQueryTagValue(r, fp) + if err != nil { + return nil, err } } @@ -323,15 +185,15 @@ func makeDecoder(iface interface{}) kithttp.DecodeRequestFunc { if !license.IsPremium(ctx) { for _, fp := range fields { - if prem, ok := fp.sf.Tag.Lookup("premium"); ok { + if prem, ok := fp.Sf.Tag.Lookup("premium"); ok { val, err := strconv.ParseBool(prem) if err != nil { return nil, err } - if val && !fp.v.IsZero() { + if val && !fp.V.IsZero() { return nil, &fleet.BadRequestError{Message: fmt.Sprintf( "option %s requires a premium license", - fp.sf.Name, + fp.Sf.Name, )} } continue @@ -347,18 +209,6 @@ func badRequest(msg string) error { return &fleet.BadRequestError{Message: msg} } -func badRequestErr(publicMsg string, internalErr error) error { - // ensure timeout errors don't become BadRequestErrors. - var opErr *net.OpError - if errors.As(internalErr, &opErr) { - return fmt.Errorf(publicMsg+", internal: %w", internalErr) - } - return &fleet.BadRequestError{ - Message: publicMsg, - InternalErr: internalErr, - } -} - type authEndpointer struct { svc fleet.Service opts []kithttp.ServerOption diff --git a/server/service/endpoint_utils_test.go b/server/service/endpoint_utils_test.go index 994d43194a92..17775430c253 100644 --- a/server/service/endpoint_utils_test.go +++ b/server/service/endpoint_utils_test.go @@ -15,6 +15,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service/middleware/auth" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/go-kit/kit/endpoint" kithttp "github.com/go-kit/kit/transport/http" kitlog "github.com/go-kit/log" @@ -287,8 +288,8 @@ func TestEndpointer(t *testing.T) { kithttp.PopulateRequestContext, // populate the request context with common fields auth.SetRequestsContexts(svc), ), - kithttp.ServerErrorHandler(&errorHandler{kitlog.NewNopLogger()}), - kithttp.ServerErrorEncoder(encodeError), + kithttp.ServerErrorHandler(&endpoint_utils.ErrorHandler{Logger: kitlog.NewNopLogger()}), + kithttp.ServerErrorEncoder(endpoint_utils.EncodeError), kithttp.ServerAfter( kithttp.SetContentType("application/json; charset=utf-8"), logRequestEnd(kitlog.NewNopLogger()), @@ -297,11 +298,11 @@ func TestEndpointer(t *testing.T) { } e := newUserAuthenticatedEndpointer(svc, fleetAPIOptions, r, "v1", "2021-11") - nopHandler := func(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + nopHandler := func(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { setAuthCheckedOnPreAuthErr(ctx) return stringErrorer("nop"), nil } - overrideHandler := func(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + overrideHandler := func(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { setAuthCheckedOnPreAuthErr(ctx) return stringErrorer("override"), nil } @@ -407,8 +408,8 @@ func TestEndpointerCustomMiddleware(t *testing.T) { kithttp.PopulateRequestContext, auth.SetRequestsContexts(svc), ), - kithttp.ServerErrorHandler(&errorHandler{kitlog.NewNopLogger()}), - kithttp.ServerErrorEncoder(encodeError), + kithttp.ServerErrorHandler(&endpoint_utils.ErrorHandler{Logger: kitlog.NewNopLogger()}), + kithttp.ServerErrorEncoder(endpoint_utils.EncodeError), kithttp.ServerAfter( kithttp.SetContentType("application/json; charset=utf-8"), logRequestEnd(kitlog.NewNopLogger()), @@ -418,7 +419,7 @@ func TestEndpointerCustomMiddleware(t *testing.T) { var buf bytes.Buffer e := newNoAuthEndpointer(svc, fleetAPIOptions, r, "v1") - e.GET("/none/", func(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + e.GET("/none/", func(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { buf.WriteString("H1") return nil, nil }, nil) @@ -443,7 +444,7 @@ func TestEndpointerCustomMiddleware(t *testing.T) { } }, ). - GET("/mw/", func(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + GET("/mw/", func(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { buf.WriteString("H2") return nil, nil }, nil) diff --git a/server/service/global_policies.go b/server/service/global_policies.go index 4946d509fb02..f600bbbcbb5a 100644 --- a/server/service/global_policies.go +++ b/server/service/global_policies.go @@ -41,7 +41,7 @@ type globalPolicyResponse struct { func (r globalPolicyResponse) Error() error { return r.Err } -func globalPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func globalPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*globalPolicyRequest) resp, err := svc.NewGlobalPolicy(ctx, fleet.PolicyPayload{ QueryID: req.QueryID, @@ -105,7 +105,7 @@ type listGlobalPoliciesResponse struct { func (r listGlobalPoliciesResponse) Error() error { return r.Err } -func listGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listGlobalPoliciesRequest) resp, err := svc.ListGlobalPolicies(ctx, req.Opts) if err != nil { @@ -137,7 +137,7 @@ type getPolicyByIDResponse struct { func (r getPolicyByIDResponse) Error() error { return r.Err } -func getPolicyByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getPolicyByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getPolicyByIDRequest) policy, err := svc.GetPolicyByIDQueries(ctx, req.PolicyID) if err != nil { @@ -179,7 +179,7 @@ type countGlobalPoliciesResponse struct { func (r countGlobalPoliciesResponse) Error() error { return r.Err } -func countGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func countGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*countGlobalPoliciesRequest) resp, err := svc.CountGlobalPolicies(ctx, req.ListOptions.MatchQuery) if err != nil { @@ -216,7 +216,7 @@ type deleteGlobalPoliciesResponse struct { func (r deleteGlobalPoliciesResponse) Error() error { return r.Err } -func deleteGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteGlobalPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteGlobalPoliciesRequest) resp, err := svc.DeleteGlobalPolicies(ctx, req.IDs) if err != nil { @@ -317,7 +317,7 @@ type modifyGlobalPolicyResponse struct { func (r modifyGlobalPolicyResponse) Error() error { return r.Err } -func modifyGlobalPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyGlobalPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyGlobalPolicyRequest) resp, err := svc.ModifyGlobalPolicy(ctx, req.PolicyID, req.ModifyPolicyPayload) if err != nil { @@ -345,7 +345,7 @@ type resetAutomationResponse struct { func (r resetAutomationResponse) Error() error { return r.Err } -func resetAutomationEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func resetAutomationEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*resetAutomationRequest) err := svc.ResetAutomation(ctx, req.TeamIDs, req.PolicyIDs) return resetAutomationResponse{Err: err}, nil @@ -476,7 +476,7 @@ type applyPolicySpecsResponse struct { func (r applyPolicySpecsResponse) Error() error { return r.Err } -func applyPolicySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func applyPolicySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*applyPolicySpecsRequest) err := svc.ApplyPolicySpecs(ctx, req.Specs) if err != nil { @@ -582,7 +582,7 @@ func (a autofillPoliciesResponse) Error() error { return a.Err } -func autofillPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func autofillPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*autofillPoliciesRequest) description, resolution, err := svc.AutofillPolicySql(ctx, req.SQL) return autofillPoliciesResponse{Description: description, Resolution: resolution, Err: err}, nil diff --git a/server/service/global_schedule.go b/server/service/global_schedule.go index 8840f86261e8..fdb1925a92f9 100644 --- a/server/service/global_schedule.go +++ b/server/service/global_schedule.go @@ -23,7 +23,7 @@ type getGlobalScheduleResponse struct { func (r getGlobalScheduleResponse) Error() error { return r.Err } -func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getGlobalScheduleRequest) gp, err := svc.GetGlobalScheduledQueries(ctx, req.ListOptions) @@ -69,7 +69,7 @@ type globalScheduleQueryResponse struct { func (r globalScheduleQueryResponse) Error() error { return r.Err } -func globalScheduleQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func globalScheduleQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*globalScheduleQueryRequest) scheduled, err := svc.GlobalScheduleQuery(ctx, &fleet.ScheduledQuery{ @@ -121,7 +121,7 @@ type modifyGlobalScheduleResponse struct { func (r modifyGlobalScheduleResponse) Error() error { return r.Err } -func modifyGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyGlobalScheduleRequest) sq, err := svc.ModifyGlobalScheduledQueries(ctx, req.ID, req.ScheduledQueryPayload) @@ -156,7 +156,7 @@ type deleteGlobalScheduleResponse struct { func (r deleteGlobalScheduleResponse) Error() error { return r.Err } -func deleteGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteGlobalScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteGlobalScheduleRequest) err := svc.DeleteGlobalScheduledQueries(ctx, req.ID) if err != nil { diff --git a/server/service/handler.go b/server/service/handler.go index 74b8528e71db..3b5970bb3bd3 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -3,7 +3,6 @@ package service import ( "context" "encoding/json" - "errors" "fmt" "net/http" "os" @@ -27,6 +26,7 @@ import ( scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server" "github.com/fleetdm/fleet/v4/server/service/middleware/auth" "github.com/fleetdm/fleet/v4/server/service/middleware/authzcheck" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/service/middleware/mdmconfigured" "github.com/fleetdm/fleet/v4/server/service/middleware/ratelimit" "github.com/go-kit/kit/endpoint" @@ -44,39 +44,6 @@ import ( microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" ) -type errorHandler struct { - logger kitlog.Logger -} - -func (h *errorHandler) Handle(ctx context.Context, err error) { - // get the request path - path, _ := ctx.Value(kithttp.ContextKeyRequestPath).(string) - logger := level.Info(kitlog.With(h.logger, "path", path)) - - var ewi fleet.ErrWithInternal - if errors.As(err, &ewi) { - logger = kitlog.With(logger, "internal", ewi.Internal()) - } - - var ewlf fleet.ErrWithLogFields - if errors.As(err, &ewlf) { - logger = kitlog.With(logger, ewlf.LogFields()...) - } - - var uuider fleet.ErrorUUIDer - if errors.As(err, &uuider) { - logger = kitlog.With(logger, "uuid", uuider.UUID()) - } - - var rle ratelimit.Error - if errors.As(err, &rle) { - res := rle.Result() - logger.Log("err", "limit exceeded", "retry_after", res.RetryAfter) - } else { - logger.Log("err", err) - } -} - func logRequestEnd(logger kitlog.Logger) func(context.Context, http.ResponseWriter) context.Context { return func(ctx context.Context, w http.ResponseWriter) context.Context { logCtx, ok := logging.FromContext(ctx) @@ -121,6 +88,7 @@ func MakeHandler( config config.FleetConfig, logger kitlog.Logger, limitStore throttled.GCRAStore, + featureRoutes []endpoint_utils.HandlerRoutesFunc, extra ...ExtraHandlerOption, ) http.Handler { var eopts extraHandlerOpts @@ -133,8 +101,8 @@ func MakeHandler( kithttp.PopulateRequestContext, // populate the request context with common fields auth.SetRequestsContexts(svc), ), - kithttp.ServerErrorHandler(&errorHandler{logger}), - kithttp.ServerErrorEncoder(encodeError), + kithttp.ServerErrorHandler(&endpoint_utils.ErrorHandler{Logger: logger}), + kithttp.ServerErrorEncoder(endpoint_utils.EncodeError), kithttp.ServerAfter( kithttp.SetContentType("application/json; charset=utf-8"), logRequestEnd(logger), @@ -154,6 +122,9 @@ func MakeHandler( r.Use(publicIP) attachFleetAPIRoutes(r, svc, config, logger, limitStore, fleetAPIOptions, eopts) + for _, featureRoute := range featureRoutes { + featureRoute(r, fleetAPIOptions) + } addMetrics(r) return r @@ -161,7 +132,7 @@ func MakeHandler( func publicIP(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ip := extractIP(r) + ip := endpoint_utils.ExtractIP(r) if ip != "" { r.RemoteAddr = ip } diff --git a/server/service/handler_test.go b/server/service/handler_test.go index 116b155ac99a..b18abb0f72f9 100644 --- a/server/service/handler_test.go +++ b/server/service/handler_test.go @@ -26,7 +26,7 @@ func TestAPIRoutesConflicts(t *testing.T) { svc, _ := newTestService(t, ds, nil, nil) limitStore, _ := memstore.New(0) cfg := config.TestConfig() - h := MakeHandler(svc, cfg, kitlog.NewNopLogger(), limitStore) + h := MakeHandler(svc, cfg, kitlog.NewNopLogger(), limitStore, nil) router := h.(*mux.Router) type testCase struct { @@ -80,7 +80,7 @@ func TestAPIRoutesMetrics(t *testing.T) { svc, _ := newTestService(t, ds, nil, nil) limitStore, _ := memstore.New(0) - h := MakeHandler(svc, config.TestConfig(), kitlog.NewNopLogger(), limitStore) + h := MakeHandler(svc, config.TestConfig(), kitlog.NewNopLogger(), limitStore, nil) router := h.(*mux.Router) // replace all handlers with mocks, and collect the requests to make to each diff --git a/server/service/hosts.go b/server/service/hosts.go index 006df7434f0f..3cfe056d457f 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -28,6 +28,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/assets" mdmlifecycle "github.com/fleetdm/fleet/v4/server/mdm/lifecycle" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/worker" "github.com/go-kit/log/level" "github.com/gocarina/gocsv" @@ -89,7 +90,7 @@ type listHostsResponse struct { func (r listHostsResponse) Error() error { return r.Err } -func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listHostsRequest) var software *fleet.Software @@ -276,7 +277,7 @@ func (r deleteHostsResponse) Error() error { return r.Err } // Status implements statuser interface to send out custom HTTP success codes. func (r deleteHostsResponse) Status() int { return r.StatusCode } -func deleteHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteHostsRequest) // Since bulk deletes can take a long time, after DeleteHostsTimeout, we will return a 202 (Accepted) status code @@ -395,7 +396,7 @@ type countHostsResponse struct { func (r countHostsResponse) Error() error { return r.Err } -func countHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func countHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*countHostsRequest) count, err := svc.CountHosts(ctx, req.LabelID, req.Opts) if err != nil { @@ -460,7 +461,7 @@ type searchHostsResponse struct { func (r searchHostsResponse) Error() error { return r.Err } -func searchHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func searchHostsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*searchHostsRequest) hosts, err := svc.SearchHosts(ctx, req.MatchQuery, req.QueryID, req.ExcludedHostIDs) @@ -528,7 +529,7 @@ type getHostResponse struct { func (r getHostResponse) Error() error { return r.Err } -func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostRequest) opts := fleet.HostDetailOptions{ IncludeCVEScores: false, @@ -633,7 +634,7 @@ type getHostSummaryResponse struct { func (r getHostSummaryResponse) Error() error { return r.Err } -func getHostSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostSummaryRequest) summary, err := svc.GetHostSummary(ctx, req.TeamID, req.Platform, req.LowDiskSpace) if err != nil { @@ -708,7 +709,7 @@ type hostByIdentifierRequest struct { ExcludeSoftware bool `query:"exclude_software,optional"` } -func hostByIdentifierEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func hostByIdentifierEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*hostByIdentifierRequest) opts := fleet.HostDetailOptions{ IncludeCVEScores: false, @@ -767,7 +768,7 @@ type deleteHostResponse struct { func (r deleteHostResponse) Error() error { return r.Err } -func deleteHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteHostRequest) err := svc.DeleteHost(ctx, req.ID) if err != nil { @@ -824,7 +825,7 @@ type addHostsToTeamResponse struct { func (r addHostsToTeamResponse) Error() error { return r.Err } -func addHostsToTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func addHostsToTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*addHostsToTeamRequest) err := svc.AddHostsToTeam(ctx, req.TeamID, req.HostIDs, false) if err != nil { @@ -943,7 +944,7 @@ type addHostsToTeamByFilterResponse struct { func (r addHostsToTeamByFilterResponse) Error() error { return r.Err } -func addHostsToTeamByFilterEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func addHostsToTeamByFilterEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*addHostsToTeamByFilterRequest) err := svc.AddHostsToTeamByFilter(ctx, req.TeamID, req.Filters) if err != nil { @@ -1021,7 +1022,7 @@ func (r refetchHostResponse) Error() error { return r.Err } -func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*refetchHostRequest) err := svc.RefetchHost(ctx, req.ID) if err != nil { @@ -1336,7 +1337,7 @@ type getHostQueryReportResponse struct { func (r getHostQueryReportResponse) Error() error { return r.Err } -func getHostQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostQueryReportRequest) // Need to return hostname in response even if there are no report results @@ -1456,7 +1457,7 @@ type listHostDeviceMappingResponse struct { func (r listHostDeviceMappingResponse) Error() error { return r.Err } -func listHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listHostDeviceMappingRequest) dms, err := svc.ListHostDeviceMapping(ctx, req.ID) if err != nil { @@ -1502,7 +1503,7 @@ type putHostDeviceMappingResponse struct { func (r putHostDeviceMappingResponse) Error() error { return r.Err } -func putHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func putHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*putHostDeviceMappingRequest) dms, err := svc.SetCustomHostDeviceMapping(ctx, req.ID, req.Email) if err != nil { @@ -1551,7 +1552,7 @@ type getHostMDMResponse struct { func (r getHostMDMResponse) Error() error { return r.Err } -func getHostMDM(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostMDM(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostMDMRequest) mdm, err := svc.MDMData(ctx, req.ID) if err != nil { @@ -1572,7 +1573,7 @@ type getHostMDMSummaryRequest struct { func (r getHostMDMSummaryResponse) Error() error { return r.Err } -func getHostMDMSummary(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostMDMSummary(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostMDMSummaryRequest) resp := getHostMDMSummaryResponse{} var err error @@ -1599,7 +1600,7 @@ type getMacadminsDataResponse struct { func (r getMacadminsDataResponse) Error() error { return r.Err } -func getMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMacadminsDataRequest) data, err := svc.MacadminsData(ctx, req.ID) if err != nil { @@ -1676,7 +1677,7 @@ type getAggregatedMacadminsDataResponse struct { func (r getAggregatedMacadminsDataResponse) Error() error { return r.Err } -func getAggregatedMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getAggregatedMacadminsDataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getAggregatedMacadminsDataRequest) data, err := svc.AggregatedMacadminsData(ctx, req.TeamID) if err != nil { @@ -1832,7 +1833,7 @@ func (r hostsReportResponse) hijackRender(ctx context.Context, w http.ResponseWr var buf bytes.Buffer if err := gocsv.Marshal(r.Hosts, &buf); err != nil { logging.WithErr(ctx, err) - encodeError(ctx, ctxerr.New(ctx, "failed to generate CSV file"), w) + endpoint_utils.EncodeError(ctx, ctxerr.New(ctx, "failed to generate CSV file"), w) return } @@ -1844,7 +1845,7 @@ func (r hostsReportResponse) hijackRender(ctx context.Context, w http.ResponseWr recs, err := csv.NewReader(&buf).ReadAll() if err != nil { logging.WithErr(ctx, err) - encodeError(ctx, ctxerr.New(ctx, "failed to generate CSV file"), w) + endpoint_utils.EncodeError(ctx, ctxerr.New(ctx, "failed to generate CSV file"), w) return } @@ -1865,7 +1866,7 @@ func (r hostsReportResponse) hijackRender(ctx context.Context, w http.ResponseWr // duplicating the list of columns from the Host's struct tags to a // map and keep this in sync, for what is essentially a programmer // mistake that should be caught and corrected early. - encodeError(ctx, &fleet.BadRequestError{Message: fmt.Sprintf("invalid column name: %q", col)}, w) + endpoint_utils.EncodeError(ctx, &fleet.BadRequestError{Message: fmt.Sprintf("invalid column name: %q", col)}, w) return } outRows[i] = append(outRows[i], rec[colIx]) @@ -1890,7 +1891,7 @@ func (r hostsReportResponse) hijackRender(ctx context.Context, w http.ResponseWr } } -func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*hostsReportRequest) // for now, only csv format is allowed @@ -1973,7 +1974,7 @@ type osVersionsResponse struct { func (r osVersionsResponse) Error() error { return r.Err } -func osVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func osVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*osVersionsRequest) osVersions, count, metadata, err := svc.OSVersions(ctx, req.TeamID, req.Platform, req.Name, req.Version, req.ListOptions, false) @@ -2103,7 +2104,7 @@ type getOSVersionResponse struct { func (r getOSVersionResponse) Error() error { return r.Err } -func getOSVersionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getOSVersionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getOSVersionRequest) osVersion, updateTime, err := svc.OSVersion(ctx, req.ID, req.TeamID, false) @@ -2205,7 +2206,7 @@ type getHostEncryptionKeyResponse struct { func (r getHostEncryptionKeyResponse) Error() error { return r.Err } -func getHostEncryptionKey(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostEncryptionKey(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostEncryptionKeyRequest) key, err := svc.HostEncryptionKey(ctx, req.ID) if err != nil { @@ -2331,7 +2332,7 @@ type getHostHealthResponse struct { func (r getHostHealthResponse) Error() error { return r.Err } -func getHostHealthEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostHealthEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostHealthRequest) hh, err := svc.GetHostHealth(ctx, req.ID) if err != nil { @@ -2463,7 +2464,7 @@ type addLabelsToHostResponse struct { func (r addLabelsToHostResponse) Error() error { return r.Err } -func addLabelsToHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func addLabelsToHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*addLabelsToHostRequest) if err := svc.AddLabelsToHost(ctx, req.ID, req.Labels); err != nil { return addLabelsToHostResponse{Err: err}, nil @@ -2508,7 +2509,7 @@ type removeLabelsFromHostResponse struct { func (r removeLabelsFromHostResponse) Error() error { return r.Err } -func removeLabelsFromHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func removeLabelsFromHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*removeLabelsFromHostRequest) if err := svc.RemoveLabelsFromHost(ctx, req.ID, req.Labels); err != nil { return removeLabelsFromHostResponse{Err: err}, nil @@ -2640,7 +2641,7 @@ type getHostSoftwareResponse struct { func (r getHostSoftwareResponse) Error() error { return r.Err } -func getHostSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostSoftwareRequest) res, meta, err := svc.ListHostSoftware(ctx, req.ID, req.HostSoftwareTitleListOptions) if err != nil { diff --git a/server/service/http_publicip.go b/server/service/http_publicip.go deleted file mode 100644 index ba8b289f7915..000000000000 --- a/server/service/http_publicip.go +++ /dev/null @@ -1,33 +0,0 @@ -package service - -import ( - "net/http" - "strings" -) - -// copied from https://github.com/go-chi/chi/blob/c97bc988430d623a14f50b7019fb40529036a35a/middleware/realip.go#L42 - -var trueClientIP = http.CanonicalHeaderKey("True-Client-IP") -var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") -var xRealIP = http.CanonicalHeaderKey("X-Real-IP") - -func extractIP(r *http.Request) string { - ip := r.RemoteAddr - if i := strings.LastIndexByte(ip, ':'); i != -1 { - ip = ip[:i] - } - - if tcip := r.Header.Get(trueClientIP); tcip != "" { - ip = tcip - } else if xrip := r.Header.Get(xRealIP); xrip != "" { - ip = xrip - } else if xff := r.Header.Get(xForwardedFor); xff != "" { - i := strings.Index(xff, ",") - if i == -1 { - i = len(xff) - } - ip = xff[:i] - } - - return ip -} diff --git a/server/service/installer.go b/server/service/installer.go index cebb5c73f60e..9299af073123 100644 --- a/server/service/installer.go +++ b/server/service/installer.go @@ -11,6 +11,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/gorilla/mux" ) @@ -27,7 +28,7 @@ type getInstallerRequest struct { func (getInstallerRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) { k, ok := mux.Vars(r)["kind"] if !ok { - return "", errBadRoute + return "", endpoint_utils.ErrBadRoute } return getInstallerRequest{ @@ -65,7 +66,7 @@ func (r getInstallerResponse) hijackRender(ctx context.Context, w http.ResponseW r.fileReader.Close() } -func getInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(getInstallerRequest) fileReader, fileLength, err := svc.GetInstaller(ctx, fleet.Installer{ @@ -123,7 +124,7 @@ type checkInstallerResponse struct { func (r checkInstallerResponse) Error() error { return r.Err } -func checkInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func checkInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*checkInstallerRequest) err := svc.CheckInstallerExistence(ctx, fleet.Installer{ diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index d40207ae8e87..ff3b4c4bbf11 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -28,6 +28,7 @@ import ( "github.com/fleetdm/fleet/v4/server/live_query/live_query_mock" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service/async" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/service/osquery_utils" "github.com/fleetdm/fleet/v4/server/test" "github.com/ghodss/yaml" @@ -9750,7 +9751,7 @@ func (s *integrationTestSuite) TestTryingToEnrollWithTheWrongSecret() { }) require.NoError(t, err) - var resp jsonError + var resp endpoint_utils.JsonError s.DoJSON("POST", "/api/fleet/orbit/enroll", EnrollOrbitRequest{ EnrollSecret: uuid.New().String(), HardwareUUID: h.UUID, diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index abc729f0c834..f1bf7b184e0f 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -105,6 +105,7 @@ type integrationMDMTestSuite struct { appleVPPConfigSrv *httptest.Server appleVPPConfigSrvConfig *appleVPPConfigSrvConf appleITunesSrv *httptest.Server + appleITunesSrvData map[string]string appleGDMFSrv *httptest.Server mockedDownloadFleetdmMeta fleetdbase.Metadata } @@ -506,27 +507,28 @@ func (s *integrationMDMTestSuite) SetupSuite() { _, _ = w.Write(resp) })) - s.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, + s.appleITunesSrvData = 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, + // ipados app + "3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3, "supportedDevices": ["iPadAir-iPadAir"] }`, - "4": `{"bundleId": "d-4", "artworkUrl512": "https://example.com/images/4", "version": "4.0.0", "trackName": "App 4", "TrackID": 4}`, - } + "4": `{"bundleId": "d-4", "artworkUrl512": "https://example.com/images/4", "version": "4.0.0", "trackName": "App 4", "TrackID": 4}`, + } + + s.appleITunesSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // a map of apps we can respond with adamIDString := r.URL.Query().Get("id") adamIDs := strings.Split(adamIDString, ",") var objs []string for _, a := range adamIDs { - objs = append(objs, db[a]) + objs = append(objs, s.appleITunesSrvData[a]) } _, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ",")))) @@ -690,6 +692,11 @@ func (s *integrationMDMTestSuite) TearDownTest() { _, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_scripts;") return err }) + + mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error { + _, err := tx.ExecContext(ctx, "DELETE FROM vpp_apps;") + return err + }) } func (s *integrationMDMTestSuite) mockDEPResponse(orgName string, handler http.Handler) { @@ -11183,6 +11190,102 @@ func (s *integrationMDMTestSuite) TestVPPApps() { s.lastActivityMatches(fleet.ActivityDeletedAppStoreApp{}.ActivityName(), fmt.Sprintf(activityData, team.Name, excludeAnyApp.Name, excludeAnyApp.AdamID, team.ID, excludeAnyApp.Platform, l2.ID, l2.Name), 0) + + // iOS and iPadOS + + iOSApp := fleet.VPPApp{ + VPPAppTeam: fleet.VPPAppTeam{ + VPPAppID: fleet.VPPAppID{ + AdamID: "2", + Platform: fleet.IOSPlatform, + }, + }, + Name: "App 2", + BundleIdentifier: "b-2", + IconURL: "https://example.com/images/2", + LatestVersion: "2.0.0", + } + + iPadOSApp := fleet.VPPApp{ + VPPAppTeam: fleet.VPPAppTeam{ + VPPAppID: fleet.VPPAppID{ + AdamID: "2", + Platform: fleet.IPadOSPlatform, + }, + }, + Name: "App 2", + BundleIdentifier: "b-2", + IconURL: "https://example.com/images/2", + LatestVersion: "2.0.0", + } + + testCases := []struct { + desc string + app fleet.VPPApp + addAppReq *addAppStoreAppRequest + }{ + { + desc: "ios device", + app: iOSApp, + addAppReq: &addAppStoreAppRequest{ + TeamID: &team.ID, + AppStoreID: iOSApp.AdamID, + LabelsIncludeAny: []string{l2.Name}, + Platform: iOSApp.Platform, + }, + }, + + { + desc: "ipados device", + app: iPadOSApp, + addAppReq: &addAppStoreAppRequest{ + TeamID: &team.ID, + AppStoreID: iPadOSApp.AdamID, + LabelsIncludeAny: []string{l2.Name}, + Platform: iPadOSApp.Platform, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + mobileHost, mobileMDMDevice := s.createAppleMobileHostThenEnrollMDM(string(tc.app.Platform)) + s.Do("POST", "/api/latest/fleet/hosts/transfer", &addHostsToTeamRequest{HostIDs: []uint{mobileHost.ID}, TeamID: &team.ID}, http.StatusOK) + + s.Do("POST", "/api/latest/fleet/software/app_store_apps", tc.addAppReq, http.StatusOK) + titleID = getSoftwareTitleIDFromApp(&tc.app) + s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mobileMDMDevice.SerialNumber) + + // Should fail because label is include_any and the host doesn't have it + res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mobileHost.ID, titleID), &installSoftwareRequest{}, http.StatusBadRequest) + require.Contains(t, extractServerErrorText(res.Body), "Couldn't install. This host isn't a member of the labels defined for this software title.") + + // Create a new label + s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: "label3" + t.Name(), Hosts: []string{mobileMDMDevice.SerialNumber}}, http.StatusOK, &createLabelResp) + l3 := createLabelResp.Label + require.NotNil(t, l3) + + // Should fail because label is exclude_any and the host does have it + updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, LabelsExcludeAny: []string{l3.Name}} + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK, &updateAppResp) + res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mobileHost.ID, titleID), &installSoftwareRequest{}, http.StatusBadRequest) + require.Contains(t, extractServerErrorText(res.Body), "Couldn't install. This host isn't a member of the labels defined for this software title.") + + // Should succeed because the label is include_any and the host does have it + updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, LabelsIncludeAny: []string{l3.Name}} + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK, &updateAppResp) + s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mobileHost.ID, titleID), &installSoftwareRequest{}, http.StatusAccepted) + + // Create a new host + mobileHost, mobileMDMDevice = s.createAppleMobileHostThenEnrollMDM(string(tc.app.Platform)) + s.Do("POST", "/api/latest/fleet/hosts/transfer", &addHostsToTeamRequest{HostIDs: []uint{mobileHost.ID}, TeamID: &team.ID}, http.StatusOK) + s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mobileMDMDevice.SerialNumber) + + // Should succeed because the label is exclude_any and the iOS host doesn't have it + updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, LabelsExcludeAny: []string{l3.Name}} + s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK, &updateAppResp) + s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mobileHost.ID, titleID), &installSoftwareRequest{}, http.StatusAccepted) + }) + } }) // Create a team @@ -13546,3 +13649,126 @@ func (s *integrationMDMTestSuite) TestVPPPolicyAutomationLabelScopingRetrigger() require.Equal(t, uint(0), policy1.PassingHostCount) require.Equal(t, uint(1), policy1.FailingHostCount) } + +func (s *integrationMDMTestSuite) TestRefreshVPPAppVersions() { + t := s.T() + ctx := context.Background() + + // Reset the iTunes data to what it was before this test + t.Cleanup(func() { + s.appleITunesSrvData = 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"] }`, + + "4": `{"bundleId": "d-4", "artworkUrl512": "https://example.com/images/4", "version": "4.0.0", "trackName": "App 4", "TrackID": 4}`, + } + }) + + // Set up 3 apps - macOS, iOS, and iPadOS + s.appleITunesSrvData = map[string]string{ + "1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`, + "2": `{"bundleId": "d-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2, + "supportedDevices": ["iPhone5s-iPhone5s"] }`, + "3": `{"bundleId": "b-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3, + "supportedDevices": ["iPadAir-iPadAir"] }`, + } + + var newTeamResp teamResponse + s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1" + t.Name())}}, http.StatusOK, &newTeamResp) + team := newTeamResp.Team + + // Set up VPP token + orgName := "Fleet Device Management Inc." + token := "mycooltoken" + expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second) + expDate := expTime.Format(fleet.VPPTimeFormat) + tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName) + t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL) + var validToken uploadVPPTokenResponse + s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken) + + // Get the token + var resp getVPPTokensResponse + s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp) + require.NoError(t, resp.Err) + + // 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{team.ID}}, http.StatusOK, &resPatchVPP) + + var appResp getAppStoreAppsResponse + s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", &getAppStoreAppsRequest{}, http.StatusOK, &appResp, "team_id", + fmt.Sprint(team.ID)) + require.NoError(t, appResp.Err) + app1 := appResp.AppStoreApps[0] + app2 := appResp.AppStoreApps[1] + app3 := appResp.AppStoreApps[2] + + // Add 3 apps: 2 will have version changes, the 3rd will not + var addAppResp addAppStoreAppResponse + s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{ + TeamID: &team.ID, + Platform: app1.Platform, + AppStoreID: app1.AdamID, + }, http.StatusOK, &addAppResp) + + s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{ + TeamID: &team.ID, + Platform: app2.Platform, + AppStoreID: app2.AdamID, + }, http.StatusOK, &addAppResp) + + s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{ + TeamID: &team.ID, + Platform: app3.Platform, + AppStoreID: app3.AdamID, + }, http.StatusOK, &addAppResp) + + // Check versions before refresh + var listSWTitlesResp listSoftwareTitlesResponse + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app1.Name) + require.Len(t, listSWTitlesResp.SoftwareTitles, 1) + require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp) + require.Equal(t, "1.0.0", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version) + + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app2.Name) + require.Len(t, listSWTitlesResp.SoftwareTitles, 1) + require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp) + require.Equal(t, "2.0.0", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version) + + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app3.Name) + require.Len(t, listSWTitlesResp.SoftwareTitles, 1) + require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp) + require.Equal(t, "3.0.0", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version) + + // "update" the versions + s.appleITunesSrvData["1"] = `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "9.9.9", "trackName": "App 1", "TrackID": 1}` + s.appleITunesSrvData["2"] = `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "10.10.10", "trackName": "App 2", "TrackID": 2, + "supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }` + + err := vpp.RefreshVersions(ctx, s.ds) + require.NoError(t, err) + + // 1 and 2 should be updated + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app1.Name) + require.Len(t, listSWTitlesResp.SoftwareTitles, 1) + require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp) + require.Equal(t, "9.9.9", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version) + + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app2.Name) + require.Len(t, listSWTitlesResp.SoftwareTitles, 1) + require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp) + require.Equal(t, "10.10.10", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version) + + // No change + s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app3.Name) + require.Len(t, listSWTitlesResp.SoftwareTitles, 1) + require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp) + require.Equal(t, "3.0.0", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version) +} diff --git a/server/service/invites.go b/server/service/invites.go index 2aac9f6c5d3d..9f5c1ff763d2 100644 --- a/server/service/invites.go +++ b/server/service/invites.go @@ -30,7 +30,7 @@ type createInviteResponse struct { func (r createInviteResponse) Error() error { return r.Err } -func createInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createInviteRequest) invite, err := svc.InviteNewUser(ctx, req.InvitePayload) if err != nil { @@ -170,7 +170,7 @@ type listInvitesResponse struct { func (r listInvitesResponse) Error() error { return r.Err } -func listInvitesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listInvitesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listInvitesRequest) invites, err := svc.ListInvites(ctx, req.ListOptions) if err != nil { @@ -207,7 +207,7 @@ type updateInviteResponse struct { func (r updateInviteResponse) Error() error { return r.Err } -func updateInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func updateInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateInviteRequest) invite, err := svc.UpdateInvite(ctx, req.ID, req.InvitePayload) if err != nil { @@ -290,7 +290,7 @@ type deleteInviteResponse struct { func (r deleteInviteResponse) Error() error { return r.Err } -func deleteInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteInviteRequest) err := svc.DeleteInvite(ctx, req.ID) if err != nil { @@ -321,7 +321,7 @@ type verifyInviteResponse struct { func (r verifyInviteResponse) Error() error { return r.Err } -func verifyInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func verifyInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*verifyInviteRequest) invite, err := svc.VerifyInvite(ctx, req.Token) if err != nil { diff --git a/server/service/labels.go b/server/service/labels.go index 09e8c73aa329..b4f26be3ab11 100644 --- a/server/service/labels.go +++ b/server/service/labels.go @@ -28,7 +28,7 @@ type createLabelResponse struct { func (r createLabelResponse) Error() error { return r.Err } -func createLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createLabelRequest) label, hostIDs, err := svc.NewLabel(ctx, req.LabelPayload) @@ -115,7 +115,7 @@ type modifyLabelResponse struct { func (r modifyLabelResponse) Error() error { return r.Err } -func modifyLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyLabelRequest) label, hostIDs, err := svc.ModifyLabel(ctx, req.ID, req.ModifyLabelPayload) if err != nil { @@ -203,7 +203,7 @@ type getLabelResponse struct { func (r getLabelResponse) Error() error { return r.Err } -func getLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getLabelRequest) label, hostIDs, err := svc.GetLabel(ctx, req.ID) if err != nil { @@ -244,7 +244,7 @@ type listLabelsResponse struct { func (r listLabelsResponse) Error() error { return r.Err } -func listLabelsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listLabelsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listLabelsRequest) labels, err := svc.ListLabels(ctx, req.ListOptions) @@ -302,7 +302,7 @@ type getLabelsSummaryResponse struct { func (r getLabelsSummaryResponse) Error() error { return r.Err } -func getLabelsSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getLabelsSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { labels, err := svc.LabelsSummary(ctx) if err != nil { return getLabelsSummaryResponse{Err: err}, nil @@ -327,7 +327,7 @@ type listHostsInLabelRequest struct { ListOptions fleet.HostListOptions `url:"host_options"` } -func listHostsInLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listHostsInLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listHostsInLabelRequest) hosts, err := svc.ListHostsInLabel(ctx, req.ID, req.ListOptions) if err != nil { @@ -397,7 +397,7 @@ type deleteLabelResponse struct { func (r deleteLabelResponse) Error() error { return r.Err } -func deleteLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteLabelEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteLabelRequest) err := svc.DeleteLabel(ctx, req.Name) if err != nil { @@ -435,7 +435,7 @@ type deleteLabelByIDResponse struct { func (r deleteLabelByIDResponse) Error() error { return r.Err } -func deleteLabelByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteLabelByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteLabelByIDRequest) err := svc.DeleteLabelByID(ctx, req.ID) if err != nil { @@ -484,7 +484,7 @@ type applyLabelSpecsResponse struct { func (r applyLabelSpecsResponse) Error() error { return r.Err } -func applyLabelSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func applyLabelSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*applyLabelSpecsRequest) err := svc.ApplyLabelSpecs(ctx, req.Specs) if err != nil { @@ -568,7 +568,7 @@ type getLabelSpecsResponse struct { func (r getLabelSpecsResponse) Error() error { return r.Err } -func getLabelSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getLabelSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { specs, err := svc.GetLabelSpecs(ctx) if err != nil { return getLabelSpecsResponse{Err: err}, nil @@ -595,7 +595,7 @@ type getLabelSpecResponse struct { func (r getLabelSpecResponse) Error() error { return r.Err } -func getLabelSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getLabelSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getGenericSpecRequest) spec, err := svc.GetLabelSpec(ctx, req.Name) if err != nil { diff --git a/server/service/live_queries.go b/server/service/live_queries.go index 8c49c70804cb..f017ffae58be 100644 --- a/server/service/live_queries.go +++ b/server/service/live_queries.go @@ -72,7 +72,7 @@ type runLiveQueryOnHostResponse struct { func (r runLiveQueryOnHostResponse) Error() error { return nil } -func runOneLiveQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func runOneLiveQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*runOneLiveQueryRequest) // Only allow a host to be specified once in HostIDs @@ -102,7 +102,7 @@ func runOneLiveQueryEndpoint(ctx context.Context, request interface{}, svc fleet return res, nil } -func runLiveQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func runLiveQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*runLiveQueryRequest) // Only allow a query to be specified once @@ -125,7 +125,7 @@ func runLiveQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Se return res, nil } -func runLiveQueryOnHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func runLiveQueryOnHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*runLiveQueryOnHostRequest) host, err := svc.HostLiteByIdentifier(ctx, req.Identifier) @@ -136,7 +136,7 @@ func runLiveQueryOnHostEndpoint(ctx context.Context, request interface{}, svc fl return runLiveQueryOnHost(svc, ctx, host, req.Query) } -func runLiveQueryOnHostByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func runLiveQueryOnHostByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*runLiveQueryOnHostByIDRequest) host, err := svc.HostLiteByID(ctx, req.HostID) @@ -147,7 +147,7 @@ func runLiveQueryOnHostByIDEndpoint(ctx context.Context, request interface{}, sv return runLiveQueryOnHost(svc, ctx, host, req.Query) } -func runLiveQueryOnHost(svc fleet.Service, ctx context.Context, host *fleet.HostLite, query string) (errorer, error) { +func runLiveQueryOnHost(svc fleet.Service, ctx context.Context, host *fleet.HostLite, query string) (fleet.Errorer, error) { query = strings.TrimSpace(query) if query == "" { return nil, ctxerr.Wrap(ctx, badRequest("query is required")) diff --git a/server/service/maintained_apps.go b/server/service/maintained_apps.go index 2a028c827b13..e7e76847858b 100644 --- a/server/service/maintained_apps.go +++ b/server/service/maintained_apps.go @@ -28,7 +28,7 @@ type addFleetMaintainedAppResponse struct { func (r addFleetMaintainedAppResponse) Error() error { return r.Err } -func addFleetMaintainedAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func addFleetMaintainedAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*addFleetMaintainedAppRequest) ctx, cancel := context.WithTimeout(ctx, maintainedapps.InstallerTimeout) defer cancel() @@ -74,7 +74,7 @@ type editFleetMaintainedAppRequest struct { LabelsExcludeAny []string `json:"labels_exclude_any"` } -func editFleetMaintainedAppEndpoint(ctx context.Context, request any, svc fleet.Service) (errorer, error) { +func editFleetMaintainedAppEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { // TODO: implement this return nil, errors.New("not implemented") @@ -95,7 +95,7 @@ type listFleetMaintainedAppsResponse struct { func (r listFleetMaintainedAppsResponse) Error() error { return r.Err } -func listFleetMaintainedAppsEndpoint(ctx context.Context, request any, svc fleet.Service) (errorer, error) { +func listFleetMaintainedAppsEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listFleetMaintainedAppsRequest) req.IncludeMetadata = true @@ -143,7 +143,7 @@ type getFleetMaintainedAppResponse struct { func (r getFleetMaintainedAppResponse) Error() error { return r.Err } -func getFleetMaintainedApp(ctx context.Context, request any, svc fleet.Service) (errorer, error) { +func getFleetMaintainedApp(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getFleetMaintainedAppRequest) app, err := svc.GetFleetMaintainedApp(ctx, req.AppID) diff --git a/server/service/mdm.go b/server/service/mdm.go index 8bdd9e968393..7357ef484e92 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -33,6 +33,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/cryptoutil" nanomdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/go-kit/log/level" "github.com/go-sql-driver/mysql" ) @@ -48,7 +49,7 @@ type getAppleMDMResponse struct { func (r getAppleMDMResponse) Error() error { return r.Err } -func getAppleMDMEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getAppleMDMEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { appleMDM, err := svc.GetAppleMDM(ctx) if err != nil { return getAppleMDMResponse{Err: err}, nil @@ -90,7 +91,7 @@ type getAppleBMResponse struct { func (r getAppleBMResponse) Error() error { return r.Err } -func getAppleBMEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getAppleBMEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { appleBM, err := svc.GetAppleBM(ctx) if err != nil { return getAppleBMResponse{Err: err}, nil @@ -123,7 +124,7 @@ type requestMDMAppleCSRResponse struct { func (r requestMDMAppleCSRResponse) Error() error { return r.Err } -func requestMDMAppleCSREndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func requestMDMAppleCSREndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*requestMDMAppleCSRRequest) csr, err := svc.RequestMDMAppleCSR(ctx, req.EmailAddress, req.Organization) @@ -264,7 +265,7 @@ type createMDMEULAResponse struct { func (r createMDMEULAResponse) Error() error { return r.Err } -func createMDMEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createMDMEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createMDMEULARequest) ff, err := req.EULA.Open() if err != nil { @@ -318,7 +319,7 @@ func (r getMDMEULAResponse) hijackRender(ctx context.Context, w http.ResponseWri } } -func getMDMEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMEULARequest) eula, err := svc.MDMGetEULABytes(ctx, req.Token) @@ -350,7 +351,7 @@ type getMDMEULAMetadataResponse struct { func (r getMDMEULAMetadataResponse) Error() error { return r.Err } -func getMDMEULAMetadataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMEULAMetadataEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { eula, err := svc.MDMGetEULAMetadata(ctx) if err != nil { return getMDMEULAMetadataResponse{Err: err}, nil @@ -381,7 +382,7 @@ type deleteMDMEULAResponse struct { func (r deleteMDMEULAResponse) Error() error { return r.Err } -func deleteMDMEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteMDMEULAEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteMDMEULARequest) if err := svc.MDMDeleteEULA(ctx, req.Token); err != nil { return deleteMDMEULAResponse{Err: err}, nil @@ -457,7 +458,7 @@ type runMDMCommandResponse struct { func (r runMDMCommandResponse) Error() error { return r.Err } -func runMDMCommandEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func runMDMCommandEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*runMDMCommandRequest) result, err := svc.RunMDMCommand(ctx, req.Command, req.HostUUIDs) if err != nil { @@ -652,7 +653,7 @@ type getMDMCommandResultsResponse struct { func (r getMDMCommandResultsResponse) Error() error { return r.Err } -func getMDMCommandResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMCommandResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMCommandResultsRequest) results, err := svc.GetMDMCommandResults(ctx, req.CommandUUID) if err != nil { @@ -769,7 +770,7 @@ type listMDMCommandsResponse struct { func (r listMDMCommandsResponse) Error() error { return r.Err } -func listMDMCommandsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listMDMCommandsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listMDMCommandsRequest) results, err := svc.ListMDMCommands(ctx, &fleet.MDMCommandListOptions{ ListOptions: req.ListOptions, @@ -885,7 +886,7 @@ type getMDMDiskEncryptionSummaryResponse struct { func (r getMDMDiskEncryptionSummaryResponse) Error() error { return r.Err } -func getMDMDiskEncryptionSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMDiskEncryptionSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMDiskEncryptionSummaryRequest) des, err := svc.GetMDMDiskEncryptionSummary(ctx, req.TeamID) @@ -922,7 +923,7 @@ type getMDMProfilesSummaryResponse struct { func (r getMDMProfilesSummaryResponse) Error() error { return r.Err } -func getMDMProfilesSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMProfilesSummaryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMProfilesSummaryRequest) res := getMDMProfilesSummaryResponse{} @@ -1018,7 +1019,7 @@ type getMDMConfigProfileResponse struct { func (r getMDMConfigProfileResponse) Error() error { return r.Err } -func getMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getMDMConfigProfileRequest) downloadRequested := req.Alt == "media" @@ -1113,7 +1114,7 @@ type deleteMDMConfigProfileResponse struct { func (r deleteMDMConfigProfileResponse) Error() error { return r.Err } -func deleteMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteMDMConfigProfileRequest) var err error @@ -1286,7 +1287,7 @@ type newMDMConfigProfileResponse struct { func (r newMDMConfigProfileResponse) Error() error { return r.Err } -func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func newMDMConfigProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*newMDMConfigProfileRequest) ff, err := req.Profile.Open() @@ -1439,7 +1440,7 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint, newCP, err := svc.ds.NewMDMWindowsConfigProfile(ctx, cp) if err != nil { - var existsErr existsErrorInterface + var existsErr endpoint_utils.ExistsErrorInterface if errors.As(err, &existsErr) { err = fleet.NewInvalidArgumentError("profile", SameProfileNameUploadErrorMsg). WithStatus(http.StatusConflict) @@ -1575,7 +1576,7 @@ func (r batchSetMDMProfilesResponse) Error() error { return r.Err } func (r batchSetMDMProfilesResponse) Status() int { return http.StatusNoContent } -func batchSetMDMProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func batchSetMDMProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchSetMDMProfilesRequest) if err := svc.BatchSetMDMProfiles( ctx, req.TeamID, req.TeamName, req.Profiles, req.DryRun, false, req.AssumeEnabled, @@ -2156,7 +2157,7 @@ type listMDMConfigProfilesResponse struct { func (r listMDMConfigProfilesResponse) Error() error { return r.Err } -func listMDMConfigProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listMDMConfigProfilesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listMDMConfigProfilesRequest) profs, meta, err := svc.ListMDMConfigProfiles(ctx, req.TeamID, req.ListOptions) @@ -2214,7 +2215,7 @@ func (r updateMDMDiskEncryptionResponse) Error() error { return r.Err } func (r updateMDMDiskEncryptionResponse) Status() int { return http.StatusNoContent } -func updateDiskEncryptionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func updateDiskEncryptionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateDiskEncryptionRequest) if err := svc.UpdateMDMDiskEncryption(ctx, req.TeamID, &req.EnableDiskEncryption); err != nil { return updateMDMDiskEncryptionResponse{Err: err}, nil @@ -2267,7 +2268,7 @@ func (r resendHostMDMProfileResponse) Error() error { return r.Err } func (r resendHostMDMProfileResponse) Status() int { return http.StatusAccepted } -func resendHostMDMProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func resendHostMDMProfileEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*resendHostMDMProfileRequest) if err := svc.ResendHostMDMProfile(ctx, req.HostID, req.ProfileUUID); err != nil { @@ -2394,7 +2395,7 @@ type getMDMAppleCSRResponse struct { func (r getMDMAppleCSRResponse) Error() error { return r.Err } -func getMDMAppleCSREndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getMDMAppleCSREndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { signedCSRB64, err := svc.GetMDMAppleCSR(ctx) if err != nil { return &getMDMAppleCSRResponse{Err: err}, nil @@ -2558,7 +2559,7 @@ func (r uploadMDMAppleAPNSCertResponse) Error() error { func (r uploadMDMAppleAPNSCertResponse) Status() int { return http.StatusAccepted } -func uploadMDMAppleAPNSCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func uploadMDMAppleAPNSCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uploadMDMAppleAPNSCertRequest) file, err := req.File.Open() if err != nil { @@ -2704,7 +2705,7 @@ func (r deleteMDMAppleAPNSCertResponse) Error() error { return r.Err } -func deleteMDMAppleAPNSCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteMDMAppleAPNSCertEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { if err := svc.DeleteMDMAppleAPNSCert(ctx); err != nil { return &deleteMDMAppleAPNSCertResponse{Err: err}, nil } diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index ce283cefcba6..cec78335d445 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -402,7 +402,7 @@ func NewSoapFault(errorType string, origMessage int, errorMessage error) mdm_typ } // getSTSAuthContent Retuns STS auth content -func getSTSAuthContent(data string) errorer { +func getSTSAuthContent(data string) mdm_types.Errorer { return MDMAuthContainer{ Data: &data, Err: nil, @@ -410,7 +410,7 @@ func getSTSAuthContent(data string) errorer { } // getSoapResponseFault Returns a SoapResponse with a SoapFault on its body -func getSoapResponseFault(relatesTo string, soapFault *mdm_types.SoapFault) errorer { +func getSoapResponseFault(relatesTo string, soapFault *mdm_types.SoapFault) mdm_types.Errorer { if len(relatesTo) == 0 { relatesTo = "invalid_message_id" } @@ -753,7 +753,7 @@ func NewProvisioningDoc(certStoreData mdm_types.Characteristic, applicationData // mdmMicrosoftDiscoveryEndpoint handles the Discovery message and returns a valid DiscoveryResponse message // DiscoverResponse message contains the Uniform Resource Locators (URLs) of service endpoints required for the following enrollment steps -func mdmMicrosoftDiscoveryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmMicrosoftDiscoveryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (mdm_types.Errorer, error) { req := request.(*SoapRequestContainer).Data // Checking first if Discovery message is valid and returning error if this is not the case @@ -783,7 +783,7 @@ func mdmMicrosoftDiscoveryEndpoint(ctx context.Context, request interface{}, svc } // mdmMicrosoftAuthEndpoint handles the Security Token Service (STS) implementation -func mdmMicrosoftAuthEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmMicrosoftAuthEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (mdm_types.Errorer, error) { params := request.(*SoapRequestContainer).Params // Sanity check on the expected query params @@ -809,7 +809,7 @@ func mdmMicrosoftAuthEndpoint(ctx context.Context, request interface{}, svc flee // mdmMicrosoftPolicyEndpoint handles the GetPolicies message and returns a valid GetPoliciesResponse message // GetPoliciesResponse message contains the certificate policies required for the next enrollment step. For more information about these messages, see [MS-XCEP] sections 3.1.4.1.1.1 and 3.1.4.1.1.2. -func mdmMicrosoftPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmMicrosoftPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (mdm_types.Errorer, error) { req := request.(*SoapRequestContainer).Data // Checking first if GetPolicies message is valid and returning error if this is not the case @@ -847,7 +847,7 @@ func mdmMicrosoftPolicyEndpoint(ctx context.Context, request interface{}, svc fl // mdmMicrosoftEnrollEndpoint handles the RequestSecurityToken message and returns a valid RequestSecurityTokenResponseCollection message // RequestSecurityTokenResponseCollection message contains the identity and provisioning information for the device management client. -func mdmMicrosoftEnrollEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmMicrosoftEnrollEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (mdm_types.Errorer, error) { req := request.(*SoapRequestContainer).Data // Checking first if RequestSecurityToken message is valid and returning error if this is not the case @@ -895,7 +895,7 @@ func mdmMicrosoftEnrollEndpoint(ctx context.Context, request interface{}, svc fl // SyncML message with protocol commands results and more protocol commands for the calling host // Note: This logic needs to be improved with better SyncML message parsing, better message tracking // and better security authentication (done through TLS and in-message hash) -func mdmMicrosoftManagementEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmMicrosoftManagementEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (mdm_types.Errorer, error) { reqSyncML := request.(*SyncMLReqMsgContainer).Data // Checking first if incoming SyncML message is valid and returning error if this is not the case @@ -918,7 +918,7 @@ func mdmMicrosoftManagementEndpoint(ctx context.Context, request interface{}, sv } // mdmMicrosoftTOSEndpoint handles the TOS content for the incoming MDM enrollment request -func mdmMicrosoftTOSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func mdmMicrosoftTOSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (mdm_types.Errorer, error) { params := request.(*MDMWebContainer).Params // Sanity check on the expected query params diff --git a/server/service/middleware/endpoint_utils/endpoint_utils.go b/server/service/middleware/endpoint_utils/endpoint_utils.go new file mode 100644 index 000000000000..a2a8b28d1d21 --- /dev/null +++ b/server/service/middleware/endpoint_utils/endpoint_utils.go @@ -0,0 +1,288 @@ +package endpoint_utils + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "reflect" + "strconv" + "strings" + + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/service/middleware/ratelimit" + kithttp "github.com/go-kit/kit/transport/http" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/gorilla/mux" +) + +type HandlerRoutesFunc func(r *mux.Router, opts []kithttp.ServerOption) + +// ParseTag parses a `url` tag and whether it's optional or not, which is an optional part of the tag +func ParseTag(tag string) (string, bool, error) { + parts := strings.Split(tag, ",") + switch len(parts) { + case 0: + return "", false, fmt.Errorf("Error parsing %s: too few parts", tag) + case 1: + return tag, false, nil + case 2: + return parts[0], parts[1] == "optional", nil + default: + return "", false, fmt.Errorf("Error parsing %s: too many parts", tag) + } +} + +type FieldPair struct { + Sf reflect.StructField + V reflect.Value +} + +// AllFields returns all the fields for a struct, including the ones from embedded structs +func AllFields(ifv reflect.Value) []FieldPair { + if ifv.Kind() == reflect.Ptr { + ifv = ifv.Elem() + } + if ifv.Kind() != reflect.Struct { + return nil + } + + var fields []FieldPair + + if !ifv.IsValid() { + return nil + } + + t := ifv.Type() + + for i := 0; i < ifv.NumField(); i++ { + v := ifv.Field(i) + + if v.Kind() == reflect.Struct && t.Field(i).Anonymous { + fields = append(fields, AllFields(v)...) + continue + } + fields = append(fields, FieldPair{Sf: ifv.Type().Field(i), V: v}) + } + + return fields +} + +func BadRequestErr(publicMsg string, internalErr error) error { + // ensure timeout errors don't become BadRequestErrors. + var opErr *net.OpError + if errors.As(internalErr, &opErr) { + return fmt.Errorf(publicMsg+", internal: %w", internalErr) + } + return &fleet.BadRequestError{ + Message: publicMsg, + InternalErr: internalErr, + } +} + +func UintFromRequest(r *http.Request, name string) (uint64, error) { + vars := mux.Vars(r) + s, ok := vars[name] + if !ok { + return 0, ErrBadRoute + } + u, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, ctxerr.Wrap(r.Context(), err, "UintFromRequest") + } + return u, nil +} + +func IntFromRequest(r *http.Request, name string) (int64, error) { + vars := mux.Vars(r) + s, ok := vars[name] + if !ok { + return 0, ErrBadRoute + } + u, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, ctxerr.Wrap(r.Context(), err, "IntFromRequest") + } + return u, nil +} + +func StringFromRequest(r *http.Request, name string) (string, error) { + vars := mux.Vars(r) + s, ok := vars[name] + if !ok { + return "", ErrBadRoute + } + unescaped, err := url.PathUnescape(s) + if err != nil { + return "", ctxerr.Wrap(r.Context(), err, "unescape value in path") + } + return unescaped, nil +} + +func DecodeURLTagValue(r *http.Request, field reflect.Value, urlTagValue string, optional bool) error { + switch field.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v, err := IntFromRequest(r, urlTagValue) + if err != nil { + if errors.Is(err, ErrBadRoute) && optional { + return nil + } + return BadRequestErr("IntFromRequest", err) + } + field.SetInt(v) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + v, err := UintFromRequest(r, urlTagValue) + if err != nil { + if errors.Is(err, ErrBadRoute) && optional { + return nil + } + return BadRequestErr("UintFromRequest", err) + } + field.SetUint(v) + + case reflect.String: + v, err := StringFromRequest(r, urlTagValue) + if err != nil { + if errors.Is(err, ErrBadRoute) && optional { + return nil + } + return BadRequestErr("StringFromRequest", err) + } + field.SetString(v) + + default: + return fmt.Errorf("unsupported type for field %s for 'url' decoding: %s", urlTagValue, field.Kind()) + } + return nil +} + +func DecodeQueryTagValue(r *http.Request, fp FieldPair) error { + queryTagValue, ok := fp.Sf.Tag.Lookup("query") + + if ok { + var err error + var optional bool + queryTagValue, optional, err = ParseTag(queryTagValue) + if err != nil { + return err + } + queryVal := r.URL.Query().Get(queryTagValue) + // if optional and it's a ptr, leave as nil + if queryVal == "" { + if optional { + return nil + } + return &fleet.BadRequestError{Message: fmt.Sprintf("Param %s is required", fp.Sf.Name)} + } + field := fp.V + if field.Kind() == reflect.Ptr { + // create the new instance of whatever it is + field.Set(reflect.New(field.Type().Elem())) + field = field.Elem() + } + switch field.Kind() { + case reflect.String: + field.SetString(queryVal) + case reflect.Uint: + queryValUint, err := strconv.Atoi(queryVal) + if err != nil { + return BadRequestErr("parsing uint from query", err) + } + field.SetUint(uint64(queryValUint)) //nolint:gosec // dismiss G115 + case reflect.Float64: + queryValFloat, err := strconv.ParseFloat(queryVal, 64) + if err != nil { + return BadRequestErr("parsing float from query", err) + } + field.SetFloat(queryValFloat) + case reflect.Bool: + field.SetBool(queryVal == "1" || queryVal == "true") + case reflect.Int: + queryValInt := 0 + switch queryTagValue { + case "order_direction", "inherited_order_direction": + switch queryVal { + case "desc": + queryValInt = int(fleet.OrderDescending) + case "asc": + queryValInt = int(fleet.OrderAscending) + default: + return &fleet.BadRequestError{Message: "unknown order_direction: " + queryVal} + } + default: + queryValInt, err = strconv.Atoi(queryVal) + if err != nil { + return BadRequestErr("parsing int from query", err) + } + } + field.SetInt(int64(queryValInt)) + default: + return fmt.Errorf("Cant handle type for field %s %s", fp.Sf.Name, field.Kind()) + } + } + return nil +} + +// copied from https://github.com/go-chi/chi/blob/c97bc988430d623a14f50b7019fb40529036a35a/middleware/realip.go#L42 +var trueClientIP = http.CanonicalHeaderKey("True-Client-IP") +var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") +var xRealIP = http.CanonicalHeaderKey("X-Real-IP") + +func ExtractIP(r *http.Request) string { + ip := r.RemoteAddr + if i := strings.LastIndexByte(ip, ':'); i != -1 { + ip = ip[:i] + } + + if tcip := r.Header.Get(trueClientIP); tcip != "" { + ip = tcip + } else if xrip := r.Header.Get(xRealIP); xrip != "" { + ip = xrip + } else if xff := r.Header.Get(xForwardedFor); xff != "" { + i := strings.Index(xff, ",") + if i == -1 { + i = len(xff) + } + ip = xff[:i] + } + + return ip +} + +type ErrorHandler struct { + Logger log.Logger +} + +func (h *ErrorHandler) Handle(ctx context.Context, err error) { + // get the request path + path, _ := ctx.Value(kithttp.ContextKeyRequestPath).(string) + logger := level.Info(log.With(h.Logger, "path", path)) + + var ewi fleet.ErrWithInternal + if errors.As(err, &ewi) { + logger = log.With(logger, "internal", ewi.Internal()) + } + + var ewlf fleet.ErrWithLogFields + if errors.As(err, &ewlf) { + logger = log.With(logger, ewlf.LogFields()...) + } + + var uuider fleet.ErrorUUIDer + if errors.As(err, &uuider) { + logger = log.With(logger, "uuid", uuider.UUID()) + } + + var rle ratelimit.Error + if errors.As(err, &rle) { + res := rle.Result() + logger.Log("err", "limit exceeded", "retry_after", res.RetryAfter) + } else { + logger.Log("err", err) + } +} diff --git a/server/service/transport_error.go b/server/service/middleware/endpoint_utils/transport_error.go similarity index 76% rename from server/service/transport_error.go rename to server/service/middleware/endpoint_utils/transport_error.go index 14edadaaaba9..7fdb5088059b 100644 --- a/server/service/transport_error.go +++ b/server/service/middleware/endpoint_utils/transport_error.go @@ -1,9 +1,10 @@ -package service +package endpoint_utils import ( "context" "encoding/json" "errors" + "fmt" "net" "net/http" "strconv" @@ -14,19 +15,17 @@ import ( "github.com/go-sql-driver/mysql" ) -// errorer interface is implemented by response structs to encode business logic errors -type errorer interface { - Error() error -} +// ErrBadRoute is used for mux errors +var ErrBadRoute = errors.New("bad route") -type jsonError struct { +type JsonError struct { Message string `json:"message"` Code int `json:"code,omitempty"` Errors []map[string]string `json:"errors,omitempty"` UUID string `json:"uuid,omitempty"` } -// use baseError to encode an jsonError.Errors field with an error that has +// use baseError to encode an JsonError.Errors field with an error that has // a generic "name" field. The frontend client always expects errors in a // []map[string]string format. func baseError(err string) []map[string]string { @@ -53,12 +52,12 @@ type badRequestErrorInterface interface { BadRequestError() []map[string]string } -type notFoundErrorInterface interface { +type NotFoundErrorInterface interface { error IsNotFound() bool } -type existsErrorInterface interface { +type ExistsErrorInterface interface { error IsExists() bool } @@ -68,8 +67,8 @@ type conflictErrorInterface interface { IsConflict() bool } -// encode error and status header to the client -func encodeError(ctx context.Context, err error, w http.ResponseWriter) { +// EncodeError encodes error and status header to the client +func EncodeError(ctx context.Context, err error, w http.ResponseWriter) { ctxerr.Handle(ctx, err) origErr := err @@ -83,13 +82,13 @@ func encodeError(ctx context.Context, err error, w http.ResponseWriter) { uuid = uuidErr.UUID() } - jsonErr := jsonError{ + jsonErr := JsonError{ UUID: uuid, } switch e := err.(type) { case validationErrorInterface: - if statusErr, ok := e.(statuser); ok { + if statusErr, ok := e.(interface{ Status() int }); ok { w.WriteHeader(statusErr.Status()) } else { w.WriteHeader(http.StatusUnprocessableEntity) @@ -100,11 +99,11 @@ func encodeError(ctx context.Context, err error, w http.ResponseWriter) { jsonErr.Message = "Permission Denied" jsonErr.Errors = e.PermissionError() w.WriteHeader(http.StatusForbidden) - case mailError: + case MailError: jsonErr.Message = "Mail Error" jsonErr.Errors = e.MailError() w.WriteHeader(http.StatusInternalServerError) - case *osqueryError: + case *OsqueryError: // osquery expects to receive the node_invalid key when a TLS // request provides an invalid node_key for authentication. It // doesn't use the error message provided, but we provide this @@ -130,11 +129,11 @@ func encodeError(ctx context.Context, err error, w http.ResponseWriter) { enc.Encode(errMap) //nolint:errcheck return - case notFoundErrorInterface: + case NotFoundErrorInterface: jsonErr.Message = "Resource Not Found" jsonErr.Errors = baseError(e.Error()) w.WriteHeader(http.StatusNotFound) - case existsErrorInterface: + case ExistsErrorInterface: jsonErr.Message = "Resource Already Exists" jsonErr.Errors = baseError(e.Error()) w.WriteHeader(http.StatusConflict) @@ -209,3 +208,53 @@ func encodeError(ctx context.Context, err error, w http.ResponseWriter) { enc.Encode(jsonErr) //nolint:errcheck } + +// MailError is set when an error performing mail operations +type MailError struct { + Message string +} + +func (e MailError) Error() string { + return fmt.Sprintf("a mail error occurred: %s", e.Message) +} + +func (e MailError) MailError() []map[string]string { + return []map[string]string{ + { + "name": "base", + "reason": e.Message, + }, + } +} + +// OsqueryError is the error returned to osquery agents. +type OsqueryError struct { + message string + nodeInvalid bool + StatusCode int + fleet.ErrorWithUUID +} + +var _ fleet.ErrorUUIDer = (*OsqueryError)(nil) + +// Error implements the error interface. +func (e *OsqueryError) Error() string { + return e.message +} + +// NodeInvalid returns whether the error returned to osquery +// should contain the node_invalid property. +func (e *OsqueryError) NodeInvalid() bool { + return e.nodeInvalid +} + +func (e *OsqueryError) Status() int { + return e.StatusCode +} + +func NewOsqueryError(message string, nodeInvalid bool) *OsqueryError { + return &OsqueryError{ + message: message, + nodeInvalid: nodeInvalid, + } +} diff --git a/server/service/transport_error_test.go b/server/service/middleware/endpoint_utils/transport_error_test.go similarity index 82% rename from server/service/transport_error_test.go rename to server/service/middleware/endpoint_utils/transport_error_test.go index 14dcc74f3fd8..002d52b62a01 100644 --- a/server/service/transport_error_test.go +++ b/server/service/middleware/endpoint_utils/transport_error_test.go @@ -1,4 +1,4 @@ -package service +package endpoint_utils import ( "context" @@ -24,6 +24,18 @@ type newAndExciting struct{} func (newAndExciting) Error() string { return "" } +type notFoundError struct { + fleet.ErrorWithUUID +} + +func (e *notFoundError) Error() string { + return "not found" +} + +func (e *notFoundError) IsNotFound() bool { + return true +} + func TestHandlesErrorsCode(t *testing.T) { errorTests := []struct { name string @@ -47,17 +59,17 @@ func TestHandlesErrorsCode(t *testing.T) { }, { "mail error", - mailError{}, + MailError{}, http.StatusInternalServerError, }, { "osquery error - invalid node", - &osqueryError{nodeInvalid: true}, + &OsqueryError{nodeInvalid: true}, http.StatusUnauthorized, }, { "osquery error - valid node", - &osqueryError{}, + &OsqueryError{}, http.StatusInternalServerError, }, { @@ -85,7 +97,7 @@ func TestHandlesErrorsCode(t *testing.T) { for _, tt := range errorTests { t.Run(tt.name, func(t *testing.T) { recorder := httptest.NewRecorder() - encodeError(context.Background(), tt.err, recorder) + EncodeError(context.Background(), tt.err, recorder) assert.Equal(t, recorder.Code, tt.code) }) } diff --git a/server/service/orbit.go b/server/service/orbit.go index ef885f879c3b..8047727206e5 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -19,6 +19,7 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm" microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/worker" "github.com/go-kit/log/level" ) @@ -83,11 +84,11 @@ func (r EnrollOrbitResponse) hijackRender(ctx context.Context, w http.ResponseWr enc.SetIndent("", " ") if err := enc.Encode(r); err != nil { - encodeError(ctx, newOsqueryError(fmt.Sprintf("orbit enroll failed: %s", err)), w) + endpoint_utils.EncodeError(ctx, newOsqueryError(fmt.Sprintf("orbit enroll failed: %s", err)), w) } } -func enrollOrbitEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func enrollOrbitEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*EnrollOrbitRequest) nodeKey, err := svc.EnrollOrbit(ctx, fleet.OrbitHostInfo{ HardwareUUID: req.HardwareUUID, @@ -183,7 +184,7 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf return orbitNodeKey, nil } -func getOrbitConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getOrbitConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { cfg, err := svc.GetOrbitConfig(ctx) if err != nil { return orbitGetConfigResponse{Err: err}, nil @@ -658,7 +659,7 @@ func (r orbitPingResponse) Error() error { return nil } // NOTE: we're intentionally not reading the capabilities header in this // endpoint as is unauthenticated and we don't want to trust whatever comes in // there. -func orbitPingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func orbitPingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { svc.DisableAuthForPing(ctx) return orbitPingResponse{}, nil } @@ -686,7 +687,7 @@ type setOrUpdateDeviceTokenResponse struct { func (r setOrUpdateDeviceTokenResponse) Error() error { return r.Err } -func setOrUpdateDeviceTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func setOrUpdateDeviceTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*setOrUpdateDeviceTokenRequest) if err := svc.SetOrUpdateDeviceAuthToken(ctx, req.DeviceAuthToken); err != nil { return setOrUpdateDeviceTokenResponse{Err: err}, nil @@ -747,7 +748,7 @@ type orbitGetScriptResponse struct { func (r orbitGetScriptResponse) Error() error { return r.Err } -func getOrbitScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getOrbitScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*orbitGetScriptRequest) script, err := svc.GetHostScript(ctx, req.ExecutionID) if err != nil { @@ -810,7 +811,7 @@ type orbitPostScriptResultResponse struct { func (r orbitPostScriptResultResponse) Error() error { return r.Err } -func postOrbitScriptResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func postOrbitScriptResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*orbitPostScriptResultRequest) if err := svc.SaveHostScriptResult(ctx, req.HostScriptResultPayload); err != nil { return orbitPostScriptResultResponse{Err: err}, nil @@ -961,7 +962,7 @@ type orbitPutDeviceMappingResponse struct { func (r orbitPutDeviceMappingResponse) Error() error { return r.Err } -func putOrbitDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func putOrbitDeviceMappingEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*orbitPutDeviceMappingRequest) host, ok := hostctx.FromContext(ctx) @@ -1001,7 +1002,7 @@ type orbitPostDiskEncryptionKeyResponse struct { func (r orbitPostDiskEncryptionKeyResponse) Error() error { return r.Err } func (r orbitPostDiskEncryptionKeyResponse) Status() int { return http.StatusNoContent } -func postOrbitDiskEncryptionKeyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func postOrbitDiskEncryptionKeyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*orbitPostDiskEncryptionKeyRequest) if err := svc.SetOrUpdateDiskEncryptionKey(ctx, string(req.EncryptionKey), req.ClientError); err != nil { return orbitPostDiskEncryptionKeyResponse{Err: err}, nil @@ -1084,7 +1085,7 @@ type orbitPostLUKSResponse struct { func (r orbitPostLUKSResponse) Error() error { return r.Err } func (r orbitPostLUKSResponse) Status() int { return http.StatusNoContent } -func postOrbitLUKSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func postOrbitLUKSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*orbitPostLUKSRequest) if err := svc.EscrowLUKSData(ctx, req.Passphrase, req.Salt, req.KeySlot, req.ClientError); err != nil { return orbitPostLUKSResponse{Err: err}, nil @@ -1147,6 +1148,7 @@ type orbitGetSoftwareInstallRequest struct { // interface implementation required by the OrbitClient func (r *orbitGetSoftwareInstallRequest) setOrbitNodeKey(nodeKey string) { r.OrbitNodeKey = nodeKey + r.OrbotNodeKey = nodeKey // legacy typo -- keep for backwards compatability with fleet server < 4.63.0 } // interface implementation required by the OrbitClient @@ -1164,7 +1166,7 @@ type orbitGetSoftwareInstallResponse struct { func (r orbitGetSoftwareInstallResponse) Error() error { return r.Err } -func getOrbitSoftwareInstallDetails(ctx context.Context, request any, svc fleet.Service) (errorer, error) { +func getOrbitSoftwareInstallDetails(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*orbitGetSoftwareInstallRequest) details, err := svc.GetSoftwareInstallDetails(ctx, req.InstallUUID) if err != nil { @@ -1214,7 +1216,7 @@ func (r *orbitDownloadSoftwareInstallerRequest) orbitHostNodeKey() string { return r.OrbitNodeKey } -func orbitDownloadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func orbitDownloadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*orbitDownloadSoftwareInstallerRequest) downloadRequested := req.Alt == "media" @@ -1263,7 +1265,7 @@ type orbitPostSoftwareInstallResultResponse struct { func (r orbitPostSoftwareInstallResultResponse) Error() error { return r.Err } func (r orbitPostSoftwareInstallResultResponse) Status() int { return http.StatusNoContent } -func postOrbitSoftwareInstallResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func postOrbitSoftwareInstallResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*orbitPostSoftwareInstallResultRequest) if err := svc.SaveHostSoftwareInstallResult(ctx, req.HostSoftwareInstallResultPayload); err != nil { return orbitPostSoftwareInstallResultResponse{Err: err}, nil @@ -1367,7 +1369,7 @@ type getOrbitSetupExperienceStatusResponse struct { func (r getOrbitSetupExperienceStatusResponse) Error() error { return r.Err } -func getOrbitSetupExperienceStatusEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getOrbitSetupExperienceStatusEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getOrbitSetupExperienceStatusRequest) results, err := svc.GetOrbitSetupExperienceStatus(ctx, req.OrbitNodeKey, req.ForceRelease) if err != nil { diff --git a/server/service/osquery.go b/server/service/osquery.go index 60c6597cbde6..1491b1a1db2e 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -22,6 +22,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/pubsub" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/service/osquery_utils" kithttp "github.com/go-kit/kit/transport/http" "github.com/go-kit/log" @@ -30,43 +31,12 @@ import ( "golang.org/x/exp/slices" ) -// osqueryError is the error returned to osquery agents. -type osqueryError struct { - message string - nodeInvalid bool - statusCode int - fleet.ErrorWithUUID +func newOsqueryErrorWithInvalidNode(msg string) *endpoint_utils.OsqueryError { + return endpoint_utils.NewOsqueryError(msg, true) } -var _ fleet.ErrorUUIDer = (*osqueryError)(nil) - -// Error implements the error interface. -func (e *osqueryError) Error() string { - return e.message -} - -// NodeInvalid returns whether the error returned to osquery -// should contain the node_invalid property. -func (e *osqueryError) NodeInvalid() bool { - return e.nodeInvalid -} - -func (e *osqueryError) Status() int { - return e.statusCode -} - -func newOsqueryErrorWithInvalidNode(msg string) *osqueryError { - return &osqueryError{ - message: msg, - nodeInvalid: true, - } -} - -func newOsqueryError(msg string) *osqueryError { - return &osqueryError{ - message: msg, - nodeInvalid: false, - } +func newOsqueryError(msg string) *endpoint_utils.OsqueryError { + return endpoint_utils.NewOsqueryError(msg, false) } func (svc *Service) AuthenticateHost(ctx context.Context, nodeKey string) (*fleet.Host, bool, error) { @@ -118,7 +88,7 @@ type enrollAgentResponse struct { func (r enrollAgentResponse) Error() error { return r.Err } -func enrollAgentEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func enrollAgentEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*enrollAgentRequest) nodeKey, err := svc.EnrollAgent(ctx, req.EnrollSecret, req.HostIdentifier, req.HostDetails) if err != nil { @@ -352,7 +322,7 @@ func (r *getClientConfigResponse) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &r.Config) } -func getClientConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getClientConfigEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { config, err := svc.GetClientConfig(ctx) if err != nil { return getClientConfigResponse{Err: err}, nil @@ -582,7 +552,7 @@ type getDistributedQueriesResponse struct { func (r getDistributedQueriesResponse) Error() error { return r.Err } -func getDistributedQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getDistributedQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { queries, discovery, accelerate, err := svc.GetDistributedQueries(ctx) if err != nil { return getDistributedQueriesResponse{Err: err}, nil @@ -886,7 +856,7 @@ type submitDistributedQueryResultsResponse struct { func (r submitDistributedQueryResultsResponse) Error() error { return r.Err } -func submitDistributedQueryResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func submitDistributedQueryResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { shim := request.(*submitDistributedQueryResultsRequestShim) req, err := shim.toRequest(ctx) if err != nil { @@ -2225,7 +2195,7 @@ type submitLogsResponse struct { func (r submitLogsResponse) Error() error { return r.Err } -func submitLogsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func submitLogsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*submitLogsRequest) var err error @@ -2354,7 +2324,7 @@ func (svc *Service) SubmitStatusLogs(ctx context.Context, logs []json.RawMessage if err := svc.osqueryLogWriter.Status.Write(ctx, logs); err != nil { osqueryErr := newOsqueryError("error writing status logs: " + err.Error()) // Attempting to write a large amount of data is the most likely explanation for this error. - osqueryErr.statusCode = http.StatusRequestEntityTooLarge + osqueryErr.StatusCode = http.StatusRequestEntityTooLarge return osqueryErr } return nil @@ -2439,7 +2409,7 @@ func (svc *Service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage "increasing logger_tls_period and decreasing logger_tls_max_lines): " + err.Error(), ) // Attempting to write a large amount of data is the most likely explanation for this error. - osqueryErr.statusCode = http.StatusRequestEntityTooLarge + osqueryErr.StatusCode = http.StatusRequestEntityTooLarge return osqueryErr } return nil @@ -2836,7 +2806,7 @@ func (r getYaraResponse) hijackRender(ctx context.Context, w http.ResponseWriter _, _ = w.Write([]byte(r.Content)) } -func getYaraEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getYaraEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { r := request.(*getYaraRequest) rule, err := svc.YaraRuleByName(ctx, r.Name) if err != nil { diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index d1d364abdfe6..b8dfbbc6363c 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -32,6 +32,7 @@ import ( "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/pubsub" "github.com/fleetdm/fleet/v4/server/service/async" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/service/osquery_utils" "github.com/fleetdm/fleet/v4/server/service/redis_policy_set" "github.com/go-kit/log" @@ -965,7 +966,7 @@ func TestSubmitResultLogsFail(t *testing.T) { // Expect an error when unable to write to logging destination. err = svc.SubmitResultLogs(ctx, results) require.Error(t, err) - assert.Equal(t, http.StatusRequestEntityTooLarge, err.(*osqueryError).Status()) + assert.Equal(t, http.StatusRequestEntityTooLarge, err.(*endpoint_utils.OsqueryError).Status()) } func TestGetQueryNameAndTeamIDFromResult(t *testing.T) { @@ -2771,7 +2772,7 @@ func TestAuthenticationErrors(t *testing.T) { _, _, err := svc.AuthenticateHost(ctx, "") require.Error(t, err) - require.True(t, err.(*osqueryError).NodeInvalid()) + require.True(t, err.(*endpoint_utils.OsqueryError).NodeInvalid()) ms.LoadHostByNodeKeyFunc = func(ctx context.Context, nodeKey string) (*fleet.Host, error) { return &fleet.Host{ID: 1}, nil @@ -2789,7 +2790,7 @@ func TestAuthenticationErrors(t *testing.T) { _, _, err = svc.AuthenticateHost(ctx, "foo") require.Error(t, err) - require.True(t, err.(*osqueryError).NodeInvalid()) + require.True(t, err.(*endpoint_utils.OsqueryError).NodeInvalid()) // return other error ms.LoadHostByNodeKeyFunc = func(ctx context.Context, nodeKey string) (*fleet.Host, error) { @@ -2798,7 +2799,7 @@ func TestAuthenticationErrors(t *testing.T) { _, _, err = svc.AuthenticateHost(ctx, "foo") require.NotNil(t, err) - require.False(t, err.(*osqueryError).NodeInvalid()) + require.False(t, err.(*endpoint_utils.OsqueryError).NodeInvalid()) } func TestGetHostIdentifier(t *testing.T) { diff --git a/server/service/packs.go b/server/service/packs.go index d79211d75e21..4bff5ec229cc 100644 --- a/server/service/packs.go +++ b/server/service/packs.go @@ -111,7 +111,7 @@ type getPackResponse struct { func (r getPackResponse) Error() error { return r.Err } -func getPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getPackRequest) pack, err := svc.GetPack(ctx, req.ID) if err != nil { @@ -151,7 +151,7 @@ type createPackResponse struct { func (r createPackResponse) Error() error { return r.Err } -func createPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createPackRequest) pack, err := svc.NewPack(ctx, req.PackPayload) if err != nil { @@ -244,7 +244,7 @@ type modifyPackResponse struct { func (r modifyPackResponse) Error() error { return r.Err } -func modifyPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyPackRequest) pack, err := svc.ModifyPack(ctx, req.ID, req.PackPayload) if err != nil { @@ -339,7 +339,7 @@ type listPacksResponse struct { func (r listPacksResponse) Error() error { return r.Err } -func listPacksEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listPacksEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listPacksRequest) packs, err := svc.ListPacks(ctx, fleet.PackListOptions{ListOptions: req.ListOptions, IncludeSystemPacks: false}) if err != nil { @@ -379,7 +379,7 @@ type deletePackResponse struct { func (r deletePackResponse) Error() error { return r.Err } -func deletePackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deletePackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deletePackRequest) err := svc.DeletePack(ctx, req.Name) if err != nil { @@ -432,7 +432,7 @@ type deletePackByIDResponse struct { func (r deletePackByIDResponse) Error() error { return r.Err } -func deletePackByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deletePackByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deletePackByIDRequest) err := svc.DeletePackByID(ctx, req.ID) if err != nil { @@ -483,7 +483,7 @@ type applyPackSpecsResponse struct { func (r applyPackSpecsResponse) Error() error { return r.Err } -func applyPackSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func applyPackSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*applyPackSpecsRequest) _, err := svc.ApplyPackSpecs(ctx, req.Specs) if err != nil { @@ -557,7 +557,7 @@ type getPackSpecsResponse struct { func (r getPackSpecsResponse) Error() error { return r.Err } -func getPackSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getPackSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { specs, err := svc.GetPackSpecs(ctx) if err != nil { return getPackSpecsResponse{Err: err}, nil @@ -584,7 +584,7 @@ type getPackSpecResponse struct { func (r getPackSpecResponse) Error() error { return r.Err } -func getPackSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getPackSpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getGenericSpecRequest) spec, err := svc.GetPackSpec(ctx, req.Name) if err != nil { diff --git a/server/service/queries.go b/server/service/queries.go index 1c44472b3ae6..6ec18c69eb7f 100644 --- a/server/service/queries.go +++ b/server/service/queries.go @@ -29,7 +29,7 @@ type getQueryResponse struct { func (r getQueryResponse) Error() error { return r.Err } -func getQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getQueryRequest) query, err := svc.GetQuery(ctx, req.ID) if err != nil { @@ -73,7 +73,7 @@ type listQueriesResponse struct { func (r listQueriesResponse) Error() error { return r.Err } -func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listQueriesRequest) var teamID *uint @@ -164,7 +164,7 @@ type getQueryReportResponse struct { func (r getQueryReportResponse) Error() error { return r.Err } -func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getQueryReportRequest) queryReportResults, reportClipped, err := svc.GetQueryReportResults(ctx, req.ID, req.TeamID) if err != nil { @@ -254,7 +254,7 @@ type createQueryResponse struct { func (r createQueryResponse) Error() error { return r.Err } -func createQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createQueryRequest) query, err := svc.NewQuery(ctx, req.QueryPayload) if err != nil { @@ -362,7 +362,7 @@ type modifyQueryResponse struct { func (r modifyQueryResponse) Error() error { return r.Err } -func modifyQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyQueryRequest) query, err := svc.ModifyQuery(ctx, req.ID, req.QueryPayload) if err != nil { @@ -488,7 +488,7 @@ type deleteQueryResponse struct { func (r deleteQueryResponse) Error() error { return r.Err } -func deleteQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteQueryRequest) var teamID *uint if req.TeamID != 0 { @@ -542,7 +542,7 @@ type deleteQueryByIDResponse struct { func (r deleteQueryByIDResponse) Error() error { return r.Err } -func deleteQueryByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteQueryByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteQueryByIDRequest) err := svc.DeleteQueryByID(ctx, req.ID) if err != nil { @@ -593,7 +593,7 @@ type deleteQueriesResponse struct { func (r deleteQueriesResponse) Error() error { return r.Err } -func deleteQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteQueriesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteQueriesRequest) deleted, err := svc.DeleteQueries(ctx, req.IDs) if err != nil { @@ -646,7 +646,7 @@ type applyQuerySpecsResponse struct { func (r applyQuerySpecsResponse) Error() error { return r.Err } -func applyQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func applyQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*applyQuerySpecsRequest) err := svc.ApplyQuerySpecs(ctx, req.Specs) if err != nil { @@ -766,7 +766,7 @@ type getQuerySpecsRequest struct { func (r getQuerySpecsResponse) Error() error { return r.Err } -func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getQuerySpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getQuerySpecsRequest) var teamID *uint if req.TeamID != 0 { @@ -838,7 +838,7 @@ type getQuerySpecRequest struct { func (r getQuerySpecResponse) Error() error { return r.Err } -func getQuerySpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getQuerySpecEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getQuerySpecRequest) var teamID *uint if req.TeamID != 0 { diff --git a/server/service/scheduled_queries.go b/server/service/scheduled_queries.go index ef5f91d80ade..8cd315127036 100644 --- a/server/service/scheduled_queries.go +++ b/server/service/scheduled_queries.go @@ -33,7 +33,7 @@ type getScheduledQueriesInPackResponse struct { func (r getScheduledQueriesInPackResponse) Error() error { return r.Err } -func getScheduledQueriesInPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getScheduledQueriesInPackEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getScheduledQueriesInPackRequest) resp := getScheduledQueriesInPackResponse{Scheduled: []scheduledQueryResponse{}} @@ -81,7 +81,7 @@ type scheduleQueryResponse struct { func (r scheduleQueryResponse) Error() error { return r.Err } -func scheduleQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func scheduleQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*scheduleQueryRequest) scheduled, err := svc.ScheduleQuery(ctx, &fleet.ScheduledQuery{ @@ -169,7 +169,7 @@ type getScheduledQueryResponse struct { func (r getScheduledQueryResponse) Error() error { return r.Err } -func getScheduledQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getScheduledQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getScheduledQueryRequest) sq, err := svc.GetScheduledQuery(ctx, req.ID) @@ -209,7 +209,7 @@ type modifyScheduledQueryResponse struct { func (r modifyScheduledQueryResponse) Error() error { return r.Err } -func modifyScheduledQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyScheduledQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyScheduledQueryRequest) sq, err := svc.ModifyScheduledQuery(ctx, req.ID, req.ScheduledQueryPayload) @@ -299,7 +299,7 @@ type deleteScheduledQueryResponse struct { func (r deleteScheduledQueryResponse) Error() error { return r.Err } -func deleteScheduledQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteScheduledQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteScheduledQueryRequest) err := svc.DeleteScheduledQuery(ctx, req.ID) diff --git a/server/service/scripts.go b/server/service/scripts.go index 535f6740d1f5..7dba2dca077a 100644 --- a/server/service/scripts.go +++ b/server/service/scripts.go @@ -44,7 +44,7 @@ type runScriptResponse struct { func (r runScriptResponse) Error() error { return r.Err } func (r runScriptResponse) Status() int { return http.StatusAccepted } -func runScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func runScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*runScriptRequest) var noWait time.Duration @@ -94,7 +94,7 @@ func (r runScriptSyncResponse) Status() int { // this is to be used only by tests, to be able to use a shorter timeout. var testRunScriptWaitForResult time.Duration -func runScriptSyncEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func runScriptSyncEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { waitForResult := scripts.MaxServerWaitTime if testRunScriptWaitForResult != 0 { waitForResult = testRunScriptWaitForResult @@ -115,7 +115,7 @@ func runScriptSyncEndpoint(ctx context.Context, request interface{}, svc fleet.S } // We should still return the execution id and host id in this timeout case, // so the user knows what script request to look at in the UI. We cannot - // return an error (field Err) in this case, as the errorer interface's + // return an error (field Err) in this case, as the Errorer interface's // rendering logic would take over and only render the error part of the // response struct. hostTimeout = true @@ -375,7 +375,7 @@ type getScriptResultResponse struct { func (r getScriptResultResponse) Error() error { return r.Err } -func getScriptResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getScriptResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getScriptResultRequest) scriptResult, err := svc.GetScriptResult(ctx, req.ExecutionID) if err != nil { @@ -487,7 +487,7 @@ type createScriptResponse struct { func (r createScriptResponse) Error() error { return r.Err } -func createScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createScriptRequest) scriptFile, err := req.Script.Open() @@ -580,7 +580,7 @@ type deleteScriptResponse struct { func (r deleteScriptResponse) Error() error { return r.Err } func (r deleteScriptResponse) Status() int { return http.StatusNoContent } -func deleteScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteScriptRequest) err := svc.DeleteScript(ctx, req.ScriptID) if err != nil { @@ -640,7 +640,7 @@ type listScriptsResponse struct { func (r listScriptsResponse) Error() error { return r.Err } -func listScriptsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listScriptsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listScriptsRequest) scripts, meta, err := svc.ListScripts(ctx, req.TeamID, req.ListOptions) if err != nil { @@ -713,7 +713,7 @@ func (r downloadFileResponse) hijackRender(ctx context.Context, w http.ResponseW } } -func getScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getScriptRequest) downloadRequested := req.Alt == "media" @@ -799,7 +799,7 @@ type updateScriptResponse struct { func (r updateScriptResponse) Error() error { return r.Err } -func updateScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func updateScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateScriptRequest) scriptFile, err := req.Script.Open() @@ -888,7 +888,7 @@ type getHostScriptDetailsResponse struct { func (r getHostScriptDetailsResponse) Error() error { return r.Err } -func getHostScriptDetailsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getHostScriptDetailsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getHostScriptDetailsRequest) scripts, meta, err := svc.GetHostScriptDetails(ctx, req.HostID, req.ListOptions) if err != nil { @@ -948,7 +948,7 @@ type batchSetScriptsResponse struct { func (r batchSetScriptsResponse) Error() error { return r.Err } -func batchSetScriptsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func batchSetScriptsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchSetScriptsRequest) scriptList, err := svc.BatchSetScripts(ctx, req.TeamID, req.TeamName, req.Scripts, req.DryRun) if err != nil { @@ -1074,7 +1074,7 @@ type lockHostResponse struct { func (r lockHostResponse) Error() error { return r.Err } -func lockHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func lockHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*lockHostRequest) unlockPIN, err := svc.LockHost(ctx, req.HostID, req.ViewPin) if err != nil { @@ -1115,7 +1115,7 @@ type unlockHostResponse struct { func (r unlockHostResponse) Error() error { return r.Err } -func unlockHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func unlockHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*unlockHostRequest) pin, err := svc.UnlockHost(ctx, req.HostID) if err != nil { @@ -1156,7 +1156,7 @@ type wipeHostResponse struct { func (r wipeHostResponse) Error() error { return r.Err } -func wipeHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func wipeHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*wipeHostRequest) if err := svc.WipeHost(ctx, req.HostID); err != nil { return wipeHostResponse{Err: err}, nil diff --git a/server/service/secret_variables.go b/server/service/secret_variables.go index 8c00d05ab096..8bf622466faa 100644 --- a/server/service/secret_variables.go +++ b/server/service/secret_variables.go @@ -29,7 +29,7 @@ type secretVariablesResponse struct { func (r secretVariablesResponse) Error() error { return r.Err } -func secretVariablesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func secretVariablesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*secretVariablesRequest) err := svc.CreateSecretVariables(ctx, req.SecretVariables, req.DryRun) return secretVariablesResponse{Err: err}, nil diff --git a/server/service/service_appconfig.go b/server/service/service_appconfig.go index 3562fa42981a..3344b15e83da 100644 --- a/server/service/service_appconfig.go +++ b/server/service/service_appconfig.go @@ -2,7 +2,6 @@ package service import ( "context" - "fmt" "html/template" "strings" @@ -13,26 +12,9 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mail" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" ) -// mailError is set when an error performing mail operations -type mailError struct { - message string -} - -func (e mailError) Error() string { - return fmt.Sprintf("a mail error occurred: %s", e.message) -} - -func (e mailError) MailError() []map[string]string { - return []map[string]string{ - { - "name": "base", - "reason": e.message, - }, - } -} - func (svc *Service) NewAppConfig(ctx context.Context, p fleet.AppConfig) (*fleet.AppConfig, error) { // skipauth: No user context yet when the app config is first created. svc.authz.SkipAuthorization(ctx) @@ -86,7 +68,7 @@ func (svc *Service) sendTestEmail(ctx context.Context, config *fleet.AppConfig) } if err := mail.Test(svc.mailService, testMail); err != nil { - return mailError{message: err.Error()} + return endpoint_utils.MailError{Message: err.Error()} } return nil } diff --git a/server/service/sessions.go b/server/service/sessions.go index 477ce92eccae..bf2524b20147 100644 --- a/server/service/sessions.go +++ b/server/service/sessions.go @@ -16,6 +16,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mail" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/sso" "github.com/go-kit/log/level" ) @@ -37,7 +38,7 @@ type getInfoAboutSessionResponse struct { func (r getInfoAboutSessionResponse) Error() error { return r.Err } -func getInfoAboutSessionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getInfoAboutSessionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getInfoAboutSessionRequest) session, err := svc.GetInfoAboutSession(ctx, req.ID) if err != nil { @@ -84,7 +85,7 @@ type deleteSessionResponse struct { func (r deleteSessionResponse) Error() error { return r.Err } -func deleteSessionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteSessionEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteSessionRequest) err := svc.DeleteSession(ctx, req.ID) if err != nil { @@ -139,7 +140,7 @@ func (r loginMfaResponse) Status() int { return http.StatusAccepted } func (r loginMfaResponse) Error() error { return r.Err } -func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*loginRequest) req.Email = strings.ToLower(req.Email) @@ -170,7 +171,7 @@ func loginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) //goland:noinspection GoErrorStringFormat var sendingMFAEmail = errors.New("sending MFA email") var noMFASupported = errors.New("client with no MFA email support") -var mfaNotSupportedForClient = badRequestErr( +var mfaNotSupportedForClient = endpoint_utils.BadRequestErr( "Your login client does not support MFA. Please log in via the web, then use an API token to authenticate.", noMFASupported, ) @@ -257,7 +258,7 @@ type sessionCreateRequest struct { Token string `json:"token,omitempty"` } -func sessionCreateEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func sessionCreateEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*sessionCreateRequest) session, user, err := svc.CompleteMFA(ctx, req.Token) if err != nil { @@ -314,7 +315,7 @@ type logoutResponse struct { func (r logoutResponse) Error() error { return r.Err } -func logoutEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func logoutEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { err := svc.Logout(ctx) if err != nil { return logoutResponse{Err: err}, nil @@ -366,7 +367,7 @@ type initiateSSOResponse struct { func (r initiateSSOResponse) Error() error { return r.Err } -func initiateSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func initiateSSOEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*initiateSSORequest) idProviderURL, err := svc.InitiateSSO(ctx, req.RelayURL) if err != nil { @@ -399,7 +400,7 @@ func (svc *Service) InitiateSSO(ctx context.Context, redirectURL string) (string metadata, err := sso.GetMetadata(&appConfig.SSOSettings.SSOProviderSettings) if err != nil { - return "", ctxerr.Wrap(ctx, badRequestErr("Could not get SSO Metadata. Check your SSO settings.", err)) + return "", ctxerr.Wrap(ctx, endpoint_utils.BadRequestErr("Could not get SSO Metadata. Check your SSO settings.", err)) } serverURL := appConfig.ServerSettings.ServerURL @@ -467,7 +468,7 @@ func (r callbackSSOResponse) Error() error { return r.Err } func (r callbackSSOResponse) html() string { return r.content } func makeCallbackSSOEndpoint(urlPrefix string) handlerFunc { - return func(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + return func(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { authResponse := request.(fleet.Auth) session, err := getSSOSession(ctx, svc, authResponse) var resp callbackSSOResponse @@ -589,7 +590,7 @@ func (svc *Service) InitSSOCallback(ctx context.Context, auth fleet.Auth) (strin func (svc *Service) GetSSOUser(ctx context.Context, auth fleet.Auth) (*fleet.User, error) { user, err := svc.ds.UserByEmail(ctx, auth.UserID()) if err != nil { - var nfe notFoundErrorInterface + var nfe endpoint_utils.NotFoundErrorInterface if errors.As(err, &nfe) { return nil, ctxerr.Wrap(ctx, newSSOError(err, ssoAccountInvalid)) } @@ -638,7 +639,7 @@ type ssoSettingsResponse struct { func (r ssoSettingsResponse) Error() error { return r.Err } -func settingsSSOEndpoint(ctx context.Context, _ interface{}, svc fleet.Service) (errorer, error) { +func settingsSSOEndpoint(ctx context.Context, _ interface{}, svc fleet.Service) (fleet.Errorer, error) { settings, err := svc.SSOSettings(ctx) if err != nil { return ssoSettingsResponse{Err: err}, nil diff --git a/server/service/setup_experience.go b/server/service/setup_experience.go index 29c88297b2a8..32aa7ac4107e 100644 --- a/server/service/setup_experience.go +++ b/server/service/setup_experience.go @@ -26,7 +26,7 @@ type putSetupExperienceSoftwareResponse struct { func (r putSetupExperienceSoftwareResponse) Error() error { return r.Err } -func putSetupExperienceSoftware(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func putSetupExperienceSoftware(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*putSetupExperienceSoftwareRequest) err := svc.SetSetupExperienceSoftware(ctx, req.TeamID, req.TitleIDs) @@ -59,7 +59,7 @@ type getSetupExperienceSoftwareResponse struct { func (r getSetupExperienceSoftwareResponse) Error() error { return r.Err } -func getSetupExperienceSoftware(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getSetupExperienceSoftware(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSetupExperienceSoftwareRequest) titles, count, meta, err := svc.ListSetupExperienceSoftware(ctx, req.TeamID, req.ListOptions) @@ -90,7 +90,7 @@ type getSetupExperienceScriptResponse struct { func (r getSetupExperienceScriptResponse) Error() error { return r.Err } -func getSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSetupExperienceScriptRequest) downloadRequested := req.Alt == "media" // // TODO: do we want to allow end users to specify team_id=0? if so, we'll need convert it to nil here so that we can @@ -160,7 +160,7 @@ type setSetupExperienceScriptResponse struct { func (r setSetupExperienceScriptResponse) Error() error { return r.Err } -func setSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func setSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*setSetupExperienceScriptRequest) scriptFile, err := req.Script.Open() @@ -196,7 +196,7 @@ func (r deleteSetupExperienceScriptResponse) Error() error { return r.Err } // func (r deleteSetupExperienceScriptResponse) Status() int { return http.StatusNoContent } -func deleteSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteSetupExperienceScriptEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteSetupExperienceScriptRequest) // // TODO: do we want to allow end users to specify team_id=0? if so, we'll need convert it to nil here so that we can // // use it in the auth layer where team_id=0 is not allowed? diff --git a/server/service/software.go b/server/service/software.go index 1dab75804ce1..aba642e5544b 100644 --- a/server/service/software.go +++ b/server/service/software.go @@ -33,7 +33,7 @@ type listSoftwareResponse struct { func (r listSoftwareResponse) Error() error { return r.Err } // Deprecated: use listSoftwareVersionsEndpoint instead -func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listSoftwareRequest) resp, _, err := svc.ListSoftware(ctx, req.SoftwareListOptions) if err != nil { @@ -65,7 +65,7 @@ type listSoftwareVersionsResponse struct { func (r listSoftwareVersionsResponse) Error() error { return r.Err } -func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listSoftwareRequest) // always include pagination for new software versions endpoint (not included by default in @@ -145,7 +145,7 @@ type getSoftwareResponse struct { func (r getSoftwareResponse) Error() error { return r.Err } -func getSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSoftwareRequest) software, err := svc.SoftwareByID(ctx, req.ID, req.TeamID, false) @@ -219,7 +219,7 @@ func (r countSoftwareResponse) Error() error { return r.Err } // Deprecated: counts are now included directly in the listSoftwareVersionsResponse. This // endpoint is retained for backwards compatibility. -func countSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func countSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*countSoftwareRequest) count, err := svc.CountSoftware(ctx, req.SoftwareListOptions) if err != nil { diff --git a/server/service/software_installers.go b/server/service/software_installers.go index 9cc3a917c3e1..f559e6678e0e 100644 --- a/server/service/software_installers.go +++ b/server/service/software_installers.go @@ -20,6 +20,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" ) type uploadSoftwareInstallerRequest struct { @@ -60,7 +61,7 @@ func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http // populate software title ID since we're overriding the decoder that would do it for us titleID, err := uint32FromRequest(r, "id") if err != nil { - return nil, badRequestErr("intFromRequest", err) + return nil, endpoint_utils.BadRequestErr("IntFromRequest", err) } decoded.TitleID = uint(titleID) @@ -163,7 +164,7 @@ func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http return &decoded, nil } -func updateSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func updateSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateSoftwareInstallerRequest) payload := &fleet.UpdateSoftwareInstallerPayload{ @@ -330,7 +331,7 @@ func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http func (r uploadSoftwareInstallerResponse) Error() error { return r.Err } -func uploadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func uploadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uploadSoftwareInstallerRequest) ff, err := req.File.Open() if err != nil { @@ -384,7 +385,7 @@ type deleteSoftwareInstallerResponse struct { func (r deleteSoftwareInstallerResponse) Error() error { return r.Err } func (r deleteSoftwareInstallerResponse) Status() int { return http.StatusNoContent } -func deleteSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteSoftwareInstallerRequest) err := svc.DeleteSoftwareInstaller(ctx, req.TitleID, req.TeamID) if err != nil { @@ -412,7 +413,7 @@ type downloadSoftwareInstallerRequest struct { Token string `url:"token"` } -func getSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSoftwareInstallerRequest) payload, err := svc.DownloadSoftwareInstaller(ctx, false, req.Alt, req.TitleID, req.TeamID) @@ -423,7 +424,7 @@ func getSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc return orbitDownloadSoftwareInstallerResponse{payload: payload}, nil } -func getSoftwareInstallerTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getSoftwareInstallerTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSoftwareInstallerRequest) token, err := svc.GenerateSoftwareInstallerToken(ctx, req.Alt, req.TitleID, req.TeamID) @@ -433,7 +434,7 @@ func getSoftwareInstallerTokenEndpoint(ctx context.Context, request interface{}, return getSoftwareInstallerTokenResponse{Token: token}, nil } -func downloadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func downloadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*downloadSoftwareInstallerRequest) meta, err := svc.GetSoftwareInstallerTokenMetadata(ctx, req.Token, req.TitleID) @@ -540,7 +541,7 @@ func (r installSoftwareResponse) Error() error { return r.Err } func (r installSoftwareResponse) Status() int { return http.StatusAccepted } -func installSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func installSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*installSoftwareRequest) err := svc.InstallSoftwareTitle(ctx, req.HostID, req.SoftwareTitleID) @@ -572,7 +573,7 @@ type uninstallSoftwareRequest struct { SoftwareTitleID uint `url:"software_title_id"` } -func uninstallSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func uninstallSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uninstallSoftwareRequest) err := svc.UninstallSoftwareTitle(ctx, req.HostID, req.SoftwareTitleID) @@ -600,7 +601,7 @@ type getSoftwareInstallResultsResponse struct { func (r getSoftwareInstallResultsResponse) Error() error { return r.Err } -func getSoftwareInstallResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getSoftwareInstallResultsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSoftwareInstallResultsRequest) results, err := svc.GetSoftwareInstallResults(ctx, req.InstallUUID) @@ -637,7 +638,7 @@ type batchSetSoftwareInstallersResponse struct { func (r batchSetSoftwareInstallersResponse) Error() error { return r.Err } func (r batchSetSoftwareInstallersResponse) Status() int { return http.StatusAccepted } -func batchSetSoftwareInstallersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func batchSetSoftwareInstallersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchSetSoftwareInstallersRequest) requestUUID, err := svc.BatchSetSoftwareInstallers(ctx, req.TeamName, req.Software, req.DryRun) if err != nil { @@ -670,7 +671,7 @@ type batchSetSoftwareInstallersResultResponse struct { func (r batchSetSoftwareInstallersResultResponse) Error() error { return r.Err } -func batchSetSoftwareInstallersResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func batchSetSoftwareInstallersResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchSetSoftwareInstallersResultRequest) status, message, packages, err := svc.GetBatchSetSoftwareInstallersResult(ctx, req.TeamName, req.RequestUUID, req.DryRun) if err != nil { @@ -711,7 +712,7 @@ type submitSelfServiceSoftwareInstallResponse struct { func (r submitSelfServiceSoftwareInstallResponse) Error() error { return r.Err } func (r submitSelfServiceSoftwareInstallResponse) Status() int { return http.StatusAccepted } -func submitSelfServiceSoftwareInstall(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func submitSelfServiceSoftwareInstall(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) @@ -773,7 +774,7 @@ type batchAssociateAppStoreAppsResponse struct { func (r batchAssociateAppStoreAppsResponse) Error() error { return r.Err } -func batchAssociateAppStoreAppsEndpoint(ctx context.Context, request any, svc fleet.Service) (errorer, error) { +func batchAssociateAppStoreAppsEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*batchAssociateAppStoreAppsRequest) apps, err := svc.BatchAssociateVPPApps(ctx, req.TeamName, req.Apps, req.DryRun) if err != nil { diff --git a/server/service/software_titles.go b/server/service/software_titles.go index 7e970bef462f..0b07ffd25719 100644 --- a/server/service/software_titles.go +++ b/server/service/software_titles.go @@ -30,7 +30,7 @@ type listSoftwareTitlesResponse struct { func (r listSoftwareTitlesResponse) Error() error { return r.Err } -func listSoftwareTitlesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listSoftwareTitlesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listSoftwareTitlesRequest) titles, count, meta, err := svc.ListSoftwareTitles(ctx, req.SoftwareTitleListOptions) if err != nil { @@ -126,7 +126,7 @@ type getSoftwareTitleResponse struct { func (r getSoftwareTitleResponse) Error() error { return r.Err } -func getSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getSoftwareTitleRequest) software, err := svc.SoftwareTitleByID(ctx, req.ID, req.TeamID) diff --git a/server/service/status.go b/server/service/status.go index ac2ab467be46..8a6f4067637e 100644 --- a/server/service/status.go +++ b/server/service/status.go @@ -17,7 +17,7 @@ type statusResponse struct { func (m statusResponse) Error() error { return m.Err } -func statusResultStoreEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (errorer, error) { +func statusResultStoreEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (fleet.Errorer, error) { var resp statusResponse if err := svc.StatusResultStore(ctx); err != nil { resp.Err = err @@ -37,7 +37,7 @@ func (svc *Service) StatusResultStore(ctx context.Context) error { // Status Live Query //////////////////////////////////////////////////////////////////////////////// -func statusLiveQueryEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (errorer, error) { +func statusLiveQueryEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (fleet.Errorer, error) { var resp statusResponse if err := svc.StatusLiveQuery(ctx); err != nil { resp.Err = err diff --git a/server/service/targets.go b/server/service/targets.go index 36f508109c9c..543ba25b9fde 100644 --- a/server/service/targets.go +++ b/server/service/targets.go @@ -126,7 +126,7 @@ type searchTargetsResponse struct { func (r searchTargetsResponse) Error() error { return r.Err } -func searchTargetsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func searchTargetsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*searchTargetsRequest) results, err := svc.SearchTargets(ctx, req.MatchQuery, req.QueryID, req.Selected) @@ -266,7 +266,7 @@ type countTargetsResponse struct { func (r countTargetsResponse) Error() error { return r.Err } -func countTargetsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func countTargetsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*countTargetsRequest) counts, err := svc.CountHostsInTargets(ctx, req.QueryID, req.Selected) diff --git a/server/service/team_policies.go b/server/service/team_policies.go index d0f4a1684ab6..efec2f8da02d 100644 --- a/server/service/team_policies.go +++ b/server/service/team_policies.go @@ -40,7 +40,7 @@ type teamPolicyResponse struct { func (r teamPolicyResponse) Error() error { return r.Err } -func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*teamPolicyRequest) resp, err := svc.NewTeamPolicy(ctx, req.TeamID, fleet.NewTeamPolicyPayload{ QueryID: req.QueryID, @@ -187,7 +187,7 @@ type listTeamPoliciesResponse struct { func (r listTeamPoliciesResponse) Error() error { return r.Err } -func listTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listTeamPoliciesRequest) inheritedListOptions := fleet.ListOptions{ @@ -266,7 +266,7 @@ type countTeamPoliciesResponse struct { func (r countTeamPoliciesResponse) Error() error { return r.Err } -func countTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func countTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*countTeamPoliciesRequest) resp, err := svc.CountTeamPolicies(ctx, req.TeamID, req.ListOptions.MatchQuery, req.MergeInherited) if err != nil { @@ -313,7 +313,7 @@ type getTeamPolicyByIDResponse struct { func (r getTeamPolicyByIDResponse) Error() error { return r.Err } -func getTeamPolicyByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getTeamPolicyByIDEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getTeamPolicyByIDRequest) teamPolicy, err := svc.GetTeamPolicyByIDQueries(ctx, req.TeamID, req.PolicyID) if err != nil { @@ -362,7 +362,7 @@ type deleteTeamPoliciesResponse struct { func (r deleteTeamPoliciesResponse) Error() error { return r.Err } -func deleteTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteTeamPoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteTeamPoliciesRequest) resp, err := svc.DeleteTeamPolicies(ctx, req.TeamID, req.IDs) if err != nil { @@ -445,7 +445,7 @@ type modifyTeamPolicyResponse struct { func (r modifyTeamPolicyResponse) Error() error { return r.Err } -func modifyTeamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyTeamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyTeamPolicyRequest) resp, err := svc.ModifyTeamPolicy(ctx, req.TeamID, req.PolicyID, req.ModifyPolicyPayload) if err != nil { diff --git a/server/service/team_schedule.go b/server/service/team_schedule.go index 7222ab55bb2d..6f938f91b57d 100644 --- a/server/service/team_schedule.go +++ b/server/service/team_schedule.go @@ -27,7 +27,7 @@ type getTeamScheduleResponse struct { func (r getTeamScheduleResponse) Error() error { return r.Err } -func getTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getTeamScheduleRequest) resp := getTeamScheduleResponse{Scheduled: []scheduledQueryResponse{}} queries, err := svc.GetTeamScheduledQueries(ctx, req.TeamID, req.ListOptions) @@ -88,7 +88,7 @@ func nullIntToPtrUint(v *null.Int) *uint { return ptr.Uint(uint(v.ValueOrZero())) //nolint:gosec // dismiss G115 } -func teamScheduleQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func teamScheduleQueryEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*teamScheduleQueryRequest) resp, err := svc.TeamScheduleQuery(ctx, req.TeamID, &fleet.ScheduledQuery{ QueryID: uintValueOrZero(req.QueryID), @@ -147,7 +147,7 @@ type modifyTeamScheduleResponse struct { func (r modifyTeamScheduleResponse) Error() error { return r.Err } -func modifyTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyTeamScheduleRequest) if _, err := svc.ModifyTeamScheduledQueries(ctx, req.TeamID, req.ScheduledQueryID, req.ScheduledQueryPayload); err != nil { return modifyTeamScheduleResponse{Err: err}, nil @@ -186,7 +186,7 @@ type deleteTeamScheduleResponse struct { func (r deleteTeamScheduleResponse) Error() error { return r.Err } -func deleteTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteTeamScheduleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteTeamScheduleRequest) err := svc.DeleteTeamScheduledQueries(ctx, req.TeamID, req.ScheduledQueryID) if err != nil { diff --git a/server/service/teams.go b/server/service/teams.go index 9d611f542e38..13b8dea25f40 100644 --- a/server/service/teams.go +++ b/server/service/teams.go @@ -30,7 +30,7 @@ type listTeamsResponse struct { func (r listTeamsResponse) Error() error { return r.Err } -func listTeamsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listTeamsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listTeamsRequest) teams, err := svc.ListTeams(ctx, req.ListOptions) if err != nil { @@ -67,7 +67,7 @@ type getTeamResponse struct { func (r getTeamResponse) Error() error { return r.Err } -func getTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getTeamRequest) team, err := svc.GetTeam(ctx, req.ID) if err != nil { @@ -99,7 +99,7 @@ type teamResponse struct { func (r teamResponse) Error() error { return r.Err } -func createTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createTeamRequest) team, err := svc.NewTeam(ctx, req.TeamPayload) @@ -126,7 +126,7 @@ type modifyTeamRequest struct { fleet.TeamPayload } -func modifyTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyTeamRequest) team, err := svc.ModifyTeam(ctx, req.ID, req.TeamPayload) if err != nil { @@ -157,7 +157,7 @@ type deleteTeamResponse struct { func (r deleteTeamResponse) Error() error { return r.Err } -func deleteTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteTeamEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteTeamRequest) err := svc.DeleteTeam(ctx, req.ID) if err != nil { @@ -224,7 +224,7 @@ type applyTeamSpecsResponse struct { func (r applyTeamSpecsResponse) Error() error { return r.Err } -func applyTeamSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func applyTeamSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*applyTeamSpecsRequest) if !req.DryRun { req.DryRunAssumptions = nil @@ -274,7 +274,7 @@ type modifyTeamAgentOptionsRequest struct { json.RawMessage } -func modifyTeamAgentOptionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyTeamAgentOptionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyTeamAgentOptionsRequest) team, err := svc.ModifyTeamAgentOptions(ctx, req.ID, req.RawMessage, fleet.ApplySpecOptions{ Force: req.Force, @@ -303,7 +303,7 @@ type listTeamUsersRequest struct { ListOptions fleet.ListOptions `url:"list_options"` } -func listTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listTeamUsersRequest) users, err := svc.ListTeamUsers(ctx, req.TeamID, req.ListOptions) if err != nil { @@ -337,7 +337,7 @@ type modifyTeamUsersRequest struct { Users []fleet.TeamUser `json:"users"` } -func addTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func addTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyTeamUsersRequest) team, err := svc.AddTeamUsers(ctx, req.TeamID, req.Users) if err != nil { @@ -354,7 +354,7 @@ func (svc *Service) AddTeamUsers(ctx context.Context, teamID uint, users []fleet return nil, fleet.ErrMissingLicense } -func deleteTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteTeamUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyTeamUsersRequest) team, err := svc.DeleteTeamUsers(ctx, req.TeamID, req.Users) if err != nil { @@ -386,7 +386,7 @@ type teamEnrollSecretsResponse struct { func (r teamEnrollSecretsResponse) Error() error { return r.Err } -func teamEnrollSecretsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func teamEnrollSecretsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*teamEnrollSecretsRequest) secrets, err := svc.TeamEnrollSecrets(ctx, req.TeamID) if err != nil { @@ -413,7 +413,7 @@ type modifyTeamEnrollSecretsRequest struct { Secrets []fleet.EnrollSecret `json:"secrets"` } -func modifyTeamEnrollSecretsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyTeamEnrollSecretsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyTeamEnrollSecretsRequest) secrets, err := svc.ModifyTeamEnrollSecrets(ctx, req.TeamID, req.Secrets) if err != nil { diff --git a/server/service/testing_client.go b/server/service/testing_client.go index be1185787350..4d5d01dcaa05 100644 --- a/server/service/testing_client.go +++ b/server/service/testing_client.go @@ -26,6 +26,7 @@ import ( "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/live_query/live_query_mock" "github.com/fleetdm/fleet/v4/server/pubsub" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/sso" "github.com/fleetdm/fleet/v4/server/test" "github.com/ghodss/yaml" @@ -274,7 +275,7 @@ func (ts *withServer) DoRawWithHeaders( if resp.StatusCode != expectedStatusCode { defer resp.Body.Close() - var je jsonError + var je endpoint_utils.JsonError err := json.NewDecoder(resp.Body).Decode(&je) if err != nil { t.Logf("Error trying to decode response body as Fleet jsonError: %s", err) @@ -300,7 +301,7 @@ func (ts *withServer) DoJSON(verb, path string, params interface{}, expectedStat resp := ts.Do(verb, path, params, expectedStatusCode, queryParams...) err := json.NewDecoder(resp.Body).Decode(v) require.NoError(ts.s.T(), err) - if e, ok := v.(errorer); ok { + if e, ok := v.(fleet.Errorer); ok { require.NoError(ts.s.T(), e.Error()) } } @@ -315,7 +316,7 @@ func (ts *withServer) DoJSONWithoutAuth(verb, path string, params interface{}, e }) err = json.NewDecoder(resp.Body).Decode(v) require.NoError(ts.s.T(), err) - if e, ok := v.(errorer); ok { + if e, ok := v.(fleet.Errorer); ok { require.NoError(ts.s.T(), e.Error()) } } diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 28954f2ef038..dc7a2d3043e5 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -427,7 +427,7 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ rootMux.Handle("/", frontendHandler) } - apiHandler := MakeHandler(svc, cfg, logger, limitStore, WithLoginRateLimit(throttled.PerMin(1000))) + apiHandler := MakeHandler(svc, cfg, logger, limitStore, nil, WithLoginRateLimit(throttled.PerMin(1000))) rootMux.Handle("/api/", apiHandler) var errHandler *errorstore.Handler ctxErrHandler := ctxerr.FromContext(ctx) diff --git a/server/service/translator.go b/server/service/translator.go index a805648e7af0..2ec4d0c7944c 100644 --- a/server/service/translator.go +++ b/server/service/translator.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" ) type translatorRequest struct { @@ -18,7 +19,7 @@ type translatorResponse struct { func (r translatorResponse) Error() error { return r.Err } -func translatorEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func translatorEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*translatorRequest) resp, err := svc.Translate(ctx, req.List) if err != nil { @@ -97,7 +98,7 @@ func (svc *Service) Translate(ctx context.Context, payloads []fleet.TranslatePay default: // if no supported payload type, this is bad regardless of authorization svc.authz.SkipAuthorization(ctx) - return nil, badRequestErr( + return nil, endpoint_utils.BadRequestErr( fmt.Sprintf("Type %s is unknown. ", payload.Type), fleet.NewErrorf( fleet.ErrNoUnknownTranslate, diff --git a/server/service/transport.go b/server/service/transport.go index e7dbbe3f67eb..ca913b989ca0 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -3,24 +3,20 @@ package service import ( "context" "encoding/json" - "errors" "fmt" "io" "net/http" - "net/url" "strconv" "strings" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" kithttp "github.com/go-kit/kit/transport/http" "github.com/gorilla/mux" ) -// errBadRoute is used for mux errors -var errBadRoute = errors.New("bad route") - func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { // The has to happen first, if an error happens we'll redirect to an error // page and the error will be logged @@ -34,8 +30,8 @@ func encodeResponse(ctx context.Context, w http.ResponseWriter, response interfa return err } - if e, ok := response.(errorer); ok && e.Error() != nil { - encodeError(ctx, e.Error(), w) + if e, ok := response.(fleet.Errorer); ok && e.Error() != nil { + endpoint_utils.EncodeError(ctx, e.Error(), w) return nil } @@ -74,24 +70,11 @@ type renderHijacker interface { hijackRender(ctx context.Context, w http.ResponseWriter) } -func uintFromRequest(r *http.Request, name string) (uint64, error) { - vars := mux.Vars(r) - s, ok := vars[name] - if !ok { - return 0, errBadRoute - } - u, err := strconv.ParseUint(s, 10, 64) - if err != nil { - return 0, ctxerr.Wrap(r.Context(), err, "uintFromRequest") - } - return u, nil -} - func uint32FromRequest(r *http.Request, name string) (uint32, error) { vars := mux.Vars(r) s, ok := vars[name] if !ok { - return 0, errBadRoute + return 0, endpoint_utils.ErrBadRoute } u, err := strconv.ParseUint(s, 10, 32) if err != nil { @@ -100,32 +83,6 @@ func uint32FromRequest(r *http.Request, name string) (uint32, error) { return uint32(u), nil } -func intFromRequest(r *http.Request, name string) (int64, error) { - vars := mux.Vars(r) - s, ok := vars[name] - if !ok { - return 0, errBadRoute - } - u, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return 0, ctxerr.Wrap(r.Context(), err, "intFromRequest") - } - return u, nil -} - -func stringFromRequest(r *http.Request, name string) (string, error) { - vars := mux.Vars(r) - s, ok := vars[name] - if !ok { - return "", errBadRoute - } - unescaped, err := url.PathUnescape(s) - if err != nil { - return "", ctxerr.Wrap(r.Context(), err, "unescape value in path") - } - return unescaped, nil -} - // default number of items to include per page const defaultPerPage = 20 diff --git a/server/service/trigger.go b/server/service/trigger.go index 89faaabfe3d7..8419f3a09747 100644 --- a/server/service/trigger.go +++ b/server/service/trigger.go @@ -16,7 +16,7 @@ type triggerResponse struct { func (r triggerResponse) Error() error { return r.Err } -func triggerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func triggerEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { _, err := svc.AuthenticatedUser(ctx) if err != nil { return triggerResponse{Err: err}, nil diff --git a/server/service/user_roles.go b/server/service/user_roles.go index 6083b5b1692e..cee221e87726 100644 --- a/server/service/user_roles.go +++ b/server/service/user_roles.go @@ -17,7 +17,7 @@ type applyUserRoleSpecsResponse struct { func (r applyUserRoleSpecsResponse) Error() error { return r.Err } -func applyUserRoleSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func applyUserRoleSpecsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*applyUserRoleSpecsRequest) err := svc.ApplyUserRolesSpecs(ctx, *req.Spec) if err != nil { diff --git a/server/service/users.go b/server/service/users.go index 092d56f6a62d..7843b4b2c26c 100644 --- a/server/service/users.go +++ b/server/service/users.go @@ -41,7 +41,7 @@ type createUserResponse struct { func (r createUserResponse) Error() error { return r.Err } -func createUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createUserRequest) user, sessionKey, err := svc.CreateUser(ctx, req.UserPayload) if err != nil { @@ -142,7 +142,7 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet // Create User From Invite //////////////////////////////////////////////////////////////////////////////// -func createUserFromInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func createUserFromInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*createUserRequest) user, err := svc.CreateUserFromInvite(ctx, req.UserPayload) if err != nil { @@ -197,7 +197,7 @@ type listUsersResponse struct { func (r listUsersResponse) Error() error { return r.Err } -func listUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func listUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*listUsersRequest) users, err := svc.ListUsers(ctx, req.ListOptions) if err != nil { @@ -230,7 +230,7 @@ type getMeRequest struct { IncludeUISettings bool `query:"include_ui_settings,optional"` } -func meEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func meEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { user, err := svc.AuthenticatedUser(ctx) if err != nil { return getUserResponse{Err: err}, nil @@ -288,7 +288,7 @@ type getUserResponse struct { func (r getUserResponse) Error() error { return r.Err } -func getUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getUserRequest) user, err := svc.User(ctx, req.ID) if err != nil { @@ -359,7 +359,7 @@ type modifyUserResponse struct { func (r modifyUserResponse) Error() error { return r.Err } -func modifyUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func modifyUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*modifyUserRequest) user, err := svc.ModifyUser(ctx, req.ID, req.UserPayload) if err != nil { @@ -556,7 +556,7 @@ type deleteUserResponse struct { func (r deleteUserResponse) Error() error { return r.Err } -func deleteUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteUserRequest) err := svc.DeleteUser(ctx, req.ID) if err != nil { @@ -610,7 +610,7 @@ type requirePasswordResetResponse struct { func (r requirePasswordResetResponse) Error() error { return r.Err } -func requirePasswordResetEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func requirePasswordResetEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*requirePasswordResetRequest) user, err := svc.RequirePasswordReset(ctx, req.ID, req.Require) if err != nil { @@ -662,7 +662,7 @@ type changePasswordResponse struct { func (r changePasswordResponse) Error() error { return r.Err } -func changePasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func changePasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*changePasswordRequest) err := svc.ChangePassword(ctx, req.OldPassword, req.NewPassword) return changePasswordResponse{Err: err}, nil @@ -718,7 +718,7 @@ type getInfoAboutSessionsForUserResponse struct { func (r getInfoAboutSessionsForUserResponse) Error() error { return r.Err } -func getInfoAboutSessionsForUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getInfoAboutSessionsForUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getInfoAboutSessionsForUserRequest) sessions, err := svc.GetInfoAboutSessionsForUser(ctx, req.ID) if err != nil { @@ -770,7 +770,7 @@ type deleteSessionsForUserResponse struct { func (r deleteSessionsForUserResponse) Error() error { return r.Err } -func deleteSessionsForUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func deleteSessionsForUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteSessionsForUserRequest) err := svc.DeleteSessionsForUser(ctx, req.ID) if err != nil { @@ -802,7 +802,7 @@ type changeEmailResponse struct { func (r changeEmailResponse) Error() error { return r.Err } -func changeEmailEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func changeEmailEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*changeEmailRequest) newEmailAddress, err := svc.ChangeUserEmail(ctx, req.Token) if err != nil { @@ -961,7 +961,7 @@ type performRequiredPasswordResetResponse struct { func (r performRequiredPasswordResetResponse) Error() error { return r.Err } -func performRequiredPasswordResetEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func performRequiredPasswordResetEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*performRequiredPasswordResetRequest) user, err := svc.PerformRequiredPasswordReset(ctx, req.Password) if err != nil { @@ -1055,7 +1055,7 @@ type resetPasswordResponse struct { func (r resetPasswordResponse) Error() error { return r.Err } -func resetPasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func resetPasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*resetPasswordRequest) err := svc.ResetPassword(ctx, req.PasswordResetToken, req.NewPassword) return resetPasswordResponse{Err: err}, nil @@ -1130,7 +1130,7 @@ type forgotPasswordResponse struct { func (r forgotPasswordResponse) Error() error { return r.Err } func (r forgotPasswordResponse) Status() int { return http.StatusAccepted } -func forgotPasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func forgotPasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*forgotPasswordRequest) // Any error returned by the service should not be returned to the // client to prevent information disclosure (it will be logged in the diff --git a/server/service/vpp.go b/server/service/vpp.go index c87a31247443..de01698bf6c9 100644 --- a/server/service/vpp.go +++ b/server/service/vpp.go @@ -9,6 +9,7 @@ import ( "github.com/docker/go-units" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" ) ////////////////////////////////////////////////////////////////////////////// @@ -26,7 +27,7 @@ type getAppStoreAppsResponse struct { func (r getAppStoreAppsResponse) Error() error { return r.Err } -func getAppStoreAppsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func getAppStoreAppsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*getAppStoreAppsRequest) apps, err := svc.GetAppStoreApps(ctx, &req.TeamID) if err != nil { @@ -63,7 +64,7 @@ type addAppStoreAppResponse struct { func (r addAppStoreAppResponse) Error() error { return r.Err } -func addAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func addAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*addAppStoreAppRequest) err := svc.AddAppStoreApp(ctx, req.TeamID, fleet.VPPAppTeam{ VPPAppID: fleet.VPPAppID{AdamID: req.AppStoreID, Platform: req.Platform}, @@ -105,7 +106,7 @@ type updateAppStoreAppResponse struct { func (r updateAppStoreAppResponse) Error() error { return r.Err } -func updateAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func updateAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*updateAppStoreAppRequest) updatedApp, err := svc.UpdateAppStoreApp(ctx, req.TitleID, req.TeamID, req.SelfService, req.LabelsIncludeAny, req.LabelsExcludeAny) @@ -166,7 +167,7 @@ func (r uploadVPPTokenResponse) Error() error { return r.Err } -func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func uploadVPPTokenEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*uploadVPPTokenRequest) file, err := req.File.Open() if err != nil { @@ -219,7 +220,7 @@ func (patchVPPTokenRenewRequest) DecodeRequest(ctx context.Context, r *http.Requ decoded.File = r.MultipartForm.File["token"][0] - id, err := uintFromRequest(r, "id") + id, err := endpoint_utils.UintFromRequest(r, "id") if err != nil { return nil, ctxerr.Wrap(ctx, err, "failed to parse vpp token id") } @@ -240,7 +241,7 @@ func (r patchVPPTokenRenewResponse) Error() error { return r.Err } -func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { +func patchVPPTokenRenewEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) { req := request.(*patchVPPTokenRenewRequest) file, err := req.File.Open() if err != nil { @@ -280,7 +281,7 @@ type patchVPPTokensTeamsResponse struct { func (r patchVPPTokensTeamsResponse) Error() error { return r.Err } -func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (errorer, error) { +func patchVPPTokensTeams(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*patchVPPTokensTeamsRequest) tok, err := svc.UpdateVPPTokenTeams(ctx, req.ID, req.TeamIDs) @@ -311,7 +312,7 @@ type getVPPTokensResponse struct { func (r getVPPTokensResponse) Error() error { return r.Err } -func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (errorer, error) { +func getVPPTokens(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { tokens, err := svc.GetVPPTokens(ctx) if err != nil { return getVPPTokensResponse{Err: err}, nil @@ -348,7 +349,7 @@ func (r deleteVPPTokenResponse) Error() error { return r.Err } func (r deleteVPPTokenResponse) Status() int { return http.StatusNoContent } -func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (errorer, error) { +func deleteVPPToken(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) { req := request.(*deleteVPPTokenRequest) err := svc.DeleteVPPToken(ctx, req.ID) diff --git a/server/service/vulnerabilities.go b/server/service/vulnerabilities.go index d422622ec90a..2207a520c65e 100644 --- a/server/service/vulnerabilities.go +++ b/server/service/vulnerabilities.go @@ -48,7 +48,7 @@ var cveRegex = regexp.MustCompile(`(?i)^CVE-\d{4}-\d{4}\d*$`) func (r listVulnerabilitiesResponse) Error() error { return r.Err } -func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (errorer, error) { +func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (fleet.Errorer, error) { request := req.(*listVulnerabilitiesRequest) vulns, meta, err := svc.ListVulnerabilities(ctx, request.VulnListOptions) if err != nil { @@ -142,7 +142,7 @@ func (r getVulnerabilityResponse) Status() int { return r.statusCode } -func getVulnerabilityEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (errorer, error) { +func getVulnerabilityEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (fleet.Errorer, error) { request := req.(*getVulnerabilityRequest) vuln, known, err := svc.Vulnerability(ctx, request.CVE, request.TeamID, false) diff --git a/tools/android/android.go b/tools/android/android.go new file mode 100644 index 000000000000..b65b79805d8e --- /dev/null +++ b/tools/android/android.go @@ -0,0 +1,144 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "log" + "os" + + "google.golang.org/api/androidmanagement/v1" + "google.golang.org/api/option" +) + +// Required env vars: +var ( + androidServiceCredentials = os.Getenv("FLEET_ANDROID_SERVICE_CREDENTIALS") + androidProjectID = os.Getenv("FLEET_ANDROID_PROJECT_ID") +) + +func main() { + if androidServiceCredentials == "" || androidProjectID == "" { + log.Fatal("FLEET_ANDROID_SERVICE_CREDENTIALS and FLEET_ANDROID_PROJECT_ID must be set") + } + + command := flag.String("command", "", "") + enterpriseID := flag.String("enterprise_id", "", "") + deviceID := flag.String("device_id", "", "") + flag.Parse() + + ctx := context.Background() + mgmt, err := androidmanagement.NewService(ctx, option.WithCredentialsJSON([]byte(androidServiceCredentials))) + if err != nil { + log.Fatalf("Error creating android management service: %v", err) + } + + switch *command { + case "enterprises.delete": + enterprisesDelete(mgmt, *enterpriseID) + case "enterprises.list": + enterprisesList(mgmt) + case "policies.list": + policiesList(mgmt, *enterpriseID) + case "devices.list": + devicesList(mgmt, *enterpriseID) + case "devices.delete": + devicesDelete(mgmt, *enterpriseID, *deviceID) + case "devices.issueCommand.RELINQUISH_OWNERSHIP": + devicesRelinquishOwnership(mgmt, *enterpriseID, *deviceID) + default: + log.Fatalf("Unknown command: %s", *command) + } + +} + +func enterprisesDelete(mgmt *androidmanagement.Service, enterpriseID string) { + if enterpriseID == "" { + log.Fatalf("enterprise_id must be set") + } + _, err := mgmt.Enterprises.Delete("enterprises/" + enterpriseID).Do() + if err != nil { + log.Fatalf("Error deleting enterprise: %v", err) + } +} + +func enterprisesList(mgmt *androidmanagement.Service) { + enterprises, err := mgmt.Enterprises.List().ProjectId(androidProjectID).Do() + if err != nil { + log.Fatalf("Error listing enterprises: %v", err) + } + if len(enterprises.Enterprises) == 0 { + log.Printf("No enterprises found") + return + } + for _, enterprise := range enterprises.Enterprises { + log.Printf("Enterprise: %+v", *enterprise) + } +} + +func policiesList(mgmt *androidmanagement.Service, enterpriseID string) { + if enterpriseID == "" { + log.Fatalf("enterprise_id must be set") + } + result, err := mgmt.Enterprises.Policies.List("enterprises/" + enterpriseID).Do() + if err != nil { + log.Fatalf("Error listing policies: %v", err) + } + if len(result.Policies) == 0 { + log.Printf("No policies found") + return + } + for _, policy := range result.Policies { + log.Printf("Policy: %+v", *policy) + } +} + +func devicesList(mgmt *androidmanagement.Service, enterpriseID string) { + if enterpriseID == "" { + log.Fatalf("enterprise_id must be set") + } + result, err := mgmt.Enterprises.Devices.List("enterprises/" + enterpriseID).Do() + if err != nil { + log.Fatalf("Error listing devices: %v", err) + } + if len(result.Devices) == 0 { + log.Printf("No policies found") + return + } + for _, device := range result.Devices { + data, err := json.MarshalIndent(device, "", " ") + if err != nil { + log.Fatalf("Error marshalling device: %v", err) + } + log.Println(string(data)) + } + log.Printf("Total devices: %d", len(result.Devices)) +} + +func devicesDelete(mgmt *androidmanagement.Service, enterpriseID string, deviceID string) { + if enterpriseID == "" || deviceID == "" { + log.Fatalf("enterprise_id and device_id must be set") + } + _, err := mgmt.Enterprises.Devices.Delete("enterprises/" + enterpriseID + "/devices/" + deviceID).Do() + if err != nil { + log.Fatalf("Error listing devices: %v", err) + } + log.Printf("Device %s deleted", deviceID) +} + +func devicesRelinquishOwnership(mgmt *androidmanagement.Service, enterpriseID, deviceID string) { + if enterpriseID == "" || deviceID == "" { + log.Fatalf("enterprise_id and device_id must be set") + } + operation, err := mgmt.Enterprises.Devices.IssueCommand("enterprises/"+enterpriseID+"/devices/"+deviceID, &androidmanagement.Command{ + Type: "RELINQUISH_OWNERSHIP", + }).Do() + if err != nil { + log.Fatalf("Error issuing command: %v", err) + } + data, err := json.MarshalIndent(operation, "", " ") + if err != nil { + log.Fatalf("Error marshalling operation: %v", err) + } + log.Println(string(data)) +} diff --git a/tools/tuf/releaser.sh b/tools/tuf/releaser.sh index 85a2113799e8..1401c5e4edbf 100755 --- a/tools/tuf/releaser.sh +++ b/tools/tuf/releaser.sh @@ -87,22 +87,22 @@ promote_component_edge_to_stable () { pushd "$TUF_DIRECTORY" case $component_name in orbit) - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/macos/edge/orbit" --platform macos --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/linux/edge/orbit" --platform linux --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/linux-arm64/edge/orbit" --platform linux-arm64 --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/windows/edge/orbit.exe" --platform windows --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/macos/edge/orbit" --platform macos --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/linux/edge/orbit" --platform linux --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/linux-arm64/edge/orbit" --platform linux-arm64 --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/orbit/windows/edge/orbit.exe" --platform windows --name orbit --version "$component_version" -t "$major.$minor" -t "$major" -t stable ;; desktop) - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/macos/edge/desktop.app.tar.gz" --platform macos --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/linux/edge/desktop.tar.gz" --platform linux --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/linux-arm64/edge/desktop.tar.gz" --platform linux-arm64 --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/windows/edge/fleet-desktop.exe" --platform windows --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/macos/edge/desktop.app.tar.gz" --platform macos --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/linux/edge/desktop.tar.gz" --platform linux --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/linux-arm64/edge/desktop.tar.gz" --platform linux-arm64 --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/desktop/windows/edge/fleet-desktop.exe" --platform windows --name desktop --version "$component_version" -t "$major.$minor" -t "$major" -t stable ;; osqueryd) - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/macos-app/edge/osqueryd.app.tar.gz" --platform macos-app --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/linux/edge/osqueryd" --platform linux --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/linux-arm64/edge/osqueryd" --platform linux-arm64 --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable - fleetctl updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/windows/edge/osqueryd.exe" --platform windows --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/macos-app/edge/osqueryd.app.tar.gz" --platform macos-app --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/linux/edge/osqueryd" --platform linux --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/linux-arm64/edge/osqueryd" --platform linux-arm64 --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$REPOSITORY_DIRECTORY/targets/osqueryd/windows/edge/osqueryd.exe" --platform windows --name osqueryd --version "$component_version" -t "$major.$minor" -t "$major" -t stable ;; *) echo "Unknown component $component_name" @@ -162,14 +162,14 @@ release_fleetd_to_edge () { --github-username "$GITHUB_USERNAME" --github-api-token "$GITHUB_TOKEN" \ --retry pushd "$TUF_DIRECTORY" - fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/macos/orbit" --platform macos --name orbit --version "$VERSION" -t edge - fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/linux/orbit" --platform linux --name orbit --version "$VERSION" -t edge - fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/linux-arm64/orbit" --platform linux-arm64 --name orbit --version "$VERSION" -t edge - fleetctl updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/windows/orbit.exe" --platform windows --name orbit --version "$VERSION" -t edge - fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/macos/desktop.app.tar.gz" --platform macos --name desktop --version "$VERSION" -t edge - fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/linux/desktop.tar.gz" --platform linux --name desktop --version "$VERSION" -t edge - fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/linux-arm64/desktop.tar.gz" --platform linux-arm64 --name desktop --version "$VERSION" -t edge - fleetctl updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/windows/fleet-desktop.exe" --platform windows --name desktop --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/macos/orbit" --platform macos --name orbit --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/linux/orbit" --platform linux --name orbit --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/linux-arm64/orbit" --platform linux-arm64 --name orbit --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$ORBIT_ARTIFACT_DOWNLOAD_DIRECTORY/windows/orbit.exe" --platform windows --name orbit --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/macos/desktop.app.tar.gz" --platform macos --name desktop --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/linux/desktop.tar.gz" --platform linux --name desktop --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/linux-arm64/desktop.tar.gz" --platform linux-arm64 --name desktop --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$DESKTOP_ARTIFACT_DOWNLOAD_DIRECTORY/windows/fleet-desktop.exe" --platform windows --name desktop --version "$VERSION" -t edge popd } @@ -197,10 +197,10 @@ release_osqueryd_to_edge () { --github-api-token "$GITHUB_TOKEN" \ --retry pushd "$TUF_DIRECTORY" - fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/macos/osqueryd.app.tar.gz" --platform macos-app --name osqueryd --version "$VERSION" -t edge - fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/linux/osqueryd" --platform linux --name osqueryd --version "$VERSION" -t edge - fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/linux-arm64/osqueryd" --platform linux-arm64 --name osqueryd --version "$VERSION" -t edge - fleetctl updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/windows/osqueryd.exe" --platform windows --name osqueryd --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/macos/osqueryd.app.tar.gz" --platform macos-app --name osqueryd --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/linux/osqueryd" --platform linux --name osqueryd --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/linux-arm64/osqueryd" --platform linux-arm64 --name osqueryd --version "$VERSION" -t edge + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates add --target "$OSQUERYD_ARTIFACT_DOWNLOAD_DIRECTORY/windows/osqueryd.exe" --platform windows --name osqueryd --version "$VERSION" -t edge popd } @@ -217,7 +217,7 @@ release_to_edge () { update_timestamp () { pushd "$TUF_DIRECTORY" - fleetctl updates timestamp + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" updates timestamp popd } @@ -266,16 +266,16 @@ prompt () { print_reminder () { if [[ $ACTION == "release-to-edge" ]]; then if [[ $COMPONENT == "fleetd" ]]; then - prompt "To smoke test the release make sure to generate and install fleetd with 'fleetctl package [...] --update-url=https://updates-staging.fleetdm.com --orbit-channel=edge --desktop-channel=edge' on a Linux, Windows and macOS VM." + prompt "To smoke test the release make sure to generate and install fleetd with 'fleetctl package [...] --update-url=https://updates-staging.fleetdm.com --update-interval=1m --orbit-channel=edge --desktop-channel=edge' on Linux amd64, Linux arm64, Windows, and macOS." elif [[ $COMPONENT == "osqueryd" ]]; then - prompt "To smoke test the release make sure to generate and install fleetd with 'fleetctl package [...] --update-url=https://updates-staging.fleetdm.com --osqueryd-channel=edge' on a Linux, Windows and macOS VM." + prompt "To smoke test the release make sure to generate and install fleetd with 'fleetctl package [...] --update-url=https://updates-staging.fleetdm.com --osqueryd-channel=edge' on Linux amd64, Linux arm64, Windows, and macOS." fi elif [[ $ACTION == "promote-edge-to-stable" ]]; then - prompt "To smoke test the release make sure to generate and install fleetd with 'fleetctl package [...] --update-url=https://updates-staging.fleetdm.com' on a Linux, Windows and macOS VM." + prompt "To smoke test the release make sure to generate and install fleetd with 'fleetctl package [...] --update-url=https://updates-staging.fleetdm.com --update-interval=1m' on Linux amd64, Linux arm64, Windows, and macOS." elif [[ $ACTION == "update-timestamp" ]]; then : elif [[ $ACTION == "release-to-production" ]]; then - prompt "To smoke test the release make sure to generate and install fleetd with on a Linux, Windows and macOS VM. Use 'fleetctl package [...] --orbit-channel=edge --desktop-channel=edge' if you are releasing fleetd to 'edge' or 'fleetctl package [...] --osqueryd-channel=edge' if you are releasing osquery to 'edge'." + prompt "To smoke test the release make sure to generate and install fleetd with on Linux amd64, Linux arm64, Windows, and macOS. Use 'fleetctl package [...] --update-interval=1m --orbit-channel=edge --desktop-channel=edge' if you are releasing fleetd to 'edge' or 'fleetctl package [...] --osqueryd-channel=edge' if you are releasing osquery to 'edge'." else echo "Unsupported action: $ACTION" exit 1 @@ -283,8 +283,8 @@ print_reminder () { } fleetctl_version_check () { - which fleetctl - fleetctl --version + echo "Using '$GIT_REPOSITORY_DIRECTORY/build/fleetctl'" + "$GIT_REPOSITORY_DIRECTORY/build/fleetctl" --version prompt "Make sure the fleetctl executable and version are correct." } diff --git a/website/api/controllers/deliver-contact-form-message.js b/website/api/controllers/deliver-contact-form-message.js index bd5b602f1795..bbad0ad58093 100644 --- a/website/api/controllers/deliver-contact-form-message.js +++ b/website/api/controllers/deliver-contact-form-message.js @@ -50,18 +50,58 @@ module.exports = { fn: async function({emailAddress, firstName, lastName, message}) { + + let userHasPremiumSubscription = false; + let thisSubscription; + if(this.req.me){ + thisSubscription = await Subscription.findOne({user: this.req.me.id}); + if(thisSubscription) { + userHasPremiumSubscription = true; + } + } + if (!sails.config.custom.slackWebhookUrlForContactForm) { throw new Error( 'Message not delivered: slackWebhookUrlForContactForm needs to be configured in sails.config.custom. Here\'s the undelivered message: ' + `Name: ${firstName + ' ' + lastName}, Email: ${emailAddress}, Message: ${message ? message : 'No message.'}` ); } + if(userHasPremiumSubscription){ + // If the user has a Fleet Premium subscription, prepend the message with details about their subscription. + let subscriptionDetails =` +Fleet Premium subscription details: +- Fleet Premium subscriber since: ${new Date(thisSubscription.createdAt).toISOString().split('T')[0]} +- Next billing date: ${new Date(thisSubscription.nextBillingAt).toISOString().split('T')[0]} +- Host count: ${thisSubscription.numberOfHosts} +- Organization: ${this.req.me.organization} +----- + + `; + message = subscriptionDetails + message; + await sails.helpers.sendTemplateEmail.with({ + to: sails.config.custom.fromEmailAddress, + replyTo: { + name: firstName + ' '+ lastName, + emailAddress: emailAddress, + }, + subject: 'New contact form message', + layout: false, + template: 'email-contact-form', + templateData: { + emailAddress, + firstName, + lastName, + message, + }, + }); + } await sails.helpers.http.post(sails.config.custom.slackWebhookUrlForContactForm, { text: `New contact form message: (cc: <@U0801Q57JDU>, <@U05CS07KASK>) (Remember: we have to email back; can't just reply to this thread.)`+ `Name: ${firstName + ' ' + lastName}, Email: ${emailAddress}, Message: ${message ? message : 'No message.'}` }); + sails.helpers.salesforce.updateOrCreateContactAndAccount.with({ emailAddress: emailAddress, firstName: firstName, diff --git a/website/api/controllers/deliver-talk-to-us-form-submission.js b/website/api/controllers/deliver-talk-to-us-form-submission.js index e7962954a105..9c8c74bcc812 100644 --- a/website/api/controllers/deliver-talk-to-us-form-submission.js +++ b/website/api/controllers/deliver-talk-to-us-form-submission.js @@ -104,6 +104,7 @@ module.exports = { sails.log.warn(`Background task failed: When a user submitted the "Talk to us" form, a lead/contact could not be updated in the CRM for this email address: ${emailAddress}.`, err); } }); + // FUTURE: create POV here } return; diff --git a/website/api/controllers/view-contact.js b/website/api/controllers/view-contact.js index 3b644211ee96..b61de65e695b 100644 --- a/website/api/controllers/view-contact.js +++ b/website/api/controllers/view-contact.js @@ -27,12 +27,24 @@ module.exports = { let formToShow = 'talk-to-us'; + let userIsLoggedIn = !! this.req.me; + let userHasPremiumSubscription = false; + if(userIsLoggedIn) { + let thisSubscription = await Subscription.findOne({user: this.req.me.id}); + if(thisSubscription){ + formToShow = 'contact'; + userHasPremiumSubscription = true; + } + } + if(sendMessage) { formToShow = 'contact'; } // Respond with view. return { formToShow, + userIsLoggedIn, + userHasPremiumSubscription }; } diff --git a/website/assets/images/articles/pr-approval-2-933x483@2x.jpg b/website/assets/images/articles/pr-approval-2-933x483@2x.jpg new file mode 100644 index 000000000000..57d0ad3d07f1 Binary files /dev/null and b/website/assets/images/articles/pr-approval-2-933x483@2x.jpg differ diff --git a/website/assets/images/articles/pr-approval-921x475@2x.jpg b/website/assets/images/articles/pr-approval-921x475@2x.jpg new file mode 100644 index 000000000000..f8271376d828 Binary files /dev/null and b/website/assets/images/articles/pr-approval-921x475@2x.jpg differ diff --git a/website/assets/images/articles/preventing-mistakes-1-1423x771@2x.gif b/website/assets/images/articles/preventing-mistakes-1-1423x771@2x.gif new file mode 100644 index 000000000000..9d3b13914da8 Binary files /dev/null and b/website/assets/images/articles/preventing-mistakes-1-1423x771@2x.gif differ diff --git a/website/assets/images/articles/preventing-mistakes-2-960x540@2x.gif b/website/assets/images/articles/preventing-mistakes-2-960x540@2x.gif new file mode 100644 index 000000000000..3e84c26f4e99 Binary files /dev/null and b/website/assets/images/articles/preventing-mistakes-2-960x540@2x.gif differ diff --git a/website/assets/images/articles/preventing-mistakes-3-1423x771@2x.gif b/website/assets/images/articles/preventing-mistakes-3-1423x771@2x.gif new file mode 100644 index 000000000000..2af344d0635d Binary files /dev/null and b/website/assets/images/articles/preventing-mistakes-3-1423x771@2x.gif differ diff --git a/website/assets/images/articles/preventing-mistakes-4-960x540@2x.gif b/website/assets/images/articles/preventing-mistakes-4-960x540@2x.gif new file mode 100644 index 000000000000..97fb60b1b666 Binary files /dev/null and b/website/assets/images/articles/preventing-mistakes-4-960x540@2x.gif differ diff --git a/website/assets/images/articles/preventing-mistakes-5-960x540@2x.gif b/website/assets/images/articles/preventing-mistakes-5-960x540@2x.gif new file mode 100644 index 000000000000..8265c207c0e5 Binary files /dev/null and b/website/assets/images/articles/preventing-mistakes-5-960x540@2x.gif differ diff --git a/website/assets/js/pages/entrance/login.page.js b/website/assets/js/pages/entrance/login.page.js index a18738b75d53..4d04ad0dced8 100644 --- a/website/assets/js/pages/entrance/login.page.js +++ b/website/assets/js/pages/entrance/login.page.js @@ -44,6 +44,12 @@ parasails.registerPage('login', { this.pageToRedirectToAfterLogin = '/new-license#login'; window.location.hash = ''; } + // If we're redirecting this user to the contact form after they log in, modify the link to the /register page and the pageToRedirectToAfterLogin. + if(window.location.hash && window.location.hash === '#contact'){ + this.registerSlug = '/register'; + this.pageToRedirectToAfterLogin = '/contact?sendMessage'; + window.location.hash = ''; + } }, mounted: async function() { //… diff --git a/website/assets/js/pages/osquery-table-details.page.js b/website/assets/js/pages/osquery-table-details.page.js index dd3d719a954b..3e08d7e8dfee 100644 --- a/website/assets/js/pages/osquery-table-details.page.js +++ b/website/assets/js/pages/osquery-table-details.page.js @@ -40,6 +40,8 @@ parasails.registerPage('osquery-table-details', { // otherwise, default the filter to be the first supported platform of the currently viewed table. this.selectedPlatform = this.tableToDisplay.platforms[0] === 'darwin' ? 'apple' : this.tableToDisplay.platforms[0]; } + // Note: we do not personalize the selected platform on this page based on the user's + // current OS because the default table that the /tables url redirects to does not support windows. }, mounted: async function() { diff --git a/website/assets/js/pages/policy-library.page.js b/website/assets/js/pages/policy-library.page.js index cd334d3e253d..6af58a3888c1 100644 --- a/website/assets/js/pages/policy-library.page.js +++ b/website/assets/js/pages/policy-library.page.js @@ -13,7 +13,9 @@ parasails.registerPage('policy-library', { //… }, mounted: async function () { - //… + if(bowser.windows){ + this.selectedPlatform = 'windows'; + } }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ diff --git a/website/assets/js/pages/query-library.page.js b/website/assets/js/pages/query-library.page.js index 08ac6514b15a..a31061fdeac3 100644 --- a/website/assets/js/pages/query-library.page.js +++ b/website/assets/js/pages/query-library.page.js @@ -13,7 +13,9 @@ parasails.registerPage('query-library', { //… }, mounted: async function () { - //… + if(bowser.windows){ + this.selectedPlatform = 'windows'; + } }, // ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗ diff --git a/website/assets/js/pages/vital-details.page.js b/website/assets/js/pages/vital-details.page.js index 1a9bbdc4f50a..e26bc7d3f402 100644 --- a/website/assets/js/pages/vital-details.page.js +++ b/website/assets/js/pages/vital-details.page.js @@ -20,6 +20,8 @@ parasails.registerPage('vital-details', { // All links to vitals in the on-page navigation have the currently selected filter appended to them, this lets us persist the user's filter when they navigate to a new page. if(['#apple','#linux','#windows','#chrome'].includes(window.location.hash)){ this.selectedPlatform = window.location.hash.split('#')[1]; + } else if(bowser.windows){ + this.selectedPlatform = 'windows'; } let columnNamesForThisQuery = []; let tableNamesForThisQuery = []; diff --git a/website/assets/styles/layout.less b/website/assets/styles/layout.less index 4d8a9a21a273..ceb3f743813d 100644 --- a/website/assets/styles/layout.less +++ b/website/assets/styles/layout.less @@ -295,7 +295,8 @@ html, body { } [purpose='mobile-nav'] { position: fixed; - min-height: 100vh; + max-height: 100vh; + overflow: scroll; top: 0; left: 0; right: 0; @@ -779,7 +780,7 @@ body.detected-mobile { padding: 19px 20px; } [purpose='mobile-nav-container'] { - padding: 8px 24px 0px 24px; + padding: 8px 24px 24px 24px; } } } @@ -814,7 +815,7 @@ body.detected-mobile { padding: 19px 16px; } [purpose='mobile-nav-container'] { - padding: 8px 16px 0px 16px; + padding: 8px 16px 24px 16px; } } } diff --git a/website/assets/styles/pages/contact.less b/website/assets/styles/pages/contact.less index 487c58d3a36e..21033b70cf17 100644 --- a/website/assets/styles/pages/contact.less +++ b/website/assets/styles/pages/contact.less @@ -27,6 +27,34 @@ color: #515774; font-size: 14px; } + [purpose='note'] { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + border-radius: 8px; + border: 1px solid #B4B2FE; + background: #F7F7FC; + text-decoration: none; + padding: 16px 24px; + margin-bottom: 40px; + img { + height: 16px; + margin-right: 8px; + } + p { + a { + color: #515774; + } + color: #515774; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 21px; + margin-bottom: 0px; + } + } [purpose='form-option'] { user-select: none; cursor: pointer; diff --git a/website/assets/styles/pages/osquery-table-details.less b/website/assets/styles/pages/osquery-table-details.less index b4387785bbfa..250933cf474b 100644 --- a/website/assets/styles/pages/osquery-table-details.less +++ b/website/assets/styles/pages/osquery-table-details.less @@ -42,7 +42,7 @@ height: 20px; margin-right: 10px; } - p { + h1 { color: #192147; text-align: center; margin-bottom: 0px; @@ -55,7 +55,7 @@ &.selected { border-bottom: 2px solid var(--text-text-brand, #192147); padding: 16px 40px 22px 40px; - p { + h1 { font-weight: 700; } } diff --git a/website/assets/styles/pages/policy-details.less b/website/assets/styles/pages/policy-details.less index 67138ee2753e..6f11c5b71f20 100644 --- a/website/assets/styles/pages/policy-details.less +++ b/website/assets/styles/pages/policy-details.less @@ -336,6 +336,9 @@ padding-bottom: 8px; margin-bottom: 8px; } + h1 { + font-size: 0px; + } img { height: 24px; margin-right: 16px; diff --git a/website/assets/styles/pages/policy-library.less b/website/assets/styles/pages/policy-library.less index 6a2656624cca..233f5ae57268 100644 --- a/website/assets/styles/pages/policy-library.less +++ b/website/assets/styles/pages/policy-library.less @@ -135,7 +135,12 @@ margin-bottom: 16px; } } + [purpose='platform-filters'] { + &.detected-windows { + flex-direction: row-reverse; + } + flex-direction: row; border-bottom: 1px solid #E2E4EA; margin-bottom: 48px; [purpose='platform-filter'] { @@ -149,7 +154,7 @@ height: 20px; margin-right: 10px; } - p { + h1 { color: #192147; text-align: center; margin-bottom: 0px; @@ -162,7 +167,7 @@ } &.selected { border-bottom: 2px solid var(--text-text-brand, #192147); - p { + h1 { font-weight: 700; } } diff --git a/website/assets/styles/pages/query-detail.less b/website/assets/styles/pages/query-detail.less index 793d29637f3a..a33f581a8f28 100644 --- a/website/assets/styles/pages/query-detail.less +++ b/website/assets/styles/pages/query-detail.less @@ -178,6 +178,11 @@ padding-bottom: 8px; margin-bottom: 8px; } + h1 { + font-size: 0px; + line-height: 0px; + margin-bottom: 0px; + } img { height: 24px; margin-right: 16px; diff --git a/website/assets/styles/pages/query-library.less b/website/assets/styles/pages/query-library.less index 9990bb0444cc..684561322c07 100644 --- a/website/assets/styles/pages/query-library.less +++ b/website/assets/styles/pages/query-library.less @@ -142,6 +142,10 @@ } } [purpose='platform-filters'] { + &.detected-windows { + flex-direction: row-reverse; + } + flex-direction: row; border-bottom: 1px solid #E2E4EA; margin-bottom: 48px; [purpose='platform-filter'] { @@ -155,7 +159,7 @@ height: 20px; margin-right: 10px; } - p { + h1 { color: #192147; text-align: center; margin-bottom: 0px; @@ -168,7 +172,7 @@ } &.selected { border-bottom: 2px solid var(--text-text-brand, #192147); - p { + h1 { font-weight: 700; } } diff --git a/website/assets/styles/pages/support.less b/website/assets/styles/pages/support.less index a60481dcc4a0..7a2d183dea9d 100644 --- a/website/assets/styles/pages/support.less +++ b/website/assets/styles/pages/support.less @@ -92,7 +92,7 @@ width: 100%; } a { - width: 50%; + width: 33%; } @@ -116,7 +116,7 @@ [purpose='support-row'] { [purpose='support-card'] { - width: 274px; + width: 341px; height: 221px; } } diff --git a/website/assets/styles/pages/vital-details.less b/website/assets/styles/pages/vital-details.less index 6a1607c57b43..4fd3c21b59b2 100644 --- a/website/assets/styles/pages/vital-details.less +++ b/website/assets/styles/pages/vital-details.less @@ -216,6 +216,10 @@ } } [purpose='platform-filters'] { + &.detected-windows { + flex-direction: row-reverse; + } + flex-direction: row; border-bottom: 1px solid #E2E4EA; margin-bottom: 32px; padding-top: 8px; @@ -230,7 +234,7 @@ height: 20px; margin-right: 10px; } - p { + h1 { color: #192147; text-align: center; margin-bottom: 0px; @@ -243,7 +247,7 @@ &.selected { border-bottom: 2px solid var(--text-text-brand, #192147); padding: 16px 40px 22px 40px; - p { + h1 { font-weight: 700; } } diff --git a/website/config/routes.js b/website/config/routes.js index 5373bf96e9cd..cc845422ab3f 100644 --- a/website/config/routes.js +++ b/website/config/routes.js @@ -869,6 +869,7 @@ module.exports.routes = { 'GET /contribute-to/policies': 'https://github.com/fleetdm/fleet/edit/main/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml', 'GET /learn-more-about/end-user-license-agreement': '/guides/macos-setup-experience#end-user-authentication-and-end-user-license-agreement-eula', 'GET /learn-more-about/end-user-authentication': '/guides/macos-setup-experience#end-user-authentication-and-end-user-license-agreement-eula', + 'GET /learn-more-about/policy-templates': '/policies', // Sitemap // ============================================================================================================= diff --git a/website/views/pages/contact.ejs b/website/views/pages/contact.ejs index 8c4ac9994ede..2fdf4e7b0a44 100644 --- a/website/views/pages/contact.ejs +++ b/website/views/pages/contact.ejs @@ -3,15 +3,33 @@

    Get in touch

    -

    Let us help you deploy and evaluate Fleet quickly for yourself. We’d love to save you some time.

    +

    Dedicated professional support from the Fleet team.

    +

    Let us help you deploy and evaluate Fleet quickly for yourself. We’d love to save you some time.

    Schedule a personalized demo for your team and get support or training.

    Schedule a personalized demo, or ask us anything. We’d love to chat.

    -
    +
    Talk to an engineer
    Send a message
    + + +
    +
    + An icon indicating that this section has important information +
    +

    Already a Fleet customer? Sign in for Premium support

    +
    + +
    +
    + An icon indicating that this section has important information +
    +

    Already a Premium customer? Reach out to us directly or use the form below.

    +
    + +
    diff --git a/website/views/pages/osquery-table-details.ejs b/website/views/pages/osquery-table-details.ejs index 5c4344711e4b..e72859849d7c 100644 --- a/website/views/pages/osquery-table-details.ejs +++ b/website/views/pages/osquery-table-details.ejs @@ -6,30 +6,30 @@

    Tables

    Fleet uses osquery tables to query operating system, hardware, and software data. Each table provides specific data for analysis and filtering.

    -
    -
    -

    +

    +
    +

    macOS Apple -

    +

    -
    -

    +

    +

    Linux Linux -

    +

    -
    -

    +

    +

    Windows Windows -

    +

    -
    -

    +

    +

    Chrome ChromeOS -

    +

    diff --git a/website/views/pages/policy-details.ejs b/website/views/pages/policy-details.ejs index 1b9a83455833..f6c30e4bb594 100644 --- a/website/views/pages/policy-details.ejs +++ b/website/views/pages/policy-details.ejs @@ -64,10 +64,10 @@

    Platform

    - macOS - Windows - Linux - ChromeOS +

    macOSmacOS/apple

    +

    WindowsWindows

    +

    LinuxLinux

    +

    ChromeOSChromeOS

    diff --git a/website/views/pages/policy-library.ejs b/website/views/pages/policy-library.ejs index 46f1d6676044..ae935ee59e8d 100644 --- a/website/views/pages/policy-library.ejs +++ b/website/views/pages/policy-library.ejs @@ -10,24 +10,24 @@

    Contributions welcome over on GitHub.

    -
    +
    -

    +

    macOS Apple -

    +

    -

    +

    Linux Linux -

    +

    -

    +

    Windows Windows -

    +

    diff --git a/website/views/pages/query-detail.ejs b/website/views/pages/query-detail.ejs index 4040b0c62703..0d653c6c425a 100644 --- a/website/views/pages/query-detail.ejs +++ b/website/views/pages/query-detail.ejs @@ -45,10 +45,10 @@

    Platform

    - macOS - Windows - Linux - ChromeOS +

    macOSApple

    +

    WindowsWindows

    +

    LinuxLinux

    +

    ChromeOSChromeOS

    diff --git a/website/views/pages/query-library.ejs b/website/views/pages/query-library.ejs index 8b29c6467d7f..e0bf9df9a1da 100644 --- a/website/views/pages/query-library.ejs +++ b/website/views/pages/query-library.ejs @@ -9,24 +9,24 @@

    A collection of optional queries you can run anytime. Contributions welcome over on GitHub.

    -
    +
    -

    +

    macOS Apple -

    +

    -

    +

    Linux Linux -

    +

    -

    +

    Windows Windows -

    +

    diff --git a/website/views/pages/support.ejs b/website/views/pages/support.ejs index e471f2bb6668..9a940a517e62 100644 --- a/website/views/pages/support.ejs +++ b/website/views/pages/support.ejs @@ -37,13 +37,6 @@
    - -
    -
    -

    +

    +
    +

    macOS Apple -

    +

    -
    -

    +

    +

    Linux Linux -

    +

    -
    +

    Windows Windows

    -
    -

    +

    +

    Chrome ChromeOS -

    +

    diff --git a/yarn.lock b/yarn.lock index 6cfe647fbe0d..481d045c3c7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3825,6 +3825,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311" integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g== +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/unist@*", "@types/unist@^2.0.0": version "2.0.6" resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz" @@ -6075,10 +6080,12 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.0: dependencies: domelementtype "^2.2.0" -dompurify@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.3.tgz#cfe3ce4232c216d923832f68f2aa18b2fb9bd223" - integrity sha512-5sOWYSNPaxz6o2MUPvtyxTTqR4D3L77pr5rUQoWgD5ROQtVIZQgJkXbo1DLlK3vj11YGw5+LnF4SYti4gZmwng== +dompurify@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e" + integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg== + optionalDependencies: + "@types/trusted-types" "^2.0.7" domutils@^1.5.1: version "1.7.0" @@ -6131,9 +6138,9 @@ electron-to-chromium@^1.5.73: integrity sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ== elliptic@^6.5.3, elliptic@^6.5.4: - version "6.6.0" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.0.tgz#5919ec723286c1edf28685aa89261d4761afa210" - integrity sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA== + version "6.6.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" + integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== dependencies: bn.js "^4.11.9" brorand "^1.1.0"