Skip to content

Patch when closed: policy and package api - #49871

Merged
jkatz01 merged 14 commits into
feat/39962-patch-when-closedfrom
49416-patch-when-closed-policy-package-api
Jul 30, 2026
Merged

Patch when closed: policy and package api#49871
jkatz01 merged 14 commits into
feat/39962-patch-when-closedfrom
49416-patch-when-closed-policy-package-api

Conversation

@jkatz01

@jkatz01 jkatz01 commented Jul 24, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #49416

  • Adds patch_when_closed to the new policy, update policy, and update package endpoints
  • Sets pre_install_query in the software title endpoint to software_installers.app_open_query if the policy is a patch when closed policy (discussed in standup)
  • Deletes the existing pre_install_query if setting patch_when_closed (discussed in standup)
  • Fixes a small error in the patch_policy package
  • Fixes the default macOS query in patch_policy package to use to escape symbols in a.path

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.

  • Timeouts are implemented and retries are limited to avoid infinite loops

  • If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes

Testing

Summary by CodeRabbit

  • New Features

    • Added a “patch when closed” option for patch policies and Fleet-maintained apps.
    • Added installer controls for enabling patching and configuring whether apps must be closed before installation.
    • Automatically manages pre-install behavior and continuous automation requirements for these policies.
  • Bug Fixes

    • Improved app detection based on application path prefixes on macOS.
    • Corrected RStudio process detection on Windows.
    • Added validation to prevent incompatible patch policy settings.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.71429% with 33 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (feat/39962-patch-when-closed@31a096e). Learn more about missing BASE report.

Files with missing lines Patch % Lines
ee/server/service/software_installers.go 57.77% 17 Missing and 2 partials ⚠️
server/datastore/mysql/software_installers.go 61.90% 7 Missing and 1 partial ⚠️
server/service/team_policies.go 63.63% 2 Missing and 2 partials ⚠️
server/service/software_titles.go 0.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@                       Coverage Diff                       @@
##             feat/39962-patch-when-closed   #49871   +/-   ##
===============================================================
  Coverage                                ?   67.90%           
===============================================================
  Files                                   ?     3891           
  Lines                                   ?   248652           
  Branches                                ?    13022           
===============================================================
  Hits                                    ?   168837           
  Misses                                  ?    64593           
  Partials                                ?    15222           
Flag Coverage Δ
backend 69.28% <60.71%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jkatz01
jkatz01 marked this pull request as ready for review July 24, 2026 15:31
@jkatz01
jkatz01 requested a review from a team as a code owner July 24, 2026 15:31
@jkatz01

jkatz01 commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds patch_when_closed to policy and software installer APIs, validates its relationship with patch policies and continuous automations, and persists the new policy state. Installer updates can create, modify, or delete patch policies. Patch-when-closed installs use managed app-open queries, while title updates clear conflicting pre-install queries. macOS app-path matching and the RStudio Windows process query are corrected, with corresponding tests and datastore mocks added.

Possibly related PRs

  • fleetdm/fleet#49691: Updates the managed app-open query plumbing and related macOS predicate tests.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and matches the main change: patch_when_closed support for policy and package APIs.
Description check ✅ Passed The description includes the issue link, change summary, and testing status, matching the required template well enough.
Linked Issues check ✅ Passed The changes cover the #49416 policy, update-package, validation, managed-query, and persistence requirements.
Out of Scope Changes check ✅ Passed The touched files all align with patch_when_closed support, query fixes, persistence, and test updates.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 49416-patch-when-closed-policy-package-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
server/service/team_policies_test.go (1)

290-301: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't verify what its name claims.

With ContinuousAutomationsEnabled omitted (defaults false), the patch_when_closed && !continuous_automations_enabled guard in newTeamPolicyPayloadToPolicyPayload fires first, so this test passes due to errPatchWhenClosedRequiresContinuousAutomations (which happens to also contain the substring "patch_when_closed"), not because of any type-based rejection. Add ContinuousAutomationsEnabled: true to isolate the intended case.

See consolidated comment — the source constraint this test intends to cover doesn't appear to exist anywhere in the codebase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/service/team_policies_test.go` around lines 290 - 301, Update the test
case “create rejects patch_when_closed on non-patch policy” to set
ContinuousAutomationsEnabled: true in the NewTeamPolicyPayload, bypassing the
unrelated continuous-automations guard and isolating the non-patch policy
validation in newTeamPolicyPayloadToPolicyPayload.
server/service/team_policies.go (2)

47-53: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing validation: patch_when_closed should require type == "patch".

newTeamPolicyPayloadToPolicyPayload validates that PatchWhenClosed requires ContinuousAutomationsEnabled, but nothing here rejects patch_when_closed: true on a non-patch (dynamic) policy. If a caller sets Type omitted/dynamic, PatchWhenClosed: true, and ContinuousAutomationsEnabled: true, this passes silently and persists a meaningless patch_when_closed=true on a dynamic policy.

