diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx
index f04e855af7..c7ca027402 100644
--- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx
+++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tests.tsx
@@ -1203,4 +1203,34 @@ describe("Activity Feed", () => {
render();
expect(screen.getByText("An end user")).toBeInTheDocument();
});
+
+ it("renders the correct actor for a installed_app_store_app activity without self_service", () => {
+ const activity = createMockActivity({
+ type: ActivityType.InstalledAppStoreApp,
+ actor_id: 1,
+ actor_full_name: "Test Admin",
+ details: {
+ software_title: "Foo Software",
+ host_display_name: "Foo Host",
+ },
+ });
+
+ render();
+ expect(screen.getByText("Test Admin")).toBeInTheDocument();
+ });
+
+ it("renders the correct actor for a installed_app_store_app activity that was self_service", () => {
+ const activity = createMockActivity({
+ type: ActivityType.InstalledAppStoreApp,
+ actor_id: 1,
+ details: {
+ software_title: "Foo Software",
+ self_service: true,
+ host_display_name: "Foo Host",
+ },
+ });
+
+ render();
+ expect(screen.getByText("An end user")).toBeInTheDocument();
+ });
});
diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
index c0f81ef72d..1e5e4a40a1 100644
--- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
+++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
@@ -1177,6 +1177,7 @@ const ActivityItem = ({
DEFAULT_ACTOR_DISPLAY
);
case ActivityType.InstalledSoftware:
+ case ActivityType.InstalledAppStoreApp:
return activity.details?.self_service ? (
An end user
) : (
diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go
index d0e6dee037..d4cf76f3cc 100644
--- a/server/datastore/mysql/activities.go
+++ b/server/datastore/mysql/activities.go
@@ -356,6 +356,7 @@ SELECT
'software_title', st.name,
'app_store_id', hvsi.adam_id,
'command_uuid', hvsi.command_uuid,
+ 'self_service', hvsi.self_service IS TRUE,
-- status is always pending because only pending MDM commands are upcoming.
'status', :software_status_pending
) AS details
diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go
index e91a825227..be87524c05 100644
--- a/server/datastore/mysql/activities_test.go
+++ b/server/datastore/mysql/activities_test.go
@@ -373,8 +373,11 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
// create three hosts
h1 := test.NewHost(t, ds, "h1.local", "10.10.10.1", "1", "1", time.Now())
+ nanoEnrollAndSetHostMDMData(t, ds, h1, false)
h2 := test.NewHost(t, ds, "h2.local", "10.10.10.2", "2", "2", time.Now())
+ nanoEnrollAndSetHostMDMData(t, ds, h2, false)
h3 := test.NewHost(t, ds, "h3.local", "10.10.10.3", "3", "3", time.Now())
+ nanoEnrollAndSetHostMDMData(t, ds, h3, false)
// create a couple of named scripts
scr1, err := ds.NewScript(ctx, &fleet.Script{
@@ -415,6 +418,35 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
sw2Meta, err := ds.GetSoftwareInstallerMetadataByID(ctx, sw2)
require.NoError(t, err)
+ // insert a VPP app
+ vppCommand1, vppCommand2 := "vpp-command-1", "vpp-command-2"
+ vppApp := &fleet.VPPApp{
+ Name: "vpp_no_team_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "3", Platform: fleet.MacOSPlatform}},
+ BundleIdentifier: "b3",
+ }
+ _, err = ds.InsertVPPAppWithTeam(ctx, vppApp, nil)
+ require.NoError(t, err)
+
+ // install the VPP app on h1
+ commander, _ := createMDMAppleCommanderAndStorage(t, ds)
+ err = ds.InsertHostVPPSoftwareInstall(ctx, h1.ID, vppApp.VPPAppID, vppCommand1, "event-id-1", false)
+ require.NoError(t, err)
+ err = commander.EnqueueCommand(
+ ctx,
+ []string{h1.UUID},
+ createRawAppleCmd("InstallApplication", vppCommand1),
+ )
+ require.NoError(t, err)
+ // install the VPP app on h2, self-service
+ err = ds.InsertHostVPPSoftwareInstall(noUserCtx, h2.ID, vppApp.VPPAppID, vppCommand2, "event-id-2", true)
+ require.NoError(t, err)
+ err = commander.EnqueueCommand(
+ ctx,
+ []string{h1.UUID},
+ createRawAppleCmd("InstallApplication", vppCommand2),
+ )
+ require.NoError(t, err)
+
// create a sync script request for h1 that has been pending for > MaxWaitTime, will not show up
hsr, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h1.ID, ScriptContents: "sync", UserID: &u.ID, SyncRequest: true})
require.NoError(t, err)
@@ -484,19 +516,22 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h1C, h1D, h1E)
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h1FooInstalled, h1Foo)
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h2Bar)
- SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h2A, h2F)
+ endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h2A, h2F)
+ SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_vpp_software_installs", "command_uuid", vppCommand1, vppCommand2)
execIDsWithUser := map[string]bool{
- h1A: true,
- h1B: true,
- h1C: true,
- h1D: false,
- h1E: false,
- h2A: true,
- h2F: true,
- h1Foo: false,
- h1Bar: true,
- h2Bar: true,
+ h1A: true,
+ h1B: true,
+ h1C: true,
+ h1D: false,
+ h1E: false,
+ h2A: true,
+ h2F: true,
+ h1Foo: false,
+ h1Bar: true,
+ h2Bar: true,
+ vppCommand1: true,
+ vppCommand2: false,
}
execIDsScriptName := map[string]string{
h1A: scr1.Name,
@@ -519,49 +554,49 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
opts: fleet.ListOptions{PerPage: 2},
hostID: h1.ID,
wantExecs: []string{h1A, h1B},
- wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 7},
+ wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 8},
},
{
opts: fleet.ListOptions{Page: 1, PerPage: 2},
hostID: h1.ID,
wantExecs: []string{h1Bar, h1C},
- wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 7},
+ wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 8},
},
{
opts: fleet.ListOptions{Page: 2, PerPage: 2},
hostID: h1.ID,
wantExecs: []string{h1D, h1E},
- wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 7},
+ wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 8},
},
{
opts: fleet.ListOptions{Page: 3, PerPage: 2},
hostID: h1.ID,
- wantExecs: []string{h1Foo},
- wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 7},
+ wantExecs: []string{h1Foo, vppCommand1},
+ wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 8},
},
{
opts: fleet.ListOptions{PerPage: 4},
hostID: h1.ID,
wantExecs: []string{h1A, h1B, h1Bar, h1C},
- wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 7},
+ wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 8},
},
{
opts: fleet.ListOptions{Page: 1, PerPage: 4},
hostID: h1.ID,
- wantExecs: []string{h1D, h1E, h1Foo},
- wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 7},
+ wantExecs: []string{h1D, h1E, h1Foo, vppCommand1},
+ wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 8},
},
{
opts: fleet.ListOptions{Page: 2, PerPage: 4},
hostID: h1.ID,
wantExecs: []string{},
- wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 7},
+ wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 8},
},
{
opts: fleet.ListOptions{PerPage: 3},
hostID: h2.ID,
- wantExecs: []string{h2Bar, h2A},
- wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 2},
+ wantExecs: []string{h2Bar, h2A, vppCommand2},
+ wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 3},
},
{
opts: fleet.ListOptions{},
@@ -604,6 +639,12 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
require.Equal(t, execIDsSoftwareTitle[wantExec], details["software_title"], "result %d", i)
wantUser = u2
+ case fleet.ActivityInstalledAppStoreApp{}.ActivityName():
+ require.Equal(t, wantExec, details["command_uuid"], "result %d", i)
+ require.Equal(t, "vpp_no_team_app_1", details["software_title"], "result %d", i)
+ require.Equal(t, !execIDsWithUser[wantExec], details["self_service"], "result %d", i)
+ wantUser = u2
+
default:
t.Fatalf("unknown activity type %s", a.Type)
}
diff --git a/server/datastore/mysql/testing_utils.go b/server/datastore/mysql/testing_utils.go
index 7e8b964e43..a8b7d12d20 100644
--- a/server/datastore/mysql/testing_utils.go
+++ b/server/datastore/mysql/testing_utils.go
@@ -683,7 +683,7 @@ func GetAggregatedStats(ctx context.Context, ds *Datastore, aggregate fleet.Aggr
func SetOrderedCreatedAtTimestamps(t testing.TB, ds *Datastore, afterTime time.Time, table, keyCol string, keys ...any) time.Time {
now := afterTime
for i := 0; i < len(keys); i++ {
- now = afterTime.Add(time.Second)
+ now = now.Add(time.Second)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(context.Background(),
fmt.Sprintf(`UPDATE %s SET created_at=? WHERE %s=?`, table, keyCol), now, keys[i])
diff --git a/server/datastore/mysql/vpp.go b/server/datastore/mysql/vpp.go
index b22d5a61cf..306e37b514 100644
--- a/server/datastore/mysql/vpp.go
+++ b/server/datastore/mysql/vpp.go
@@ -478,7 +478,7 @@ SELECT
st.name AS software_title,
hvsi.adam_id AS app_store_id,
hvsi.command_uuid AS command_uuid,
- hvsi.self_service AS self_service
+ hvsi.self_service AS self_service
FROM
host_vpp_software_installs hvsi
LEFT OUTER JOIN users u ON hvsi.user_id = u.id
@@ -490,15 +490,15 @@ WHERE
`
type result struct {
- HostID uint `db:"host_id"`
- HostDisplayName string `db:"host_display_name"`
- SoftwareTitle string `db:"software_title"`
- AppStoreID string `db:"app_store_id"`
- CommandUUID string `db:"command_uuid"`
- UserName string `db:"user_name"`
- UserID uint `db:"user_id"`
- UserEmail string `db:"user_email"`
- SelfService bool `db:"self_service"`
+ HostID uint `db:"host_id"`
+ HostDisplayName string `db:"host_display_name"`
+ SoftwareTitle string `db:"software_title"`
+ AppStoreID string `db:"app_store_id"`
+ CommandUUID string `db:"command_uuid"`
+ UserName *string `db:"user_name"`
+ UserID *uint `db:"user_id"`
+ UserEmail *string `db:"user_email"`
+ SelfService bool `db:"self_service"`
}
listStmt, args, err := sqlx.Named(stmt, map[string]any{
@@ -519,10 +519,13 @@ WHERE
return nil, nil, ctxerr.Wrap(ctx, err, "select past activity data for VPP app install")
}
- user := &fleet.User{
- ID: res.UserID,
- Name: res.UserName,
- Email: res.UserEmail,
+ var user *fleet.User
+ if res.UserID != nil {
+ user = &fleet.User{
+ ID: *res.UserID,
+ Name: *res.UserName,
+ Email: *res.UserEmail,
+ }
}
var status string
diff --git a/server/datastore/mysql/vpp_test.go b/server/datastore/mysql/vpp_test.go
index 4d831176b9..c3e21b8a3a 100644
--- a/server/datastore/mysql/vpp_test.go
+++ b/server/datastore/mysql/vpp_test.go
@@ -308,6 +308,7 @@ func testVPPAppStatus(t *testing.T, ds *Datastore) {
require.Equal(t, user.ID, actUser.ID)
require.Equal(t, user.Name, actUser.Name)
require.Equal(t, cmd3, act.CommandUUID)
+ require.False(t, act.SelfService)
summary, err = ds.GetSummaryHostVPPAppInstalls(ctx, nil, vpp1)
require.NoError(t, err)
@@ -350,6 +351,19 @@ func testVPPAppStatus(t *testing.T, ds *Datastore) {
summary, err = ds.GetSummaryHostVPPAppInstalls(ctx, &team1.ID, vpp3)
require.NoError(t, err)
require.Equal(t, &fleet.VPPAppStatusSummary{Pending: 0, Failed: 0, Installed: 1}, summary)
+
+ // simulate a self-service request
+ ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
+ _, err := q.ExecContext(ctx,
+ `UPDATE host_vpp_software_installs SET self_service = true, user_id = NULL WHERE command_uuid = ?`,
+ cmd3)
+ return err
+ })
+ actUser, act, err = ds.GetPastActivityDataForVPPAppInstall(ctx, &mdm.CommandResults{CommandUUID: cmd3})
+ require.NoError(t, err)
+ require.Nil(t, actUser)
+ require.Equal(t, cmd3, act.CommandUUID)
+ require.True(t, act.SelfService)
}
// simulates creating the VPP app install request on the host, returns the command UUID.
diff --git a/server/service/client.go b/server/service/client.go
index 740ac19578..b811277e38 100644
--- a/server/service/client.go
+++ b/server/service/client.go
@@ -623,7 +623,7 @@ func (c *Client) ApplyGroup(
for tmName, apps := range tmSoftwareApps {
appPayloads := make([]fleet.VPPBatchPayload, 0, len(apps))
for _, app := range apps {
- appPayloads = append(appPayloads, fleet.VPPBatchPayload{AppStoreID: app.AppStoreID})
+ appPayloads = append(appPayloads, fleet.VPPBatchPayload{AppStoreID: app.AppStoreID, SelfService: app.SelfService})
}
tmSoftwareAppsPayloads[tmName] = appPayloads
}
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index fb3905b8e6..fffda99b09 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -10628,6 +10628,13 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
strconv.Itoa(int(team.ID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Equal(t, 1, countResp.Count)
+ // send an idle request to grab the command uuid
+ cmd, err = mdmClient.Idle()
+ require.NoError(t, err)
+ var fullCmd micromdm.CommandPayload
+ require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
+ cmdUUID = cmd.CommandUUID
+
// Get pending activity
var hostActivitiesResp listHostUpcomingActivitiesResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", installHost.ID),
@@ -10642,20 +10649,24 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", activitiesToString(hostActivitiesResp.Activities))
assert.Equal(t, hostActivitiesResp.Activities[0].Type, fleet.ActivityInstalledAppStoreApp{}.ActivityName())
assert.EqualValues(t, 1, hostActivitiesResp.Count)
+ assert.JSONEq(
+ t,
+ fmt.Sprintf(
+ `{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v}`,
+ installHost.ID,
+ installHost.DisplayName(),
+ app.Name,
+ app.AdamID,
+ cmdUUID,
+ fleet.SoftwareInstallerPending,
+ install.deviceToken != "",
+ ),
+ string(*hostActivitiesResp.Activities[0].Details),
+ )
// Simulate successful installation on the host
- cmd, err = mdmClient.Idle()
+ cmd, err = mdmClient.Acknowledge(cmd.CommandUUID)
require.NoError(t, err)
- for cmd != nil {
- var fullCmd micromdm.CommandPayload
- switch cmd.Command.RequestType {
- case "InstallApplication":
- require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
- cmdUUID = cmd.CommandUUID
- cmd, err = mdmClient.Acknowledge(cmd.CommandUUID)
- require.NoError(t, err)
- }
- }
listResp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id",