Propagate self-service flag on uninstalls through to activity (#29691)

Fixes part of unreleased for #28846.

# Checklist for submitter

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

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated automated tests
- [ ] Manual QA for all new/changed functionality
This commit is contained in:
Ian Littman
2025-06-03 09:09:43 -06:00
committed by GitHub
parent 88296aee24
commit 6eb6884c4f
14 changed files with 78 additions and 67 deletions
+4 -2
View File
@@ -396,6 +396,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
'software_title', COALESCE(st.name, ua.payload->>'$.software_title_name', ''),
'script_execution_id', ua.execution_id,
'status', 'pending_uninstall',
'self_service', COALESCE(ua.payload->'$.self_service', FALSE) IS TRUE,
'policy_id', siua.policy_id,
'policy_name', p.name
) as details,
@@ -1112,7 +1113,7 @@ func (ds *Datastore) activateNextUpcomingActivityForBatchOfHosts(ctx context.Con
// - If no other activity is still activated and there is an upcoming
// activity to activate next, it does so, respecting the priority and enqueue
// order. Activation consists of inserting the activity in its respective
// table, e.g. `host_script_results` for scripts, `host_sofware_installs` for
// table, e.g. `host_script_results` for scripts, `host_software_installs` for
// software installs, `host_vpp_software_installs` and nano command queue for
// VPP installs; and setting the activated_at timestamp in the
// `upcoming_activities` table.
@@ -1359,7 +1360,7 @@ ORDER BY
INSERT INTO
host_software_installs
(execution_id, host_id, software_installer_id, user_id, uninstall, installer_filename,
software_title_id, software_title_name, version)
software_title_id, software_title_name, self_service, version)
SELECT
ua.execution_id,
ua.host_id,
@@ -1369,6 +1370,7 @@ SELECT
'', -- no installer_filename for uninstalls
siua.software_title_id,
COALESCE(ua.payload->>'$.software_title_name', '[deleted title]'),
COALESCE(ua.payload->>'$.self_service', FALSE),
'unknown'
FROM
upcoming_activities ua
+3 -3
View File
@@ -571,7 +571,7 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
// create pending install and uninstall requests for h3 that will be deleted
_, err = ds.InsertSoftwareInstallRequest(ctx, h3.ID, sw3Meta.InstallerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
err = ds.InsertSoftwareUninstallRequest(ctx, "uninstallRun", h3.ID, sw3Meta.InstallerID)
err = ds.InsertSoftwareUninstallRequest(ctx, "uninstallRun", h3.ID, sw3Meta.InstallerID, false)
require.NoError(t, err)
// delete installer (should clear pending requests)
@@ -1344,7 +1344,7 @@ func testActivateNextActivity(t *testing.T, ds *Datastore) {
// create a pending uninstall request
sw1_2 := uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, sw1_2, h1.ID, sw1)
err = ds.InsertSoftwareUninstallRequest(ctx, sw1_2, h1.ID, sw1, false)
require.NoError(t, err)
// still hasn't changed the pending queue
@@ -1495,7 +1495,7 @@ func testActivateItselfOnEmptyQueue(t *testing.T, ds *Datastore) {
// create a pending uninstall request
sw1_2 := uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, sw1_2, h1.ID, sw1)
err = ds.InsertSoftwareUninstallRequest(ctx, sw1_2, h1.ID, sw1, false)
require.NoError(t, err)
// set a result for the software uninstall
+17 -12
View File
@@ -1060,7 +1060,7 @@ func (ds *Datastore) runInstallerUpdateSideEffectsInTransaction(ctx context.Cont
return affectedHostIDs, nil
}
func (ds *Datastore) InsertSoftwareUninstallRequest(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error {
func (ds *Datastore) InsertSoftwareUninstallRequest(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint, selfService bool) error {
const (
getInstallerStmt = `SELECT title_id, COALESCE(st.name, '[deleted title]') title_name
FROM software_installers si LEFT JOIN software_titles st ON si.title_id = st.id WHERE si.id = ?`
@@ -1074,7 +1074,8 @@ VALUES
'installer_filename', '',
'version', 'unknown',
'software_title_name', ?,
'user', (SELECT JSON_OBJECT('name', name, 'email', email, 'gravatar_url', gravatar_url) FROM users WHERE id = ?)
'user', (SELECT JSON_OBJECT('name', name, 'email', email, 'gravatar_url', gravatar_url) FROM users WHERE id = ?),
'self_service', ?
)
)`
@@ -1123,6 +1124,7 @@ VALUES
executionID,
installerDetails.TitleName,
userID,
selfService,
)
if err != nil {
return err
@@ -2305,17 +2307,17 @@ func (ds *Datastore) HasSelfServiceSoftwareInstallers(ctx context.Context, hostP
return hasInstallers, nil
}
func (ds *Datastore) GetSoftwareTitleNameFromExecutionID(ctx context.Context, executionID string) (string, error) {
func (ds *Datastore) GetDetailsForUninstallFromExecutionID(ctx context.Context, executionID string) (string, bool, error) {
stmt := `
SELECT st.name
SELECT COALESCE(st.name, hsi.software_title_name) name, hsi.self_service
FROM software_titles st
INNER JOIN software_installers si ON si.title_id = st.id
INNER JOIN host_software_installs hsi ON hsi.software_installer_id = si.id
WHERE hsi.execution_id = ?
WHERE hsi.execution_id = ? AND hsi.uninstall = TRUE
UNION
SELECT st.name
SELECT st.name, COALESCE(ua.payload->'$.self_service', FALSE) self_service
FROM
software_titles st
INNER JOIN software_installers si ON si.title_id = st.id
@@ -2324,14 +2326,17 @@ func (ds *Datastore) GetSoftwareTitleNameFromExecutionID(ctx context.Context, ex
INNER JOIN upcoming_activities ua ON ua.id = siua.upcoming_activity_id
WHERE
ua.execution_id = ? AND
ua.activity_type IN ('software_install', 'software_uninstall')
ua.activity_type = 'software_uninstall'
`
var name string
err := sqlx.GetContext(ctx, ds.reader(ctx), &name, stmt, executionID, executionID)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "get software title name from execution ID")
var result struct {
Name string `db:"name"`
SelfService bool `db:"self_service"`
}
return name, nil
err := sqlx.GetContext(ctx, ds.reader(ctx), &result, stmt, executionID, executionID)
if err != nil {
return "", false, ctxerr.Wrap(ctx, err, "get software details for uninstall activity from execution ID")
}
return result.Name, result.SelfService, nil
}
func (ds *Datastore) GetSoftwareInstallersWithoutPackageIDs(ctx context.Context) (map[uint]string, error) {
@@ -43,7 +43,7 @@ func TestSoftwareInstallers(t *testing.T) {
{"GetOrGenerateSoftwareInstallerTitleID", testGetOrGenerateSoftwareInstallerTitleID},
{"BatchSetSoftwareInstallersScopedViaLabels", testBatchSetSoftwareInstallersScopedViaLabels},
{"MatchOrCreateSoftwareInstallerWithAutomaticPolicies", testMatchOrCreateSoftwareInstallerWithAutomaticPolicies},
{"GetSoftwareTitleNameFromExecutionID", testGetSoftwareTitleNameFromExecutionID},
{"GetDetailsForUninstallFromExecutionID", testGetDetailsForUninstallFromExecutionID},
{"GetTeamsWithInstallerByHash", testGetTeamsWithInstallerByHash},
{"BatchSetSoftwareInstallersSetupExperienceSideEffects", testBatchSetSoftwareInstallersSetupExperienceSideEffects},
{"EditDeleteSoftwareInstallersActivateNextActivity", testEditDeleteSoftwareInstallersActivateNextActivity},
@@ -372,7 +372,7 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
TeamID: teamID,
})
require.NoError(t, err)
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, hostPendingUninstall.ID, si.InstallerID)
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, hostPendingUninstall.ID, si.InstallerID, false)
require.NoError(t, err)
// Host with failed uninstall
@@ -387,7 +387,7 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
execID = "uuid" + tag + tc
err = ds.InsertSoftwareUninstallRequest(ctx, execID, hostFailedUninstall.ID, si.InstallerID)
err = ds.InsertSoftwareUninstallRequest(ctx, execID, hostFailedUninstall.ID, si.InstallerID, false)
require.NoError(t, err)
_, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
HostID: hostFailedUninstall.ID,
@@ -408,7 +408,7 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
execID = "uuid" + tag + tc
err = ds.InsertSoftwareUninstallRequest(ctx, execID, hostUninstalled.ID, si.InstallerID)
err = ds.InsertSoftwareUninstallRequest(ctx, execID, hostUninstalled.ID, si.InstallerID, false)
require.NoError(t, err)
_, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
HostID: hostUninstalled.ID,
@@ -418,7 +418,7 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
require.NoError(t, err)
// Uninstall request with unknown host
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, 99999, si.InstallerID)
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, 99999, si.InstallerID, false)
assert.ErrorContains(t, err, "Host")
userTeamFilter := fleet.TeamFilter{
@@ -2576,7 +2576,7 @@ Software won't be installed on Linux hosts with Debian-based distributions becau
require.Equal(t, "[Install software] Something2 (msi) 3", team3Policies[2].Name)
}
func testGetSoftwareTitleNameFromExecutionID(t *testing.T, ds *Datastore) {
func testGetDetailsForUninstallFromExecutionID(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
@@ -2615,9 +2615,10 @@ func testGetSoftwareTitleNameFromExecutionID(t *testing.T, ds *Datastore) {
require.NoError(t, err)
// get software title for unknown exec id
title, err := ds.GetSoftwareTitleNameFromExecutionID(ctx, "unknown")
title, selfService, err := ds.GetDetailsForUninstallFromExecutionID(ctx, "unknown")
require.ErrorIs(t, err, sql.ErrNoRows)
require.Empty(t, title)
require.False(t, selfService)
// create a couple pending software install request, the first will be
// immediately present in host_software_installs too (activated)
@@ -2626,13 +2627,8 @@ func testGetSoftwareTitleNameFromExecutionID(t *testing.T, ds *Datastore) {
req2, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installer2, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
title, err = ds.GetSoftwareTitleNameFromExecutionID(ctx, req1)
require.NoError(t, err)
require.Equal(t, "foobar", title)
title, err = ds.GetSoftwareTitleNameFromExecutionID(ctx, req2)
require.NoError(t, err)
require.Equal(t, "barfoo", title)
_, _, err = ds.GetDetailsForUninstallFromExecutionID(ctx, req1)
require.ErrorIs(t, err, sql.ErrNoRows)
// record a result for req1, will be deleted from upcoming_activities
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
@@ -2642,22 +2638,18 @@ func testGetSoftwareTitleNameFromExecutionID(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
title, err = ds.GetSoftwareTitleNameFromExecutionID(ctx, req1)
require.NoError(t, err)
require.Equal(t, "foobar", title)
title, err = ds.GetSoftwareTitleNameFromExecutionID(ctx, req2)
require.NoError(t, err)
require.Equal(t, "barfoo", title)
_, _, err = ds.GetDetailsForUninstallFromExecutionID(ctx, req1)
require.ErrorIs(t, err, sql.ErrNoRows)
// create an uninstall request for installer1
req3 := uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, req3, host.ID, installer1)
err = ds.InsertSoftwareUninstallRequest(ctx, req3, host.ID, installer1, true)
require.NoError(t, err)
title, err = ds.GetSoftwareTitleNameFromExecutionID(ctx, req3)
title, selfService, err = ds.GetDetailsForUninstallFromExecutionID(ctx, req3)
require.NoError(t, err)
require.Equal(t, "foobar", title)
require.True(t, selfService)
// record a result for req2, will activate req3 so it is now in host_software_installs too
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
@@ -2667,9 +2659,10 @@ func testGetSoftwareTitleNameFromExecutionID(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
title, err = ds.GetSoftwareTitleNameFromExecutionID(ctx, req3)
title, selfService, err = ds.GetDetailsForUninstallFromExecutionID(ctx, req3)
require.NoError(t, err)
require.Equal(t, "foobar", title)
require.True(t, selfService)
}
func testGetTeamsWithInstallerByHash(t *testing.T, ds *Datastore) {
+4 -4
View File
@@ -3919,14 +3919,14 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
return err
}
hostSwi6UninstallUUID = uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi6UninstallUUID, host.ID, swi6PendingUninstall)
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi6UninstallUUID, host.ID, swi6PendingUninstall, false)
if err != nil {
return err
}
// swi7 is failed uninstall
hostSwi7UninstallUUID = uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi7UninstallUUID, host.ID, swi7FailedUninstall)
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi7UninstallUUID, host.ID, swi7FailedUninstall, true)
if err != nil {
return err
}
@@ -3948,7 +3948,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
// swi8 is successful uninstall
hostSwi8UninstallUUID = uuid.NewString()
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi8UninstallUUID, host.ID, swi8Uninstalled)
err = ds.InsertSoftwareUninstallRequest(ctx, hostSwi8UninstallUUID, host.ID, swi8Uninstalled, true)
if err != nil {
return err
}
@@ -6815,7 +6815,7 @@ func testListHostSoftwareVulnerabileAndVPP(t *testing.T, ds *Datastore) {
)
require.NoError(t, err)
// pending install request
err = ds.InsertSoftwareUninstallRequest(ctx, "abc123", tmHost.ID, installerID)
err = ds.InsertSoftwareUninstallRequest(ctx, "abc123", tmHost.ID, installerID, true)
require.NoError(t, err)
require.NoError(t, ds.ReconcileSoftwareTitles(ctx))
// Ensure that software "a" & "b" are returned as they are the only vulnerable apps at this point