See consolidated comment for related gaps in ApplyPolicySpecs and the masked test coverage in team_policies_test.go.

Also applies to: 107-112, 300-339, 717-728, 804-809

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/service/team_policies.go` around lines 47 - 53, The team policy
conversion and application paths, including newTeamPolicyPayloadToPolicyPayload
and the referenced ApplyPolicySpecs flows, must reject PatchWhenClosed=true
unless the policy Type is "patch". Add this validation alongside the existing
ContinuousAutomationsEnabled check and preserve the existing behavior for patch
policies and false values; update the affected team policy test coverage
accordingly.

1-1: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No validation anywhere restricts patch_when_closed to type == "patch" policies. All three sites below share this root cause: the API and GitOps paths both accept patch_when_closed: true on a non-patch policy (silently storing meaningless state), and the one test intended to cover this doesn't actually exercise it.

  • server/service/team_policies.go#L300-339: add a check in newTeamPolicyPayloadToPolicyPayload (and the equivalent logic in modifyPolicy) rejecting patch_when_closed: true when the policy type isn't patch.
  • server/datastore/mysql/policies.go#L1809-1821: ApplyPolicySpecs needs the same spec.Type == fleet.PolicyTypePatch guard for spec.PatchWhenClosed, and should decide whether to match the API's explicit-rejection behavior for a conflicting continuous_automations_enabled: false instead of silently overriding it.
  • server/service/team_policies_test.go#L290-301: once the guard exists, add ContinuousAutomationsEnabled: true to this subtest so the assertion actually isolates the type-rejection path instead of incidentally passing via errPatchWhenClosedRequiresContinuousAutomations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/service/team_policies.go` at line 1, The policy validation paths
currently allow patch_when_closed on non-patch policies. Update
newTeamPolicyPayloadToPolicyPayload, modifyPolicy, and ApplyPolicySpecs to
reject patch_when_closed: true unless the policy type is fleet.PolicyTypePatch,
while preserving the API’s explicit conflict handling for
continuous_automations_enabled: false. In the relevant team policy test, set
ContinuousAutomationsEnabled to true so it specifically verifies type rejection.
server/datastore/mysql/policies.go (1)

1809-1821: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

GitOps apply diverges from the API for patch_when_closed/continuous_automations_enabled validation.

Here, an explicit continuous_automations_enabled: false combined with patch_when_closed: true is silently overridden to true, whereas team_policies.go's newTeamPolicyPayloadToPolicyPayload/modifyPolicy explicitly reject that same combination with errPatchWhenClosedRequiresContinuousAutomations. There's also no check here that patch_when_closed is only meaningful for spec.Type == fleet.PolicyTypePatch.

See consolidated comment for the related gap in team_policies.go and the masked test in team_policies_test.go.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/datastore/mysql/policies.go` around lines 1809 - 1821, The GitOps
policy apply path must match API validation for patch-when-closed settings. In
the flow containing the `spec.PatchWhenClosed` assignment before
`tx.ExecContext`, reject an explicit false `spec.ContinuousAutomationsEnabled`
when `spec.PatchWhenClosed` is true using
`errPatchWhenClosedRequiresContinuousAutomations`, and validate that
`PatchWhenClosed` is only set for `fleet.PolicyTypePatch`; do not silently
override the caller’s value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/patch_policy/patch_policy.go`:
- Line 172: Update the openTemplate query in the patch policy code to replace
LEFT(p.path, LENGTH(a.path) + 1) with substr(p.path, 1, LENGTH(a.path) + 1),
preserving the existing predicate semantics. Update both corresponding
expected-query strings in the patch policy tests to match the generated
SQLite-compatible query.

In `@server/fleet/policies.go`:
- Around line 617-618: Update Service.ApplyPolicySpecs to check
license.IsPremium(ctx) whenever a policy spec requests patch_when_closed, and
reject the request for non-premium viewers using the existing error pattern. Add
a regression test covering a free-tier caller applying this option, while
preserving successful application for premium viewers.

---

Nitpick comments:
In `@server/datastore/mysql/policies.go`:
- Around line 1809-1821: The GitOps policy apply path must match API validation
for patch-when-closed settings. In the flow containing the
`spec.PatchWhenClosed` assignment before `tx.ExecContext`, reject an explicit
false `spec.ContinuousAutomationsEnabled` when `spec.PatchWhenClosed` is true
using `errPatchWhenClosedRequiresContinuousAutomations`, and validate that
`PatchWhenClosed` is only set for `fleet.PolicyTypePatch`; do not silently
override the caller’s value.

