mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Cancel upcoming activities: DB schema and backend (#27710)
This commit is contained in:
parent
6063939d9d
commit
69fcda9686
14 changed files with 463 additions and 18 deletions
1
changes/27409-add-cancel-upcoming-activity-endpoint
Normal file
1
changes/27409-add-cancel-upcoming-activity-endpoint
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added the `DELETE /api/latest/fleet/hosts/:id/activities/upcoming/:activity_id` endpoint to cancel an upcoming activity for a host.
|
||||
|
|
@ -14,6 +14,7 @@ read := "read"
|
|||
list := "list"
|
||||
write := "write"
|
||||
write_host_label := "write_host_label"
|
||||
cancel_host_activity := "cancel_host_activity"
|
||||
|
||||
# User specific actions
|
||||
write_role := "write_role"
|
||||
|
|
@ -266,20 +267,27 @@ allow {
|
|||
allowed_read_roles(action, base_roles, extra_roles)[_] == subject.global_role
|
||||
}
|
||||
|
||||
# Global gitops, admin and mantainers can write hosts.
|
||||
# Global gitops, admin and maintainers can write hosts.
|
||||
allow {
|
||||
object.type == "host"
|
||||
subject.global_role == [admin, maintainer, gitops][_]
|
||||
action == write
|
||||
}
|
||||
|
||||
# Global admin, mantainers and gitops can write labels to hosts.
|
||||
# Global admin, maintainers and gitops can write labels to hosts.
|
||||
allow {
|
||||
object.type == "host"
|
||||
subject.global_role == [admin, maintainer, gitops][_]
|
||||
action == write_host_label
|
||||
}
|
||||
|
||||
# Global admin and maintainers can cancel activities on a host.
|
||||
allow {
|
||||
object.type == "host"
|
||||
subject.global_role == [admin, maintainer][_]
|
||||
action == cancel_host_activity
|
||||
}
|
||||
|
||||
# Allow read for global observer and observer_plus, selective_read for gitops.
|
||||
allow {
|
||||
object.type == "host"
|
||||
|
|
@ -310,6 +318,13 @@ allow {
|
|||
action == write_host_label
|
||||
}
|
||||
|
||||
# Team admins and maintainers can cancel activities on a host of their own team.
|
||||
allow {
|
||||
object.type == "host"
|
||||
team_role(subject, object.team_id) == [admin, maintainer][_]
|
||||
action == cancel_host_activity
|
||||
}
|
||||
|
||||
# Allow read for host health for global admin/maintainer, team admins, observer.
|
||||
allow {
|
||||
object.type == "host_health"
|
||||
|
|
|
|||
|
|
@ -14,15 +14,16 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
read = fleet.ActionRead
|
||||
list = fleet.ActionList
|
||||
write = fleet.ActionWrite
|
||||
writeRole = fleet.ActionWriteRole
|
||||
run = fleet.ActionRun
|
||||
runNew = fleet.ActionRunNew
|
||||
changePwd = fleet.ActionChangePassword
|
||||
selectiveRead = fleet.ActionSelectiveRead
|
||||
selectiveList = fleet.ActionSelectiveList
|
||||
read = fleet.ActionRead
|
||||
list = fleet.ActionList
|
||||
write = fleet.ActionWrite
|
||||
writeRole = fleet.ActionWriteRole
|
||||
run = fleet.ActionRun
|
||||
runNew = fleet.ActionRunNew
|
||||
changePwd = fleet.ActionChangePassword
|
||||
selectiveRead = fleet.ActionSelectiveRead
|
||||
selectiveList = fleet.ActionSelectiveList
|
||||
cancelHostActivity = fleet.ActionCancelHostActivity
|
||||
)
|
||||
|
||||
var auth *Authorizer
|
||||
|
|
@ -744,12 +745,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: nil, object: host, action: list, allow: false},
|
||||
{user: nil, object: host, action: selectiveList, allow: false},
|
||||
{user: nil, object: host, action: selectiveRead, allow: false},
|
||||
{user: nil, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: nil, object: hostTeam1, action: read, allow: false},
|
||||
{user: nil, object: hostTeam1, action: write, allow: false},
|
||||
{user: nil, object: hostTeam1, action: selectiveRead, allow: false},
|
||||
{user: nil, object: hostTeam1, action: cancelHostActivity, allow: false},
|
||||
{user: nil, object: hostTeam2, action: read, allow: false},
|
||||
{user: nil, object: hostTeam2, action: write, allow: false},
|
||||
{user: nil, object: hostTeam2, action: selectiveRead, allow: false},
|
||||
{user: nil, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// No host access if the user has no roles.
|
||||
{user: test.UserNoRoles, object: host, action: read, allow: false},
|
||||
|
|
@ -757,12 +761,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: test.UserNoRoles, object: host, action: list, allow: false},
|
||||
{user: test.UserNoRoles, object: host, action: selectiveList, allow: false},
|
||||
{user: test.UserNoRoles, object: host, action: selectiveRead, allow: false},
|
||||
{user: test.UserNoRoles, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: test.UserNoRoles, object: hostTeam1, action: read, allow: false},
|
||||
{user: test.UserNoRoles, object: hostTeam1, action: write, allow: false},
|
||||
{user: test.UserNoRoles, object: hostTeam1, action: selectiveRead, allow: false},
|
||||
{user: test.UserNoRoles, object: hostTeam1, action: cancelHostActivity, allow: false},
|
||||
{user: test.UserNoRoles, object: hostTeam2, action: read, allow: false},
|
||||
{user: test.UserNoRoles, object: hostTeam2, action: write, allow: false},
|
||||
{user: test.UserNoRoles, object: hostTeam2, action: selectiveRead, allow: false},
|
||||
{user: test.UserNoRoles, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// Global observer can read all
|
||||
{user: test.UserObserver, object: host, action: read, allow: true},
|
||||
|
|
@ -770,12 +777,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: test.UserObserver, object: host, action: list, allow: true},
|
||||
{user: test.UserObserver, object: host, action: selectiveList, allow: true},
|
||||
{user: test.UserObserver, object: host, action: selectiveRead, allow: true},
|
||||
{user: test.UserObserver, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: test.UserObserver, object: hostTeam1, action: read, allow: true},
|
||||
{user: test.UserObserver, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: test.UserObserver, object: hostTeam1, action: write, allow: false},
|
||||
{user: test.UserObserver, object: hostTeam1, action: cancelHostActivity, allow: false},
|
||||
{user: test.UserObserver, object: hostTeam2, action: read, allow: true},
|
||||
{user: test.UserObserver, object: hostTeam2, action: selectiveRead, allow: true},
|
||||
{user: test.UserObserver, object: hostTeam2, action: write, allow: false},
|
||||
{user: test.UserObserver, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// Global observer+ can read all
|
||||
{user: test.UserObserverPlus, object: host, action: read, allow: true},
|
||||
|
|
@ -783,12 +793,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: test.UserObserverPlus, object: host, action: list, allow: true},
|
||||
{user: test.UserObserverPlus, object: host, action: selectiveList, allow: true},
|
||||
{user: test.UserObserverPlus, object: host, action: selectiveRead, allow: true},
|
||||
{user: test.UserObserverPlus, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: test.UserObserverPlus, object: hostTeam1, action: read, allow: true},
|
||||
{user: test.UserObserverPlus, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: test.UserObserverPlus, object: hostTeam1, action: write, allow: false},
|
||||
{user: test.UserObserverPlus, object: hostTeam1, action: cancelHostActivity, allow: false},
|
||||
{user: test.UserObserverPlus, object: hostTeam2, action: read, allow: true},
|
||||
{user: test.UserObserverPlus, object: hostTeam2, action: selectiveRead, allow: true},
|
||||
{user: test.UserObserverPlus, object: hostTeam2, action: write, allow: false},
|
||||
{user: test.UserObserverPlus, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// Global admin can read/write all
|
||||
{user: test.UserAdmin, object: host, action: read, allow: true},
|
||||
|
|
@ -796,12 +809,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: test.UserAdmin, object: host, action: write, allow: true},
|
||||
{user: test.UserAdmin, object: host, action: list, allow: true},
|
||||
{user: test.UserAdmin, object: host, action: selectiveList, allow: true},
|
||||
{user: test.UserAdmin, object: host, action: cancelHostActivity, allow: true},
|
||||
{user: test.UserAdmin, object: hostTeam1, action: read, allow: true},
|
||||
{user: test.UserAdmin, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: test.UserAdmin, object: hostTeam1, action: write, allow: true},
|
||||
{user: test.UserAdmin, object: hostTeam1, action: cancelHostActivity, allow: true},
|
||||
{user: test.UserAdmin, object: hostTeam2, action: read, allow: true},
|
||||
{user: test.UserAdmin, object: hostTeam2, action: selectiveRead, allow: true},
|
||||
{user: test.UserAdmin, object: hostTeam2, action: write, allow: true},
|
||||
{user: test.UserAdmin, object: hostTeam2, action: cancelHostActivity, allow: true},
|
||||
|
||||
// Global maintainer can read/write all
|
||||
{user: test.UserMaintainer, object: host, action: read, allow: true},
|
||||
|
|
@ -809,12 +825,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: test.UserMaintainer, object: host, action: write, allow: true},
|
||||
{user: test.UserMaintainer, object: host, action: list, allow: true},
|
||||
{user: test.UserMaintainer, object: host, action: selectiveList, allow: true},
|
||||
{user: test.UserMaintainer, object: host, action: cancelHostActivity, allow: true},
|
||||
{user: test.UserMaintainer, object: hostTeam1, action: read, allow: true},
|
||||
{user: test.UserMaintainer, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: test.UserMaintainer, object: hostTeam1, action: write, allow: true},
|
||||
{user: test.UserMaintainer, object: hostTeam1, action: cancelHostActivity, allow: true},
|
||||
{user: test.UserMaintainer, object: hostTeam2, action: read, allow: true},
|
||||
{user: test.UserMaintainer, object: hostTeam2, action: selectiveRead, allow: true},
|
||||
{user: test.UserMaintainer, object: hostTeam2, action: write, allow: true},
|
||||
{user: test.UserMaintainer, object: hostTeam2, action: cancelHostActivity, allow: true},
|
||||
|
||||
// Global GitOps can write and selectively read all.
|
||||
{user: test.UserGitOps, object: host, action: read, allow: false},
|
||||
|
|
@ -822,12 +841,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: test.UserGitOps, object: host, action: selectiveRead, allow: true},
|
||||
{user: test.UserGitOps, object: host, action: list, allow: false},
|
||||
{user: test.UserGitOps, object: host, action: selectiveList, allow: true},
|
||||
{user: test.UserGitOps, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: test.UserGitOps, object: hostTeam1, action: read, allow: false},
|
||||
{user: test.UserGitOps, object: hostTeam1, action: write, allow: true},
|
||||
{user: test.UserGitOps, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: test.UserGitOps, object: hostTeam1, action: cancelHostActivity, allow: false},
|
||||
{user: test.UserGitOps, object: hostTeam2, action: read, allow: false},
|
||||
{user: test.UserGitOps, object: hostTeam2, action: write, allow: true},
|
||||
{user: test.UserGitOps, object: hostTeam2, action: selectiveRead, allow: true},
|
||||
{user: test.UserGitOps, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// Team observer can read only on appropriate team
|
||||
{user: teamObserver, object: host, action: read, allow: false},
|
||||
|
|
@ -835,12 +857,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: teamObserver, object: host, action: write, allow: false},
|
||||
{user: teamObserver, object: host, action: list, allow: true},
|
||||
{user: teamObserver, object: host, action: selectiveList, allow: true},
|
||||
{user: teamObserver, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: teamObserver, object: hostTeam1, action: read, allow: true},
|
||||
{user: teamObserver, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: teamObserver, object: hostTeam1, action: write, allow: false},
|
||||
{user: teamObserver, object: hostTeam1, action: cancelHostActivity, allow: false},
|
||||
{user: teamObserver, object: hostTeam2, action: read, allow: false},
|
||||
{user: teamObserver, object: hostTeam2, action: selectiveRead, allow: false},
|
||||
{user: teamObserver, object: hostTeam2, action: write, allow: false},
|
||||
{user: teamObserver, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// Team observer+ can read only on appropriate team
|
||||
{user: teamObserverPlus, object: host, action: read, allow: false},
|
||||
|
|
@ -848,12 +873,15 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: teamObserverPlus, object: host, action: write, allow: false},
|
||||
{user: teamObserverPlus, object: host, action: list, allow: true},
|
||||
{user: teamObserverPlus, object: host, action: selectiveList, allow: true},
|
||||
{user: teamObserverPlus, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: teamObserverPlus, object: hostTeam1, action: read, allow: true},
|
||||
{user: teamObserverPlus, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: teamObserverPlus, object: hostTeam1, action: write, allow: false},
|
||||
{user: teamObserverPlus, object: hostTeam1, action: cancelHostActivity, allow: false},
|
||||
{user: teamObserverPlus, object: hostTeam2, action: read, allow: false},
|
||||
{user: teamObserverPlus, object: hostTeam2, action: selectiveRead, allow: false},
|
||||
{user: teamObserverPlus, object: hostTeam2, action: write, allow: false},
|
||||
{user: teamObserverPlus, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// Team maintainer can read/write only on appropriate team
|
||||
{user: teamMaintainer, object: host, action: read, allow: false},
|
||||
|
|
@ -861,11 +889,14 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: teamMaintainer, object: host, action: write, allow: false},
|
||||
{user: teamMaintainer, object: host, action: list, allow: true},
|
||||
{user: teamMaintainer, object: host, action: selectiveList, allow: true},
|
||||
{user: teamMaintainer, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: teamMaintainer, object: hostTeam1, action: read, allow: true},
|
||||
{user: teamMaintainer, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: teamMaintainer, object: hostTeam1, action: write, allow: true},
|
||||
{user: teamMaintainer, object: hostTeam1, action: cancelHostActivity, allow: true},
|
||||
{user: teamMaintainer, object: hostTeam2, action: read, allow: false},
|
||||
{user: teamMaintainer, object: hostTeam2, action: write, allow: false},
|
||||
{user: teamMaintainer, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// Team admin can read/write only on appropriate team
|
||||
{user: teamAdmin, object: host, action: read, allow: false},
|
||||
|
|
@ -873,23 +904,29 @@ func TestAuthorizeHost(t *testing.T) {
|
|||
{user: teamAdmin, object: host, action: write, allow: false},
|
||||
{user: teamAdmin, object: host, action: list, allow: true},
|
||||
{user: teamAdmin, object: host, action: selectiveList, allow: true},
|
||||
{user: teamAdmin, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: teamAdmin, object: hostTeam1, action: read, allow: true},
|
||||
{user: teamAdmin, object: hostTeam1, action: write, allow: true},
|
||||
{user: teamAdmin, object: hostTeam1, action: cancelHostActivity, allow: true},
|
||||
{user: teamAdmin, object: hostTeam2, action: read, allow: false},
|
||||
{user: teamAdmin, object: hostTeam2, action: write, allow: false},
|
||||
{user: teamAdmin, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
|
||||
// Team GitOps can cannot read hosts, but it can write and selectively read them.
|
||||
{user: teamGitOps, object: host, action: read, allow: false},
|
||||
{user: teamGitOps, object: host, action: write, allow: false},
|
||||
{user: teamGitOps, object: host, action: selectiveRead, allow: false},
|
||||
{user: teamGitOps, object: host, action: cancelHostActivity, allow: false},
|
||||
{user: teamGitOps, object: hostTeam1, action: read, allow: false},
|
||||
{user: teamGitOps, object: hostTeam1, action: list, allow: false},
|
||||
{user: teamGitOps, object: hostTeam1, action: selectiveList, allow: true},
|
||||
{user: teamGitOps, object: hostTeam1, action: selectiveRead, allow: true},
|
||||
{user: teamGitOps, object: hostTeam1, action: write, allow: false},
|
||||
{user: teamGitOps, object: hostTeam1, action: cancelHostActivity, allow: false},
|
||||
{user: teamGitOps, object: hostTeam2, action: read, allow: false},
|
||||
{user: teamGitOps, object: hostTeam2, action: write, allow: false},
|
||||
{user: teamGitOps, object: hostTeam2, action: selectiveRead, allow: false},
|
||||
{user: teamGitOps, object: hostTeam2, action: cancelHostActivity, allow: false},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -667,6 +667,10 @@ func (ds *Datastore) CleanupActivitiesAndAssociatedData(ctx context.Context, max
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// This function activates the next upcoming activity, if any, for the specified host.
|
||||
// It does a few things to achieve this:
|
||||
// - If there was an activity already marked as activated (activated_at is
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20250331154206, Down_20250331154206)
|
||||
}
|
||||
|
||||
func Up_20250331154206(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
ALTER TABLE host_script_results
|
||||
ADD COLUMN canceled TINYINT(1) NOT NULL DEFAULT '0'
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to alter host_script_results: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
ALTER TABLE host_vpp_software_installs
|
||||
ADD COLUMN canceled TINYINT(1) NOT NULL DEFAULT '0'
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to alter host_vpp_software_installs: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE host_software_installs
|
||||
ADD COLUMN canceled TINYINT(1) NOT NULL DEFAULT '0',
|
||||
CHANGE COLUMN execution_status execution_status
|
||||
ENUM('pending_install', 'failed_install', 'installed', 'pending_uninstall', 'failed_uninstall', 'canceled_install', 'canceled_uninstall')
|
||||
GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN canceled = 1 AND uninstall = 0 THEN 'canceled_install'
|
||||
|
||||
WHEN canceled = 1 AND uninstall = 1 THEN 'canceled_uninstall'
|
||||
|
||||
WHEN post_install_script_exit_code IS NOT NULL AND
|
||||
post_install_script_exit_code = 0 THEN 'installed'
|
||||
|
||||
WHEN post_install_script_exit_code IS NOT NULL AND
|
||||
post_install_script_exit_code != 0 THEN 'failed_install'
|
||||
|
||||
WHEN install_script_exit_code IS NOT NULL AND
|
||||
install_script_exit_code = 0 THEN 'installed'
|
||||
|
||||
WHEN install_script_exit_code IS NOT NULL AND
|
||||
install_script_exit_code != 0 THEN 'failed_install'
|
||||
|
||||
WHEN pre_install_query_output IS NOT NULL AND
|
||||
pre_install_query_output = '' THEN 'failed_install'
|
||||
|
||||
WHEN host_id IS NOT NULL AND uninstall = 0 THEN 'pending_install'
|
||||
|
||||
WHEN uninstall_script_exit_code IS NOT NULL AND
|
||||
uninstall_script_exit_code != 0 THEN 'failed_uninstall'
|
||||
|
||||
WHEN uninstall_script_exit_code IS NOT NULL AND
|
||||
uninstall_script_exit_code = 0 THEN NULL -- available for install again
|
||||
|
||||
WHEN host_id IS NOT NULL AND uninstall = 1 THEN 'pending_uninstall'
|
||||
|
||||
ELSE NULL -- not installed from Fleet installer or successfully uninstalled
|
||||
END
|
||||
) VIRTUAL NULL,
|
||||
|
||||
CHANGE COLUMN status status
|
||||
ENUM('pending_install', 'failed_install', 'installed', 'pending_uninstall', 'failed_uninstall', 'canceled_install', 'canceled_uninstall')
|
||||
GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN removed = 1 THEN NULL
|
||||
|
||||
WHEN canceled = 1 AND uninstall = 0 THEN 'canceled_install'
|
||||
|
||||
WHEN canceled = 1 AND uninstall = 1 THEN 'canceled_uninstall'
|
||||
|
||||
WHEN post_install_script_exit_code IS NOT NULL AND
|
||||
post_install_script_exit_code = 0 THEN 'installed'
|
||||
|
||||
WHEN post_install_script_exit_code IS NOT NULL AND
|
||||
post_install_script_exit_code != 0 THEN 'failed_install'
|
||||
|
||||
WHEN install_script_exit_code IS NOT NULL AND
|
||||
install_script_exit_code = 0 THEN 'installed'
|
||||
|
||||
WHEN install_script_exit_code IS NOT NULL AND
|
||||
install_script_exit_code != 0 THEN 'failed_install'
|
||||
|
||||
WHEN pre_install_query_output IS NOT NULL AND
|
||||
pre_install_query_output = '' THEN 'failed_install'
|
||||
|
||||
WHEN host_id IS NOT NULL AND uninstall = 0 THEN 'pending_install'
|
||||
|
||||
WHEN uninstall_script_exit_code IS NOT NULL AND
|
||||
uninstall_script_exit_code != 0 THEN 'failed_uninstall'
|
||||
|
||||
WHEN uninstall_script_exit_code IS NOT NULL AND
|
||||
uninstall_script_exit_code = 0 THEN NULL -- available for install again
|
||||
|
||||
WHEN host_id IS NOT NULL AND uninstall = 1 THEN 'pending_uninstall'
|
||||
|
||||
ELSE NULL -- not installed from Fleet installer or successfully uninstalled
|
||||
END
|
||||
) STORED NULL
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add canceled column and update statuses generated columns on host_software_installs: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20250331154206(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20250331154206(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// this is basically the same test as 20241021224359_AddExecutionStatusToHostSoftwareInstalls_test.go
|
||||
// to ensure that the same state still corresponds to the same resulting statuses, after migration
|
||||
// and change of the status and execution_status columns.
|
||||
hostID := insertHost(t, db, nil)
|
||||
dataStmts := `
|
||||
INSERT INTO script_contents (id, md5_checksum, contents) VALUES
|
||||
(1, 'checksum', 'script content');
|
||||
|
||||
INSERT INTO software_titles (id, name, source, browser) VALUES (1, 'Foo.app', 'apps', '');
|
||||
|
||||
INSERT INTO software_installers
|
||||
(id, title_id, filename, version, platform, install_script_content_id, storage_id, package_ids, uninstall_script_content_id)
|
||||
VALUES
|
||||
(1, 1, 'foo-installer.pkg', '1.1', 'darwin', 1, 'storage-id', '', 1);
|
||||
`
|
||||
_, err := db.Exec(dataStmts)
|
||||
require.NoError(t, err)
|
||||
|
||||
hsiStmt := `
|
||||
INSERT INTO host_software_installs (
|
||||
host_id,
|
||||
execution_id,
|
||||
software_installer_id,
|
||||
install_script_exit_code,
|
||||
uninstall_script_exit_code,
|
||||
updated_at,
|
||||
uninstall,
|
||||
removed
|
||||
) VALUES (?, ?, ?, ?, ?, '2024-10-01 00:00:00', ?, 1)`
|
||||
hsiInstall := execNoErrLastID(t, db, hsiStmt, hostID, "execution-id1", 1, 0, nil, 0)
|
||||
hsiUninstall := execNoErrLastID(t, db, hsiStmt, hostID, "execution-id2", 1, nil, 0, 1)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
var statuses struct {
|
||||
Status *string `db:"status"`
|
||||
ExecutionStatus *string `db:"execution_status"`
|
||||
}
|
||||
|
||||
err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiInstall)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, statuses.ExecutionStatus)
|
||||
require.Equal(t, "installed", *statuses.ExecutionStatus)
|
||||
require.Nil(t, statuses.Status)
|
||||
|
||||
err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiUninstall)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, statuses.ExecutionStatus) // uninstalls have null status
|
||||
require.Nil(t, statuses.Status)
|
||||
|
||||
execNoErr(t, db, `UPDATE host_software_installs SET removed = 0`)
|
||||
|
||||
err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiInstall)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, statuses.ExecutionStatus)
|
||||
require.Equal(t, "installed", *statuses.ExecutionStatus)
|
||||
require.NotNil(t, statuses.Status)
|
||||
require.Equal(t, "installed", *statuses.Status)
|
||||
|
||||
err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiUninstall)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, statuses.ExecutionStatus) // uninstalls have null status
|
||||
require.Nil(t, statuses.Status)
|
||||
|
||||
execNoErr(t, db, `UPDATE host_software_installs SET canceled = 1`)
|
||||
|
||||
err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiInstall)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, statuses.ExecutionStatus)
|
||||
require.Equal(t, "canceled_install", *statuses.ExecutionStatus)
|
||||
require.NotNil(t, statuses.Status)
|
||||
require.Equal(t, "canceled_install", *statuses.Status)
|
||||
|
||||
err = db.Get(&statuses, "SELECT status, execution_status FROM host_software_installs WHERE id = ?", hsiUninstall)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, statuses.ExecutionStatus)
|
||||
require.Equal(t, "canceled_uninstall", *statuses.ExecutionStatus)
|
||||
require.NotNil(t, statuses.Status)
|
||||
require.Equal(t, "canceled_uninstall", *statuses.Status)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,8 @@ const (
|
|||
ActionWrite = "write"
|
||||
// ActionWriteHostLabel refers to writing labels on hosts.
|
||||
ActionWriteHostLabel = "write_host_label"
|
||||
// ActionCancelHostActivity refers to canceling an upcoming activity on a host.
|
||||
ActionCancelHostActivity = "cancel_host_activity"
|
||||
|
||||
//
|
||||
// User specific actions
|
||||
|
|
|
|||
|
|
@ -679,6 +679,7 @@ type Datastore interface {
|
|||
ListActivities(ctx context.Context, opt ListActivitiesOptions) ([]*Activity, *PaginationMetadata, error)
|
||||
MarkActivitiesAsStreamed(ctx context.Context, activityIDs []uint) error
|
||||
ListHostUpcomingActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*UpcomingActivity, *PaginationMetadata, error)
|
||||
CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error
|
||||
ListHostPastActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*Activity, *PaginationMetadata, error)
|
||||
IsExecutionPendingForHost(ctx context.Context, hostID uint, scriptID uint) (bool, error)
|
||||
|
||||
|
|
|
|||
|
|
@ -602,6 +602,11 @@ type Service interface {
|
|||
// ListHostPastActivities lists the activities that have already happened for the specified host.
|
||||
ListHostPastActivities(ctx context.Context, hostID uint, opt ListOptions) ([]*Activity, *PaginationMetadata, error)
|
||||
|
||||
// CancelHostUpcomingActivity cancels an upcoming activity for the specified
|
||||
// host. If the activity does not exist in the queue of upcoming activities
|
||||
// (e.g. it did complete), it returns a not found error.
|
||||
CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// UserRolesService
|
||||
|
||||
|
|
|
|||
|
|
@ -498,6 +498,8 @@ type MarkActivitiesAsStreamedFunc func(ctx context.Context, activityIDs []uint)
|
|||
|
||||
type ListHostUpcomingActivitiesFunc func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.UpcomingActivity, *fleet.PaginationMetadata, error)
|
||||
|
||||
type CancelHostUpcomingActivityFunc func(ctx context.Context, hostID uint, upcomingActivityID string) error
|
||||
|
||||
type ListHostPastActivitiesFunc func(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error)
|
||||
|
||||
type IsExecutionPendingForHostFunc func(ctx context.Context, hostID uint, scriptID uint) (bool, error)
|
||||
|
|
@ -714,7 +716,7 @@ type ReplaceHostBatteriesFunc func(ctx context.Context, id uint, mappings []*fle
|
|||
|
||||
type VerifyEnrollSecretFunc func(ctx context.Context, secret string) (*fleet.EnrollSecret, error)
|
||||
|
||||
type IsEnrollSecretAvailableFunc func(ctx context.Context, secret string, new bool, teamID *uint) (bool, error)
|
||||
type IsEnrollSecretAvailableFunc func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error)
|
||||
|
||||
type EnrollHostFunc func(ctx context.Context, isMDMEnabled bool, osqueryHostId string, hardwareUUID string, hardwareSerial string, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error)
|
||||
|
||||
|
|
@ -2011,6 +2013,9 @@ type DataStore struct {
|
|||
ListHostUpcomingActivitiesFunc ListHostUpcomingActivitiesFunc
|
||||
ListHostUpcomingActivitiesFuncInvoked bool
|
||||
|
||||
CancelHostUpcomingActivityFunc CancelHostUpcomingActivityFunc
|
||||
CancelHostUpcomingActivityFuncInvoked bool
|
||||
|
||||
ListHostPastActivitiesFunc ListHostPastActivitiesFunc
|
||||
ListHostPastActivitiesFuncInvoked bool
|
||||
|
||||
|
|
@ -4877,6 +4882,13 @@ func (s *DataStore) ListHostUpcomingActivities(ctx context.Context, hostID uint,
|
|||
return s.ListHostUpcomingActivitiesFunc(ctx, hostID, opt)
|
||||
}
|
||||
|
||||
func (s *DataStore) CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error {
|
||||
s.mu.Lock()
|
||||
s.CancelHostUpcomingActivityFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.CancelHostUpcomingActivityFunc(ctx, hostID, upcomingActivityID)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListHostPastActivities(ctx context.Context, hostID uint, opt fleet.ListOptions) ([]*fleet.Activity, *fleet.PaginationMetadata, error) {
|
||||
s.mu.Lock()
|
||||
s.ListHostPastActivitiesFuncInvoked = true
|
||||
|
|
@ -5633,11 +5645,11 @@ func (s *DataStore) VerifyEnrollSecret(ctx context.Context, secret string) (*fle
|
|||
return s.VerifyEnrollSecretFunc(ctx, secret)
|
||||
}
|
||||
|
||||
func (s *DataStore) IsEnrollSecretAvailable(ctx context.Context, secret string, new bool, teamID *uint) (bool, error) {
|
||||
func (s *DataStore) IsEnrollSecretAvailable(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
|
||||
s.mu.Lock()
|
||||
s.IsEnrollSecretAvailableFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.IsEnrollSecretAvailableFunc(ctx, secret, new, teamID)
|
||||
return s.IsEnrollSecretAvailableFunc(ctx, secret, isNew, teamID)
|
||||
}
|
||||
|
||||
func (s *DataStore) EnrollHost(ctx context.Context, isMDMEnabled bool, osqueryHostId string, hardwareUUID string, hardwareSerial string, nodeKey string, teamID *uint, cooldown time.Duration) (*fleet.Host, error) {
|
||||
|
|
|
|||
|
|
@ -244,3 +244,45 @@ func (svc *Service) ListHostPastActivities(ctx context.Context, hostID uint, opt
|
|||
|
||||
return svc.ds.ListHostPastActivities(ctx, hostID, opt)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Cancel host upcoming activity
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type cancelHostUpcomingActivityRequest struct {
|
||||
HostID uint `url:"id"`
|
||||
ActivityID string `url:"activity_id"`
|
||||
}
|
||||
|
||||
type cancelHostUpcomingActivityResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r cancelHostUpcomingActivityResponse) Error() error { return r.Err }
|
||||
func (r cancelHostUpcomingActivityResponse) Status() int { return http.StatusNoContent }
|
||||
|
||||
func cancelHostUpcomingActivityEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*cancelHostUpcomingActivityRequest)
|
||||
err := svc.CancelHostUpcomingActivity(ctx, req.HostID, req.ActivityID)
|
||||
if err != nil {
|
||||
return cancelHostUpcomingActivityResponse{Err: err}, nil
|
||||
}
|
||||
return cancelHostUpcomingActivityResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) CancelHostUpcomingActivity(ctx context.Context, hostID uint, upcomingActivityID string) error {
|
||||
// First ensure the user has access to list hosts, then check the specific
|
||||
// host once team_id is loaded.
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||||
return err
|
||||
}
|
||||
host, err := svc.ds.HostLite(ctx, hostID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host")
|
||||
}
|
||||
// Authorize again with team loaded now that we have team_id
|
||||
if err := svc.authz.Authorize(ctx, host, fleet.ActionCancelHostActivity); err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.ds.CancelHostUpcomingActivity(ctx, hostID, upcomingActivityID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
|
|
@ -364,3 +365,117 @@ func TestActivityWebhooksDisabled(t *testing.T) {
|
|||
require.True(t, ds.NewActivityFuncInvoked)
|
||||
assert.Equal(t, user, activityUser)
|
||||
}
|
||||
|
||||
func TestCancelHostUpcomingActivityAuth(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}})
|
||||
|
||||
const (
|
||||
teamHostID = 1
|
||||
globalHostID = 2
|
||||
)
|
||||
|
||||
teamHost := &fleet.Host{TeamID: ptr.Uint(1), Platform: "darwin"}
|
||||
globalHost := &fleet.Host{Platform: "darwin"}
|
||||
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
if hostID == teamHostID {
|
||||
return teamHost, nil
|
||||
}
|
||||
return globalHost, nil
|
||||
}
|
||||
ds.CancelHostUpcomingActivityFunc = func(ctx context.Context, hostID uint, actID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
user *fleet.User
|
||||
shouldFailGlobal bool
|
||||
shouldFailTeam bool
|
||||
}{
|
||||
{
|
||||
name: "global observer",
|
||||
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: true,
|
||||
},
|
||||
{
|
||||
name: "team observer",
|
||||
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: true,
|
||||
},
|
||||
{
|
||||
name: "global observer plus",
|
||||
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleObserverPlus)},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: true,
|
||||
},
|
||||
{
|
||||
name: "team observer plus",
|
||||
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserverPlus}}},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: true,
|
||||
},
|
||||
{
|
||||
name: "global admin",
|
||||
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
||||
shouldFailGlobal: false,
|
||||
shouldFailTeam: false,
|
||||
},
|
||||
{
|
||||
name: "team admin",
|
||||
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: false,
|
||||
},
|
||||
{
|
||||
name: "global maintainer",
|
||||
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
||||
shouldFailGlobal: false,
|
||||
shouldFailTeam: false,
|
||||
},
|
||||
{
|
||||
name: "team maintainer",
|
||||
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: false,
|
||||
},
|
||||
{
|
||||
name: "team admin wrong team",
|
||||
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 42}, Role: fleet.RoleAdmin}}},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: true,
|
||||
},
|
||||
{
|
||||
name: "team maintainer wrong team",
|
||||
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 42}, Role: fleet.RoleMaintainer}}},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: true,
|
||||
},
|
||||
{
|
||||
name: "global gitops",
|
||||
user: &fleet.User{GlobalRole: ptr.String(fleet.RoleGitOps)},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: true,
|
||||
},
|
||||
{
|
||||
name: "team gitops",
|
||||
user: &fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleGitOps}}},
|
||||
shouldFailGlobal: true,
|
||||
shouldFailTeam: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
||||
|
||||
err := svc.CancelHostUpcomingActivity(ctx, globalHostID, "abc")
|
||||
checkAuthErr(t, tt.shouldFailGlobal, err)
|
||||
err = svc.CancelHostUpcomingActivity(ctx, teamHostID, "abc")
|
||||
checkAuthErr(t, tt.shouldFailTeam, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -480,6 +480,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/scripts", getHostScriptDetailsEndpoint, getHostScriptDetailsRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/activities/upcoming", listHostUpcomingActivitiesEndpoint, listHostUpcomingActivitiesRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/activities", listHostPastActivitiesEndpoint, listHostPastActivitiesRequest{})
|
||||
ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/activities/upcoming/{activity_id}", cancelHostUpcomingActivityEndpoint, cancelHostUpcomingActivityRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/lock", lockHostEndpoint, lockHostRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/unlock", unlockHostEndpoint, unlockHostRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/wipe", wipeHostEndpoint, wipeHostRequest{})
|
||||
|
|
|
|||
Loading…
Reference in a new issue