In `@server/service/team_policies_test.go`:
- Around line 290-301: Update the test case “create rejects patch_when_closed on
non-patch policy” to set ContinuousAutomationsEnabled: true in the
NewTeamPolicyPayload, bypassing the unrelated continuous-automations guard and
isolating the non-patch policy validation in
newTeamPolicyPayloadToPolicyPayload.

In `@server/service/team_policies.go`:
- Around line 47-53: The team policy conversion and application paths, including
newTeamPolicyPayloadToPolicyPayload and the referenced ApplyPolicySpecs flows,
must reject PatchWhenClosed=true unless the policy Type is "patch". Add this
validation alongside the existing ContinuousAutomationsEnabled check and
preserve the existing behavior for patch policies and false values; update the
affected team policy test coverage accordingly.
- Line 1: The policy validation paths currently allow patch_when_closed on
non-patch policies. Update newTeamPolicyPayloadToPolicyPayload, modifyPolicy,
and ApplyPolicySpecs to reject patch_when_closed: true unless the policy type is
fleet.PolicyTypePatch, while preserving the API’s explicit conflict handling for
continuous_automations_enabled: false. In the relevant team policy test, set
ContinuousAutomationsEnabled to true so it specifically verifies type rejection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4678e792-28a4-4bd7-9415-ab3ed7ae4ff9

📥 Commits

Reviewing files that changed from the base of the PR and between 61bae56 and 50cb2ba.

📒 Files selected for processing (19)
  • ee/maintained-apps/ingesters/homebrew/ingester_test.go
  • ee/server/service/software_installers.go
  • ee/server/service/software_installers_test.go
  • pkg/patch_policy/patch_policy.go
  • pkg/patch_policy/patch_policy_test.go
  • server/datastore/mysql/migrations/tables/20260721173820_PatchWhenClosed_test.go
  • server/datastore/mysql/policies.go
  • server/datastore/mysql/software_installers.go
  • server/datastore/mysql/software_installers_test.go
  • server/fleet/api_policies.go
  • server/fleet/datastore.go
  • server/fleet/policies.go
  • server/fleet/software_installer.go
  • server/mock/datastore_mock.go
  • server/service/global_policies.go
  • server/service/software_installers.go
  • server/service/software_titles.go
  • server/service/team_policies.go
  • server/service/team_policies_test.go

Comment thread pkg/patch_policy/patch_policy.go Outdated
Comment thread server/fleet/policies.go
jkatz01 added 13 commits July 28, 2026 10:19
…install query

Thread patch_when_closed through the team-policy create/update API and the
update-package endpoint, and resolve the Fleet-managed "is app open" query at
install time.

Policy create/update:
- Add patch_when_closed to the create request, payload mapping, and modify path.
- Require type=patch (with a patch_software_title_id) when enabling it.
- Auto-set continuous_automations_enabled when patch_when_closed is on, and
  reject an explicit continuous_automations_enabled=false in the same request.
- Persist the column in the INSERT, UPDATE, ApplyPolicySpecs upsert, and the
  shared policy read column list; add PatchWhenClosed to PolicySpec.

Update-package:
- Add patch and patch_when_closed (FMA-only) to the request/payload.
- Reconcile the title's patch policy: create/update on patch=true, delete on
  patch=false, keyed on patch_software_title_id.
- Reject a direct pre_install_query edit while the managed query owns it.
- Return patch_when_closed from GetPatchPolicy.

GetSoftwareInstallDetails:
- Join the triggering policy and return app_open_query as the pre-install
  condition only on the policy-automation path when patch_when_closed is set;
  self-service, manual, and setup-experience installs keep the user query.
Collapse the two-phase planPatchPolicy/applyPatchPolicyPlan (struct, managed
method, and both functions) into one reconcilePatchPolicy call. It validates,
rejects a managed pre-install query edit, and creates, updates, or deletes the
title's patch policy.

Keep the FMA-only guard as an early fail-fast at the call site, and run the
reconcile at the "persist changes" boundary so a validation error can't leave a
stray policy behind. Reconcile re-derives state from the datastore, so a retry
after a partial failure converges without special conflict handling.

Consolidate the plan/apply tests into TestReconcilePatchPolicy and cover the
FMA-only rejection through the update-package flow.
… query in Go

Update-package: a newly created patch policy defaults patch_when_closed on (Force
patch is the explicit false), matching the documented default. An existing policy
keeps its value.

GetSoftwareInstallDetails: select the raw pre_install_query, app_open_query, and
the triggering policy's patch_when_closed, and pick the managed query in Go
instead of a SQL CASE. Policy-automation installs of a patch-when-closed policy
get the managed app-open query; every other path keeps the user query.
…en_closed is enabled

Enabling patch_when_closed hands the pre-install condition to the managed app open query, so the installer's user pre-install query is deleted and the title's pending installs are canceled. This applies from both the update-package endpoint and policy create/update.
The update-package endpoint no longer errors when patch_when_closed is set with patch off; it's ignored, since there's no policy to carry it. planPatchPolicy is now a pure resolver that takes the current patch policy as a parameter.
…abled=false with it

Per the API docs PR: update-package rejects patch_when_closed set while patch isn't enabled; policy create/update reject an explicit continuous_automations_enabled=false alongside patch_when_closed=true.
Replace the LIKE-based process path match with an exact prefix check so
a sibling app whose path shares the same prefix can't match.
…lers

Guard the patch-policy fetch, planning, and pre-install query clear behind a
Fleet-maintained-app check. A regular package edit no longer queries or touches
a patch policy, which also fixes a nil pointer dereference when the datastore
mock has no GetPatchPolicy stub.
osquery runs queries in SQLite, which has no LEFT function, so the generated
open query would error on-device. substr(p.path, 1, LENGTH(a.path) + 1) matches
the same leftmost characters.
The GitOps policy spec path already rejects the other premium-only fields for
free-tier callers but let patch_when_closed through. Reject it the same way.
@jkatz01
jkatz01 force-pushed the 49416-patch-when-closed-policy-package-api branch from 7ea13d3 to 06b8e4f Compare July 28, 2026 14:50
}
}

// Create, update, or delete the patch policy after the installer save

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query is blanked & saved before this policy write; if NewTeamPolicy fails, query's gone and no policy exists. Should these be atomic?

@jkatz01 jkatz01 Jul 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be great but I'm not sure how to fit these in a transaction or something similar. I tried to add as much validation as possible before this point to help avoid this situation but that's not guaranteed.

Maybe I should move the part that deletes the pre_install_query until after New/Modify/DeleteTeamPolicy ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can actually just delete 824 because NewTeamPolicy/ModifyTeamPolicy already call ClearPreInstallQueryForTitle internally and those run here, i.e. after the installer save. I think you'd just get the ordering for free.

if err != nil {
return nil, err
}
if patchFlag && patchWhenClosedFlag && existingInstaller.PreInstallQuery != "" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling patch_when_closed overwrites pre_install_query in place; disabling never restores it. Intended, or should we preserve/restore it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what we discussed on standup is that it's fine to just delete it. Maybe it could be an error to tell the user to manually delete the query?

Comment thread server/service/team_policies.go
Comment thread server/service/team_policies_test.go Outdated
@@ -278,8 +300,8 @@ func TestTeamPolicyPatchWhenClosed(t *testing.T) {
require.ErrorContains(t, err, "patch_when_closed")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trips the continuous-automations check first (both messages contain patch_when_closed), so the non-patch guard is never hit, seems like. Add ContinuousAutomationsEnabled: true?

continuousAutomationsEnabled := p.ContinuousAutomationsEnabled
if p.PatchWhenClosed {
continuousAutomationsEnabled = true
if p.PatchWhenClosed && !p.ContinuousAutomationsEnabled {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gist of the PR is to make the API reject patch_when_closed & continuous_automations_enabled=false, right? But it seems like here, ApplyPolicySpecs still silently coerces it. Should specs reject too?

@jkatz01 jkatz01 Jul 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I got from the API docs changes is:
"If patch_when_closed is true on a policy (existing or being set in the same request) and the request explicitly sets continuous_automations_enabled to false, Fleet rejects the request instead of silently overriding it."

So I think it makes sense in ApplyPolicySpecs to silently coerce it if the field is missing, and reject it if the field is explicitly set to false. I think this is already the behaviour in the gitops changes for this feature right?

@cdcme cdcme Jul 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only the modify path does that. It's the one field typed *bool. PolicySpec and create are plain bool, so they can't tell false from omitted. Specs always coerce, create always rejects. Fine to leave if we document gitops as normalizing; for real parity they'd need to be *bool I think.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I would think that plain bool values will get the explicit error. And *bool will only get it if not nil.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's true for create, but the opposite for specs. If what we want is "coerce-if-missing / reject-if-explicit-false" everywhere, then I think create and specs would need *bool and that same nil check, if I'm not mistaken here.

@jkatz01
jkatz01 marked this pull request as draft July 28, 2026 17:51
Also reject patch_when_closed in policy specs when the package already has a
pre-install query, and fix the non-patch patch_when_closed test so it reaches
its guard instead of tripping the continuous-automations check first.
@jkatz01
jkatz01 marked this pull request as ready for review July 28, 2026 22:28
@jkatz01
jkatz01 merged commit 345eb05 into feat/39962-patch-when-closed Jul 30, 2026
37 checks passed
@jkatz01
jkatz01 deleted the 49416-patch-when-closed-policy-package-api branch July 30, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Patch when closed: policy + package API and managed pre-install query

2 participants