mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #36509 and #36513 - batch set in-house apps will now add or delete display names with - batch set software installers will now delete display names (could already add) # Checklist for submitter If some of the following don't apply, delete the relevant line. ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
1733 lines
65 KiB
Go
1733 lines
65 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
|
nanomdm_mysql "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage/mysql"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestInHouseApps(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func(t *testing.T, ds *Datastore)
|
|
}{
|
|
{"TestInHouseAppsCrud", testInHouseAppsCrud},
|
|
{"MultipleTeams", testInHouseAppsMultipleTeams},
|
|
{"BatchSetInHouseInstallers", testBatchSetInHouseInstallers},
|
|
{"BatchSetInHouseInstallersScopedViaLabels", testBatchSetInHouseInstallersScopedViaLabels},
|
|
{"EditDeleteInHouseInstallersActivateNextActivity", testEditDeleteInHouseInstallersActivateNextActivity},
|
|
{"Categories", testInHouseAppsCategories},
|
|
{"SoftwareTitleDisplayName", testSoftwareTitleDisplayNameInHouse},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
defer TruncateTables(t, ds)
|
|
c.fn(t, ds)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testInHouseAppsCrud(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now())
|
|
host3 := test.NewHost(t, ds, "host3", "3", "host3key", "host3uuid", time.Now())
|
|
|
|
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
|
|
require.NoError(t, err)
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID, host2.ID, host3.ID}))
|
|
require.NoError(t, err)
|
|
|
|
nanoEnroll(t, ds, host1, false)
|
|
nanoEnroll(t, ds, host2, false)
|
|
nanoEnroll(t, ds, host3, false)
|
|
|
|
payload := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team.ID,
|
|
UserID: user1.ID,
|
|
Title: "foo",
|
|
Filename: "foo.ipa",
|
|
BundleIdentifier: "com.foo",
|
|
StorageID: "testingtesting123",
|
|
Platform: "ios",
|
|
Extension: "ipa",
|
|
Version: "1.2.3",
|
|
}
|
|
|
|
payloadV2 := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team.ID,
|
|
UserID: user1.ID,
|
|
Title: "foo_v3_different_name", // Not used in code, needed for test
|
|
Filename: "foo_v3_different_name.ipa",
|
|
BundleIdentifier: "com.foo",
|
|
StorageID: "testingtesting456",
|
|
Platform: "ios",
|
|
Extension: "ipa",
|
|
Version: "3.0.0",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
|
|
// -------------------------
|
|
// Upload software installer
|
|
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
|
|
require.Error(t, err, "ValidatedLabels must not be nil")
|
|
|
|
payload.ValidatedLabels = &fleet.LabelIdentsWithScope{}
|
|
installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, installerID)
|
|
require.NotZero(t, titleID)
|
|
|
|
// both ios and ipados apps are created, both installer and title
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
var countI, countS uint
|
|
var ihaFilename, titleName string
|
|
|
|
errI := sqlx.GetContext(ctx, q, &countI, `SELECT COUNT(*) FROM in_house_apps`)
|
|
require.NoError(t, errI)
|
|
require.Equal(t, uint(2), countI)
|
|
errS := sqlx.GetContext(ctx, q, &countS, `SELECT COUNT(*) FROM software_titles`)
|
|
require.NoError(t, errS)
|
|
require.Equal(t, uint(2), countS)
|
|
|
|
errI = sqlx.GetContext(ctx, q, &ihaFilename, `SELECT filename FROM in_house_apps WHERE id = ?`, installerID)
|
|
require.NoError(t, errI)
|
|
require.Equal(t, "foo.ipa", ihaFilename)
|
|
errS = sqlx.GetContext(ctx, q, &titleName, `SELECT name FROM software_titles WHERE id = ?`, titleID)
|
|
require.NoError(t, errS)
|
|
require.Equal(t, "foo", titleName)
|
|
return nil
|
|
})
|
|
|
|
// Try to upload in house app again, expect duplicate error
|
|
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
|
|
require.ErrorContains(t, err, fmt.Sprintf(`In-house app %q already exists`, payload.Filename))
|
|
// Try to upload in house app with different version or name but same bundle id
|
|
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payloadV2)
|
|
require.ErrorContains(t, err, fmt.Sprintf(`In-house app %q already exists`, payloadV2.Filename))
|
|
|
|
// Get new in house app
|
|
installer, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, payload.Title, installer.SoftwareTitle)
|
|
require.Equal(t, payload.Version, installer.Version)
|
|
require.WithinDuration(t, time.Now(), installer.UploadedAt, time.Minute)
|
|
require.False(t, payload.SelfService)
|
|
|
|
// Install on multiple users with pending, success, failure
|
|
createInHouseAppInstallRequest(t, ds, host1.ID, installerID, titleID, user1)
|
|
cmdUUID2 := createInHouseAppInstallRequest(t, ds, host2.ID, installerID, titleID, user1)
|
|
createInHouseAppInstallResultVerified(t, ds, host2, cmdUUID2, "Acknowledged")
|
|
cmdUUID3 := createInHouseAppInstallRequest(t, ds, host3.ID, installerID, titleID, user1)
|
|
createInHouseAppInstallResult(t, ds, host3, cmdUUID3, "Error")
|
|
|
|
// Get summary and expect failed, installed, pending
|
|
summary, err := ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, installerID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 1, Pending: 1, Failed: 1}, *summary)
|
|
|
|
// -------------------------
|
|
// Update software installer
|
|
label, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-1", Query: "select 1"})
|
|
require.NoError(t, err)
|
|
|
|
validatedLabels := fleet.LabelIdentsWithScope{
|
|
LabelScope: "include_any",
|
|
ByName: map[string]fleet.LabelIdent{
|
|
"include-any-1": {
|
|
LabelID: label.ID,
|
|
LabelName: label.Name,
|
|
},
|
|
}}
|
|
updatePayload := fleet.UpdateSoftwareInstallerPayload{
|
|
TeamID: &team.ID,
|
|
TitleID: titleID,
|
|
InstallerID: installerID,
|
|
Filename: "ipa_test.ipa",
|
|
StorageID: "new_storage_id",
|
|
ValidatedLabels: &validatedLabels,
|
|
SelfService: ptr.Bool(true),
|
|
}
|
|
|
|
err = ds.SaveInHouseAppUpdates(ctx, &updatePayload)
|
|
require.NoError(t, err)
|
|
|
|
// Installer updates correctly
|
|
var expectedLabels []fleet.SoftwareScopeLabel
|
|
expectedLabels = append(expectedLabels, fleet.SoftwareScopeLabel{LabelID: label.ID, LabelName: label.Name, Exclude: false, TitleID: titleID})
|
|
|
|
newInstaller, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "new_storage_id", newInstaller.StorageID)
|
|
require.Equal(t, expectedLabels, newInstaller.LabelsIncludeAny)
|
|
require.True(t, newInstaller.SelfService)
|
|
|
|
// Summary is unchanged?
|
|
summary2, err := ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, installerID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, summary, summary2)
|
|
|
|
// -------------------------
|
|
// Delete software installer
|
|
err = ds.DeleteInHouseApp(ctx, installerID)
|
|
require.NoError(t, err)
|
|
|
|
// TODO: test RemovePendingInHouseAppInstalls independently
|
|
|
|
_, err = ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleID)
|
|
require.Error(t, err)
|
|
status, err := ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, installerID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, *status)
|
|
|
|
// Check that entire tables are empty for this test
|
|
checkEmpty := func(table string) {
|
|
var count int
|
|
err := sqlx.GetContext(ctx, ds.reader(ctx), &count, fmt.Sprintf(`SELECT COUNT(*) FROM %s`, table))
|
|
require.NoError(t, err)
|
|
require.Zero(t, count, "expected %s to be empty", table)
|
|
}
|
|
|
|
checkEmpty("in_house_app_labels")
|
|
checkEmpty("host_in_house_software_installs")
|
|
checkEmpty("in_house_app_upcoming_activities")
|
|
checkEmpty("upcoming_activities")
|
|
|
|
// ipadOS installer should remain
|
|
var ipadID uint
|
|
require.NoError(t, sqlx.GetContext(ctx, ds.reader(ctx), &ipadID, `SELECT id FROM in_house_apps LIMIT 1`))
|
|
|
|
// Try to upload installer again, expect duplicate error
|
|
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
|
|
require.Error(t, err)
|
|
|
|
// Delete ipadOS installer
|
|
require.NoError(t, ds.DeleteInHouseApp(ctx, ipadID))
|
|
|
|
var count int
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(*) FROM in_house_apps`)
|
|
require.NoError(t, err)
|
|
require.Zero(t, count, "expected in_house_apps to be empty")
|
|
|
|
// create one that's self-service at creation time
|
|
payload2 := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team.ID,
|
|
UserID: user1.ID,
|
|
Title: "foo2",
|
|
Filename: "foo2.ipa",
|
|
BundleIdentifier: "com.foo2",
|
|
StorageID: "testingtesting1234",
|
|
Platform: "ios",
|
|
Extension: "ipa",
|
|
Version: "1.2.3",
|
|
SelfService: true,
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
_, titleID2, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, installerID)
|
|
require.NotZero(t, titleID)
|
|
|
|
installer2, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleID2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, payload2.Title, installer2.SoftwareTitle)
|
|
require.True(t, installer2.SelfService)
|
|
|
|
// Test that app name is correct
|
|
payloadWithTitle := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team.ID,
|
|
UserID: user1.ID,
|
|
Title: "New Title",
|
|
Filename: "foo_with_title.ipa",
|
|
BundleIdentifier: "com.foo_with_title",
|
|
StorageID: "testing5",
|
|
Extension: "ipa",
|
|
Version: "1.0.0",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
_, titleIDWithTitle, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payloadWithTitle)
|
|
require.NoError(t, err)
|
|
installerWithTitle, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleIDWithTitle)
|
|
require.NoError(t, err)
|
|
require.Equal(t, payloadWithTitle.Title, installerWithTitle.SoftwareTitle)
|
|
|
|
// Different bundle id with same name should create new software title
|
|
payloadSameName := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team.ID,
|
|
UserID: user1.ID,
|
|
Title: "New Title",
|
|
Filename: "different_filename.ipa",
|
|
BundleIdentifier: "com.different.bundle",
|
|
StorageID: "testing5",
|
|
Extension: "ipa",
|
|
Version: "1.0.0",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
_, titleSameName, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payloadSameName)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, titleIDWithTitle, titleSameName)
|
|
}
|
|
|
|
func testInHouseAppsMultipleTeams(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now())
|
|
|
|
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
|
|
|
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
|
|
require.NoError(t, err)
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host1.ID}))
|
|
require.NoError(t, err)
|
|
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"})
|
|
require.NoError(t, err)
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team2.ID, []uint{host2.ID}))
|
|
require.NoError(t, err)
|
|
|
|
nanoEnroll(t, ds, host1, false)
|
|
|
|
payload1 := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team1.ID,
|
|
UserID: user1.ID,
|
|
Title: "foo",
|
|
BundleIdentifier: "com.foo",
|
|
StorageID: "testingtesting123",
|
|
Platform: "ios",
|
|
Extension: "ipa",
|
|
Version: "1.2.3",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
|
|
payload2 := payload1
|
|
payload2.TeamID = &team2.ID
|
|
|
|
payloadNoTeam := payload1
|
|
payloadNoTeam.TeamID = nil
|
|
|
|
// Add installers for both teams
|
|
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload1)
|
|
require.NoError(t, err)
|
|
installerID2, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
|
|
require.NoError(t, err)
|
|
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payloadNoTeam)
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM software_titles`)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, count)
|
|
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 6, count)
|
|
|
|
// Team 2: Delete 1 installer from 1 team
|
|
err = ds.DeleteInHouseApp(ctx, installerID2)
|
|
require.NoError(t, err)
|
|
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 5, count)
|
|
|
|
// Team 2: Try to add installer again
|
|
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
|
|
require.Error(t, err)
|
|
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 5, count)
|
|
|
|
// Test that software titles for IHA don't get cleaned up
|
|
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
|
|
require.NoError(t, ds.CleanupSoftwareTitles(ctx))
|
|
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
|
|
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM software_titles`)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, count)
|
|
|
|
}
|
|
|
|
func testInHouseAppsCategories(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
|
|
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
|
|
|
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host1.ID})))
|
|
|
|
nanoEnroll(t, ds, host1, false)
|
|
|
|
payload1 := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team1.ID,
|
|
UserID: user1.ID,
|
|
BundleIdentifier: "com.foo",
|
|
Filename: "foo.ipa",
|
|
StorageID: "id1234",
|
|
Extension: "ipa",
|
|
SelfService: false,
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
CategoryIDs: []uint{1, 2},
|
|
}
|
|
|
|
// Add installers for both teams
|
|
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload1)
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, installerID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, count)
|
|
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM software_titles`)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, count)
|
|
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, count)
|
|
|
|
// Test with empty categories
|
|
|
|
payload2 := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team1.ID,
|
|
UserID: user1.ID,
|
|
BundleIdentifier: "com.bar",
|
|
Filename: "bar.ipa",
|
|
StorageID: "id5678",
|
|
Extension: "ipa",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
CategoryIDs: nil, // empty slice should work the same
|
|
}
|
|
|
|
secondInstallerID, secondTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
|
|
require.NoError(t, err)
|
|
|
|
// Check that this has no categories
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, secondInstallerID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, count)
|
|
|
|
// Update software categories
|
|
updatePayload := fleet.UpdateSoftwareInstallerPayload{
|
|
TeamID: payload2.TeamID,
|
|
TitleID: secondTitleID,
|
|
InstallerID: secondInstallerID,
|
|
Filename: payload2.Filename,
|
|
StorageID: payload2.StorageID,
|
|
ValidatedLabels: payload2.ValidatedLabels,
|
|
CategoryIDs: []uint{1, 2},
|
|
SelfService: ptr.Bool(true),
|
|
}
|
|
|
|
err = ds.SaveInHouseAppUpdates(ctx, &updatePayload)
|
|
require.NoError(t, err)
|
|
|
|
// Categories
|
|
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, secondInstallerID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, count)
|
|
}
|
|
|
|
func createInHouseAppInstallRequest(t *testing.T, ds *Datastore, hostID uint, appID uint, titleID uint, user *fleet.User) string {
|
|
ctx := context.Background()
|
|
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
|
|
|
|
cmdUUID := uuid.NewString()
|
|
|
|
err := ds.InsertHostInHouseAppInstall(ctx, hostID, appID, titleID, cmdUUID, fleet.HostSoftwareInstallOptions{})
|
|
require.NoError(t, err)
|
|
return cmdUUID
|
|
}
|
|
|
|
func createInHouseAppInstallResult(t *testing.T, ds *Datastore, host *fleet.Host, cmdUUID string, status string) {
|
|
ctx := context.Background()
|
|
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
|
|
|
|
nanoDB, err := nanomdm_mysql.New(nanomdm_mysql.WithDB(ds.primary.DB))
|
|
require.NoError(t, err)
|
|
nanoCtx := &mdm.Request{EnrollID: &mdm.EnrollID{ID: host.UUID}, Context: ctx}
|
|
|
|
cmdRes := &mdm.CommandResults{
|
|
CommandUUID: cmdUUID,
|
|
Status: status,
|
|
Raw: []byte(`<?xml version="1.0" encoding="UTF-8"?>`),
|
|
}
|
|
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
|
|
require.NoError(t, err)
|
|
|
|
// inserting the activity is what marks the upcoming activity as completed
|
|
// (and activates the next one).
|
|
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
|
|
HostID: host.ID,
|
|
CommandUUID: cmdUUID,
|
|
}, []byte(`{}`), time.Now())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func createInHouseAppInstallResultVerified(t *testing.T, ds *Datastore, host *fleet.Host, cmdUUID string, status string) {
|
|
createInHouseAppInstallResult(t, ds, host, cmdUUID, status)
|
|
|
|
ctx := t.Context()
|
|
timestampCol := "verification_at"
|
|
if status != "Acknowledged" {
|
|
timestampCol = "verification_failed_at"
|
|
}
|
|
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
|
|
_, err := tx.ExecContext(ctx, fmt.Sprintf(`UPDATE host_in_house_software_installs SET
|
|
%s = NOW(6), verification_command_uuid = ? WHERE host_id = ? AND command_uuid = ?`, timestampCol),
|
|
uuid.NewString(), host.ID, cmdUUID)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func testBatchSetInHouseInstallers(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
t.Cleanup(func() { ds.testActivateSpecificNextActivities = nil })
|
|
|
|
// create a team
|
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name()})
|
|
require.NoError(t, err)
|
|
|
|
// create a couple hosts
|
|
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now(), test.WithPlatform("ios"))
|
|
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now(), test.WithPlatform("ios"))
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID, host2.ID}))
|
|
require.NoError(t, err)
|
|
nanoEnroll(t, ds, host1, false)
|
|
nanoEnroll(t, ds, host2, false)
|
|
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
|
|
|
assertTitlesAndApps := func(wantTitles []fleet.SoftwareTitleListResult, wantApps []fleet.SoftwareInstaller) {
|
|
tmFilter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
|
|
titles, _, _, err := ds.ListSoftwareTitles(
|
|
ctx,
|
|
fleet.SoftwareTitleListOptions{TeamID: &team.ID},
|
|
tmFilter,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, titles, len(wantTitles))
|
|
|
|
sort.Slice(wantTitles, func(i, j int) bool {
|
|
l, r := wantTitles[i], wantTitles[j]
|
|
return l.Name < r.Name || l.Name == r.Name && l.Source < r.Source
|
|
})
|
|
sort.Slice(titles, func(i, j int) bool {
|
|
l, r := titles[i], titles[j]
|
|
return l.Name < r.Name || l.Name == r.Name && l.Source < r.Source
|
|
})
|
|
|
|
titleIDs := make([]uint, len(wantTitles))
|
|
for i, want := range wantTitles {
|
|
got := titles[i]
|
|
require.Equal(t, want.Name, got.Name)
|
|
require.Equal(t, want.Source, got.Source)
|
|
require.Equal(t, want.DisplayName, got.DisplayName)
|
|
require.Equal(t, want.BundleIdentifier == nil, got.BundleIdentifier == nil)
|
|
if want.BundleIdentifier != nil {
|
|
require.Equal(t, *want.BundleIdentifier, *got.BundleIdentifier)
|
|
}
|
|
titleIDs[i] = got.ID
|
|
}
|
|
|
|
sort.Slice(wantApps, func(i, j int) bool {
|
|
l, r := wantApps[i], wantApps[j]
|
|
return l.Name < r.Name || l.Name == r.Name && l.Platform < r.Platform
|
|
})
|
|
require.Len(t, wantApps, len(titleIDs))
|
|
|
|
for i, want := range wantApps {
|
|
got, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleIDs[i])
|
|
require.NoError(t, err)
|
|
require.Equal(t, want.Name, got.Name)
|
|
require.Equal(t, want.Platform, got.Platform)
|
|
require.Equal(t, want.Version, got.Version)
|
|
require.Equal(t, want.StorageID, got.StorageID)
|
|
require.Equal(t, want.DisplayName, got.DisplayName)
|
|
require.Equal(t, want.SelfService, got.SelfService)
|
|
require.Equal(t, want.BundleIdentifier, got.BundleIdentifier)
|
|
require.ElementsMatch(t, want.Categories, got.Categories)
|
|
}
|
|
}
|
|
|
|
// batch set with everything empty
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, nil)
|
|
require.NoError(t, err)
|
|
apps, err := ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, apps)
|
|
assertTitlesAndApps(nil, nil)
|
|
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
|
|
require.NoError(t, err)
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, apps)
|
|
assertTitlesAndApps(nil, nil)
|
|
|
|
ipa1 := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team.ID,
|
|
UserID: user1.ID,
|
|
Title: "ipa1",
|
|
Filename: "ipa1.ipa",
|
|
BundleIdentifier: "com.ipa1",
|
|
StorageID: "ipa1",
|
|
Extension: "ipa",
|
|
Version: "1.0.0",
|
|
SelfService: true,
|
|
DisplayName: "Yeehaw 1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
// the batch-upload handler would've generated both iOS and iPadOS entries
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, apps, 2)
|
|
require.NotNil(t, apps[0].TeamID)
|
|
require.Equal(t, team.ID, *apps[0].TeamID)
|
|
require.NotNil(t, apps[0].TitleID)
|
|
require.Equal(t, "https://example.com/1", apps[0].URL)
|
|
|
|
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
|
|
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
}, []fleet.SoftwareInstaller{
|
|
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
})
|
|
|
|
// add a new installer + ipa1 installer
|
|
ipa2 := fleet.UploadSoftwareInstallerPayload{
|
|
TeamID: &team.ID,
|
|
UserID: user1.ID,
|
|
Title: "ipa2",
|
|
Filename: "ipa2.ipa",
|
|
BundleIdentifier: "com.ipa2",
|
|
StorageID: "ipa2",
|
|
Extension: "ipa",
|
|
Version: "2.0.0",
|
|
SelfService: false,
|
|
DisplayName: "Yeehaw 2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
|
|
// the batch-upload handler would've generated both iOS and iPadOS entries
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
sort.Slice(apps, func(i, j int) bool {
|
|
l, r := apps[i], apps[j]
|
|
return l.URL < r.URL
|
|
})
|
|
require.Len(t, apps, 4)
|
|
require.NotNil(t, apps[0].TeamID)
|
|
require.Equal(t, team.ID, *apps[0].TeamID)
|
|
require.NotNil(t, apps[0].TitleID)
|
|
require.Equal(t, "https://example.com/1", apps[0].URL)
|
|
|
|
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
|
|
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
}, []fleet.SoftwareInstaller{
|
|
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, DisplayName: ipa2.DisplayName},
|
|
})
|
|
|
|
// rerun with no change
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, apps, 4)
|
|
|
|
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
|
|
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
}, []fleet.SoftwareInstaller{
|
|
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, DisplayName: ipa2.DisplayName},
|
|
})
|
|
|
|
// change ipa2 self-service and add categories
|
|
ipa2.SelfService = !ipa2.SelfService
|
|
ipa2.Categories = []string{"Communication", "Productivity"}
|
|
catIDs, err := ds.GetSoftwareCategoryIDs(ctx, ipa2.Categories)
|
|
require.NoError(t, err)
|
|
ipa2.CategoryIDs = catIDs
|
|
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
CategoryIDs: ipa2.CategoryIDs,
|
|
},
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
CategoryIDs: ipa2.CategoryIDs,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, apps, 4)
|
|
|
|
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
|
|
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
}, []fleet.SoftwareInstaller{
|
|
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
|
|
})
|
|
|
|
// remove ipa1
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
CategoryIDs: ipa2.CategoryIDs,
|
|
},
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
CategoryIDs: ipa2.CategoryIDs,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, apps, 2)
|
|
|
|
var iosInHouseID2, iosTitleID2 uint
|
|
for _, app := range apps {
|
|
require.NotNil(t, app.TitleID)
|
|
meta, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, *app.TitleID)
|
|
require.NoError(t, err)
|
|
if meta.Platform == "ios" {
|
|
iosInHouseID2 = meta.InstallerID
|
|
iosTitleID2 = *app.TitleID
|
|
}
|
|
}
|
|
|
|
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
|
|
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
}, []fleet.SoftwareInstaller{
|
|
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
|
|
})
|
|
|
|
// add pending and completed installs for ipa2
|
|
completedCmd := createInHouseAppInstallRequest(t, ds, host1.ID, iosInHouseID2, iosTitleID2, user1)
|
|
createInHouseAppInstallRequest(t, ds, host2.ID, iosInHouseID2, iosTitleID2, user1)
|
|
createInHouseAppInstallResultVerified(t, ds, host1, completedCmd, "Acknowledged")
|
|
|
|
summary, err := ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 1, Pending: 1}, *summary)
|
|
|
|
// batch-set without changes, should not affect installs
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
CategoryIDs: ipa2.CategoryIDs,
|
|
},
|
|
{
|
|
StorageID: ipa2.StorageID,
|
|
Filename: ipa2.Filename,
|
|
Title: ipa2.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa2.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/2",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa2.BundleIdentifier,
|
|
SelfService: ipa2.SelfService,
|
|
DisplayName: ipa2.DisplayName,
|
|
CategoryIDs: ipa2.CategoryIDs,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, apps, 2)
|
|
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
|
|
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
|
|
}, []fleet.SoftwareInstaller{
|
|
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
|
|
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
|
|
})
|
|
|
|
summary, err = ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 1, Pending: 1}, *summary)
|
|
|
|
ipa2TitleID := apps[0].TitleID
|
|
name, err := ds.getSoftwareTitleDisplayName(ctx, team.ID, *ipa2TitleID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, ipa2.DisplayName, name)
|
|
|
|
// remove ipa2 and add ipa1
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
DisplayName: ipa1.DisplayName,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, apps, 2)
|
|
|
|
var iosInHouseID1, iosTitleID1 uint
|
|
for _, app := range apps {
|
|
require.NotNil(t, app.TitleID)
|
|
meta, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, *app.TitleID)
|
|
require.NoError(t, err)
|
|
if meta.Platform == "ios" {
|
|
iosInHouseID1 = meta.InstallerID
|
|
iosTitleID1 = *app.TitleID
|
|
}
|
|
}
|
|
|
|
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
|
|
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
|
|
}, []fleet.SoftwareInstaller{
|
|
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
|
|
})
|
|
|
|
// stats don't report anything about ipa2 anymore
|
|
summary, err = ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 0, Pending: 0}, *summary)
|
|
|
|
pendingHost1, _, err := ds.ListHostUpcomingActivities(ctx, host1.ID, fleet.ListOptions{PerPage: 10})
|
|
require.NoError(t, err)
|
|
require.Empty(t, pendingHost1)
|
|
pendingHost2, _, err := ds.ListHostUpcomingActivities(ctx, host2.ID, fleet.ListOptions{PerPage: 10})
|
|
require.NoError(t, err)
|
|
require.Empty(t, pendingHost2)
|
|
|
|
// display names for ipa2 are removed
|
|
_, err = ds.getSoftwareTitleDisplayName(ctx, team.ID, *ipa2TitleID)
|
|
require.ErrorContains(t, err, "not found")
|
|
|
|
// add pending and completed installs for ipa1
|
|
completedCmd = createInHouseAppInstallRequest(t, ds, host1.ID, iosInHouseID1, iosTitleID1, user1)
|
|
createInHouseAppInstallRequest(t, ds, host2.ID, iosInHouseID1, iosTitleID1, user1)
|
|
createInHouseAppInstallResultVerified(t, ds, host1, completedCmd, "Acknowledged")
|
|
|
|
summary, err = ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 1, Pending: 1}, *summary)
|
|
|
|
// update the storage ID of ipa1 (so it is a different installer binary)
|
|
ipa1.StorageID = "ipa1-new"
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ios_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
},
|
|
{
|
|
StorageID: ipa1.StorageID,
|
|
Filename: ipa1.Filename,
|
|
Title: ipa1.Title,
|
|
Source: "ipados_apps",
|
|
Version: ipa1.Version,
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: ipa1.BundleIdentifier,
|
|
SelfService: ipa1.SelfService,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, apps, 2)
|
|
|
|
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
|
|
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1")},
|
|
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1")},
|
|
}, []fleet.SoftwareInstaller{
|
|
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier},
|
|
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier},
|
|
})
|
|
|
|
// stats don't report anything about ipa1 anymore (as if it was deleted)
|
|
summary, err = ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 0, Pending: 0}, *summary)
|
|
|
|
pendingHost1, _, err = ds.ListHostUpcomingActivities(ctx, host1.ID, fleet.ListOptions{PerPage: 10})
|
|
require.NoError(t, err)
|
|
require.Empty(t, pendingHost1)
|
|
pendingHost2, _, err = ds.ListHostUpcomingActivities(ctx, host2.ID, fleet.ListOptions{PerPage: 10})
|
|
require.NoError(t, err)
|
|
require.Empty(t, pendingHost2)
|
|
|
|
// remove everything
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
|
|
require.NoError(t, err)
|
|
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, apps)
|
|
assertTitlesAndApps(nil, nil)
|
|
}
|
|
|
|
func testBatchSetInHouseInstallersScopedViaLabels(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
|
|
// create a host to have a pending install request
|
|
host := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now(), test.WithPlatform("ios"))
|
|
nanoEnroll(t, ds, host, false)
|
|
|
|
// create a couple teams and a user
|
|
tm1, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "1"})
|
|
require.NoError(t, err)
|
|
tm2, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "2"})
|
|
require.NoError(t, err)
|
|
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
|
|
|
// create some installer payloads to be used by test cases
|
|
installers := make([]*fleet.UploadSoftwareInstallerPayload, 3)
|
|
for i := range installers {
|
|
installers[i] = &fleet.UploadSoftwareInstallerPayload{
|
|
UserID: user.ID,
|
|
Title: "ipa" + fmt.Sprint(i),
|
|
Filename: fmt.Sprintf("ipa%d.ipa", i),
|
|
BundleIdentifier: "com.ipa" + fmt.Sprint(i),
|
|
StorageID: "ipa" + fmt.Sprint(i),
|
|
Extension: "ipa",
|
|
Version: "1.0.0",
|
|
URL: "https://example.com/" + fmt.Sprint(i),
|
|
Source: "ios_apps",
|
|
Platform: "ios",
|
|
}
|
|
}
|
|
|
|
// create some labels to be used by test cases
|
|
labels := make([]*fleet.Label, 4)
|
|
for i := range labels {
|
|
lbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "label" + fmt.Sprint(i)})
|
|
require.NoError(t, err)
|
|
labels[i] = lbl
|
|
}
|
|
|
|
type testPayload struct {
|
|
Installer *fleet.UploadSoftwareInstallerPayload
|
|
Labels []*fleet.Label
|
|
Exclude bool
|
|
ShouldCancelPending *bool // nil if the installer is new (could not have pending), otherwise true/false if it was edited
|
|
}
|
|
|
|
// test scenarios - note that subtests must NOT be used as the sequence of
|
|
// tests matters - they cannot be run in isolation.
|
|
cases := []struct {
|
|
desc string
|
|
team *fleet.Team
|
|
payload []testPayload
|
|
}{
|
|
{
|
|
desc: "empty payload",
|
|
payload: nil,
|
|
},
|
|
{
|
|
desc: "no team, installer0, no label",
|
|
payload: []testPayload{
|
|
{Installer: installers[0]},
|
|
},
|
|
},
|
|
{
|
|
desc: "team 1, installer0, include label0",
|
|
team: tm1,
|
|
payload: []testPayload{
|
|
{Installer: installers[0], Labels: []*fleet.Label{labels[0]}},
|
|
},
|
|
},
|
|
{
|
|
desc: "no team, installer0 no change, add installer1 with exclude label1",
|
|
payload: []testPayload{
|
|
{Installer: installers[0], ShouldCancelPending: ptr.Bool(false)},
|
|
{Installer: installers[1], Labels: []*fleet.Label{labels[1]}, Exclude: true},
|
|
},
|
|
},
|
|
{
|
|
desc: "no team, installer0 no change, installer1 change to include label1",
|
|
payload: []testPayload{
|
|
{Installer: installers[0], ShouldCancelPending: ptr.Bool(false)},
|
|
{Installer: installers[1], Labels: []*fleet.Label{labels[1]}, Exclude: false, ShouldCancelPending: ptr.Bool(true)},
|
|
},
|
|
},
|
|
{
|
|
desc: "team 1, installer0, include label0 and add label1",
|
|
team: tm1,
|
|
payload: []testPayload{
|
|
{Installer: installers[0], Labels: []*fleet.Label{labels[0], labels[1]}, ShouldCancelPending: ptr.Bool(true)},
|
|
},
|
|
},
|
|
{
|
|
desc: "team 1, installer0, remove label0 and keep label1",
|
|
team: tm1,
|
|
payload: []testPayload{
|
|
{Installer: installers[0], Labels: []*fleet.Label{labels[1]}, ShouldCancelPending: ptr.Bool(true)},
|
|
},
|
|
},
|
|
{
|
|
desc: "team 1, installer0, switch to label0 and label2",
|
|
team: tm1,
|
|
payload: []testPayload{
|
|
{Installer: installers[0], Labels: []*fleet.Label{labels[0], labels[2]}, ShouldCancelPending: ptr.Bool(true)},
|
|
},
|
|
},
|
|
{
|
|
desc: "team 2, 3 installers, mix of labels",
|
|
team: tm2,
|
|
payload: []testPayload{
|
|
{Installer: installers[0], Labels: []*fleet.Label{labels[0]}, Exclude: false},
|
|
{Installer: installers[1], Labels: []*fleet.Label{labels[0], labels[1], labels[2]}, Exclude: true},
|
|
{Installer: installers[2], Labels: []*fleet.Label{labels[1], labels[2]}, Exclude: false},
|
|
},
|
|
},
|
|
{
|
|
desc: "team 1, installer0 no change and add installer2",
|
|
team: tm1,
|
|
payload: []testPayload{
|
|
{Installer: installers[0], Labels: []*fleet.Label{labels[0], labels[2]}, ShouldCancelPending: ptr.Bool(false)},
|
|
{Installer: installers[2]},
|
|
},
|
|
},
|
|
{
|
|
desc: "team 1, installer0 switch to labels 1 and 3, installer2 no change",
|
|
team: tm1,
|
|
payload: []testPayload{
|
|
{Installer: installers[0], Labels: []*fleet.Label{labels[1], labels[3]}, ShouldCancelPending: ptr.Bool(true)},
|
|
{Installer: installers[2], ShouldCancelPending: ptr.Bool(false)},
|
|
},
|
|
},
|
|
{
|
|
desc: "team 2, remove installer0, labels of install1 and no change installer2",
|
|
team: tm2,
|
|
payload: []testPayload{
|
|
{Installer: installers[1], ShouldCancelPending: ptr.Bool(true)},
|
|
{Installer: installers[2], Labels: []*fleet.Label{labels[1], labels[2]}, Exclude: false, ShouldCancelPending: ptr.Bool(false)},
|
|
},
|
|
},
|
|
{
|
|
desc: "no team, remove all",
|
|
payload: []testPayload{},
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Log("Running test case ", c.desc)
|
|
|
|
var teamID *uint
|
|
var globalOrTeamID uint
|
|
if c.team != nil {
|
|
teamID = &c.team.ID
|
|
globalOrTeamID = c.team.ID
|
|
}
|
|
|
|
// cleanup any existing install requests for the host
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
if _, err := q.ExecContext(ctx, `DELETE FROM upcoming_activities WHERE host_id = ?`, host.ID); err != nil {
|
|
return err
|
|
}
|
|
_, err := q.ExecContext(ctx, `DELETE FROM host_in_house_software_installs WHERE host_id = ?`, host.ID)
|
|
return err
|
|
})
|
|
|
|
installerIDs := make([]uint, len(c.payload))
|
|
if len(c.payload) > 0 {
|
|
// create pending install requests for each updated installer, to see if
|
|
// it cancels it or not as expected.
|
|
err := ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(teamID, []uint{host.ID}))
|
|
require.NoError(t, err)
|
|
for i, payload := range c.payload {
|
|
if payload.ShouldCancelPending != nil {
|
|
// the installer must exist
|
|
var ihaID, titleID uint
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
err := sqlx.GetContext(ctx, q, &titleID, `SELECT id FROM software_titles WHERE name = ? AND source = ? AND extension_for = ''`,
|
|
payload.Installer.Title, payload.Installer.Source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = sqlx.GetContext(ctx, q, &ihaID, `SELECT id FROM in_house_apps WHERE global_or_team_id = ?
|
|
AND title_id = ?`, globalOrTeamID, titleID)
|
|
return err
|
|
})
|
|
createInHouseAppInstallRequest(t, ds, host.ID, ihaID, titleID, user)
|
|
installerIDs[i] = ihaID
|
|
}
|
|
}
|
|
}
|
|
|
|
// create the payload by copying the test one, so that the original installers
|
|
// structs are not modified
|
|
payload := make([]*fleet.UploadSoftwareInstallerPayload, len(c.payload))
|
|
for i, p := range c.payload {
|
|
installer := *p.Installer
|
|
installer.ValidatedLabels = &fleet.LabelIdentsWithScope{LabelScope: fleet.LabelScopeIncludeAny}
|
|
if p.Exclude {
|
|
installer.ValidatedLabels.LabelScope = fleet.LabelScopeExcludeAny
|
|
}
|
|
byName := make(map[string]fleet.LabelIdent, len(p.Labels))
|
|
for _, lbl := range p.Labels {
|
|
byName[lbl.Name] = fleet.LabelIdent{LabelName: lbl.Name, LabelID: lbl.ID}
|
|
}
|
|
installer.ValidatedLabels.ByName = byName
|
|
payload[i] = &installer
|
|
}
|
|
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, teamID, payload)
|
|
require.NoError(t, err)
|
|
installers, err := ds.GetSoftwareInstallers(ctx, globalOrTeamID)
|
|
require.NoError(t, err)
|
|
require.Len(t, installers, len(c.payload))
|
|
|
|
// get the metadata for each installer to assert the batch did set the
|
|
// expected ones.
|
|
installersByFilename := make(map[string]*fleet.SoftwareInstaller, len(installers))
|
|
for _, ins := range installers {
|
|
meta, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, teamID, *ins.TitleID)
|
|
require.NoError(t, err)
|
|
installersByFilename[meta.Name] = meta
|
|
}
|
|
|
|
// validate that the inserted software is as expected
|
|
for i, payload := range c.payload {
|
|
meta, ok := installersByFilename[payload.Installer.Filename]
|
|
require.True(t, ok, "installer %s was not created", payload.Installer.Filename)
|
|
require.Equal(t, meta.SoftwareTitle, payload.Installer.Title)
|
|
|
|
wantLabelIDs := make([]uint, len(payload.Labels))
|
|
for j, lbl := range payload.Labels {
|
|
wantLabelIDs[j] = lbl.ID
|
|
}
|
|
if payload.Exclude {
|
|
require.Empty(t, meta.LabelsIncludeAny)
|
|
gotLabelIDs := make([]uint, len(meta.LabelsExcludeAny))
|
|
for i, lbl := range meta.LabelsExcludeAny {
|
|
gotLabelIDs[i] = lbl.LabelID
|
|
}
|
|
require.ElementsMatch(t, wantLabelIDs, gotLabelIDs)
|
|
} else {
|
|
require.Empty(t, meta.LabelsExcludeAny)
|
|
gotLabelIDs := make([]uint, len(meta.LabelsIncludeAny))
|
|
for j, lbl := range meta.LabelsIncludeAny {
|
|
gotLabelIDs[j] = lbl.LabelID
|
|
}
|
|
require.ElementsMatch(t, wantLabelIDs, gotLabelIDs)
|
|
}
|
|
|
|
// check if it deleted pending installs or not
|
|
if payload.ShouldCancelPending != nil {
|
|
var exists bool
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
err := sqlx.GetContext(ctx, q, &exists, `SELECT 1
|
|
FROM upcoming_activities ua
|
|
INNER JOIN in_house_app_upcoming_activities ihua
|
|
ON ua.id = ihua.upcoming_activity_id AND ua.activity_type = 'in_house_app_install'
|
|
WHERE ua.host_id = ? AND ihua.in_house_app_id = ?`, host.ID, installerIDs[i])
|
|
if err == sql.ErrNoRows {
|
|
err = nil
|
|
}
|
|
return err
|
|
})
|
|
if *payload.ShouldCancelPending {
|
|
require.False(t, exists, "pending install for installer %s was not cancelled but it should have been", payload.Installer.Filename)
|
|
} else {
|
|
require.True(t, exists, "pending install for installer %s was cancelled but it should not have been", payload.Installer.Filename)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func testEditDeleteInHouseInstallersActivateNextActivity(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
|
|
|
// create a label
|
|
label, err := ds.NewLabel(ctx, &fleet.Label{Name: "A"})
|
|
require.NoError(t, err)
|
|
|
|
// create a few installers
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, nil, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: "ipa0",
|
|
Filename: "ipa0.ipa",
|
|
Title: "ipa0",
|
|
Source: "ios_apps",
|
|
Version: "1.0.0",
|
|
UserID: user.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/0",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: "com.ipa0",
|
|
SelfService: false,
|
|
},
|
|
{
|
|
StorageID: "ipa1",
|
|
Filename: "ipa1.ipa",
|
|
Title: "ipa1",
|
|
Source: "ios_apps",
|
|
Version: "1.0.0",
|
|
UserID: user.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: "com.ipa1",
|
|
SelfService: false,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
installers, err := ds.GetSoftwareInstallers(ctx, 0)
|
|
require.NoError(t, err)
|
|
require.Len(t, installers, 2)
|
|
sort.Slice(installers, func(i, j int) bool {
|
|
return installers[i].URL < installers[j].URL
|
|
})
|
|
ipa0, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, nil, *installers[0].TitleID)
|
|
require.NoError(t, err)
|
|
ipa1, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, nil, *installers[1].TitleID)
|
|
require.NoError(t, err)
|
|
|
|
// create a few hosts
|
|
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now(), test.WithPlatform("ios"))
|
|
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now(), test.WithPlatform("ios"))
|
|
host3 := test.NewHost(t, ds, "host3", "3", "host3key", "host3uuid", time.Now(), test.WithPlatform("ios"))
|
|
nanoEnroll(t, ds, host1, false)
|
|
nanoEnroll(t, ds, host2, false)
|
|
nanoEnroll(t, ds, host3, false)
|
|
|
|
// enqueue software installs on each host
|
|
host1Ipa0 := createInHouseAppInstallRequest(t, ds, host1.ID, ipa0.InstallerID, *installers[0].TitleID, user)
|
|
host1Ipa1 := createInHouseAppInstallRequest(t, ds, host1.ID, ipa1.InstallerID, *installers[1].TitleID, user)
|
|
// add a script exec as last activity for host1
|
|
host1Script, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{
|
|
HostID: host1.ID, ScriptContents: "echo", UserID: &user.ID, SyncRequest: true,
|
|
})
|
|
require.NoError(t, err)
|
|
host2Ipa0 := createInHouseAppInstallRequest(t, ds, host2.ID, ipa0.InstallerID, *installers[0].TitleID, user)
|
|
host2Ipa1 := createInHouseAppInstallRequest(t, ds, host2.ID, ipa1.InstallerID, *installers[1].TitleID, user)
|
|
// add a script exec as first activity for host3
|
|
host3Script, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{
|
|
HostID: host3.ID, ScriptContents: "echo", UserID: &user.ID, SyncRequest: true,
|
|
})
|
|
require.NoError(t, err)
|
|
host3Ipa1 := createInHouseAppInstallRequest(t, ds, host3.ID, ipa1.InstallerID, *installers[1].TitleID, user)
|
|
|
|
checkUpcomingActivities(t, ds, host1, host1Ipa0, host1Ipa1, host1Script.ExecutionID)
|
|
checkUpcomingActivities(t, ds, host2, host2Ipa0, host2Ipa1)
|
|
checkUpcomingActivities(t, ds, host3, host3Script.ExecutionID, host3Ipa1)
|
|
|
|
// update installer ipa0 metadata (label condition)
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, nil, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: "ipa0",
|
|
Filename: "ipa0.ipa",
|
|
Title: "ipa0",
|
|
Source: "ios_apps",
|
|
Version: "1.0.0",
|
|
UserID: user.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/0",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{
|
|
LabelScope: fleet.LabelScopeIncludeAny,
|
|
ByName: map[string]fleet.LabelIdent{label.Name: {LabelID: label.ID, LabelName: label.Name}},
|
|
},
|
|
BundleIdentifier: "com.ipa0",
|
|
SelfService: false,
|
|
},
|
|
{
|
|
StorageID: "ipa1",
|
|
Filename: "ipa1.ipa",
|
|
Title: "ipa1",
|
|
Source: "ios_apps",
|
|
Version: "1.0.0",
|
|
UserID: user.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: "com.ipa1",
|
|
SelfService: false,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// installer ipa0 activities were deleted, next activity was activated
|
|
checkUpcomingActivities(t, ds, host1, host1Ipa1, host1Script.ExecutionID)
|
|
checkUpcomingActivities(t, ds, host2, host2Ipa1)
|
|
checkUpcomingActivities(t, ds, host3, host3Script.ExecutionID, host3Ipa1)
|
|
|
|
// delete ipa1
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, nil, []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: "ipa0",
|
|
Filename: "ipa0.ipa",
|
|
Title: "ipa0",
|
|
Source: "ios_apps",
|
|
Version: "1.0.0",
|
|
UserID: user.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/0",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{
|
|
LabelScope: fleet.LabelScopeIncludeAny,
|
|
ByName: map[string]fleet.LabelIdent{label.Name: {LabelID: label.ID, LabelName: label.Name}},
|
|
},
|
|
BundleIdentifier: "com.ipa0",
|
|
SelfService: false,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// installer ipa1 activities were deleted, next activity was activated for host1 and host2
|
|
checkUpcomingActivities(t, ds, host1, host1Script.ExecutionID)
|
|
checkUpcomingActivities(t, ds, host2)
|
|
checkUpcomingActivities(t, ds, host3, host3Script.ExecutionID)
|
|
}
|
|
|
|
func testSoftwareTitleDisplayNameInHouse(t *testing.T, ds *Datastore) {
|
|
ctx := context.Background()
|
|
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
|
|
|
payload := fleet.UploadSoftwareInstallerPayload{
|
|
UserID: user1.ID,
|
|
Title: "foo",
|
|
BundleIdentifier: "com.foo",
|
|
Filename: "foo.ipa",
|
|
StorageID: "testingtesting123",
|
|
Platform: "ios",
|
|
Extension: "ipa",
|
|
Version: "1.2.3",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
|
|
require.NoError(t, err)
|
|
|
|
err = ds.SaveInHouseAppUpdates(ctx, &fleet.UpdateSoftwareInstallerPayload{
|
|
TitleID: titleID,
|
|
InstallerID: installerID,
|
|
Filename: "new_foo_file.ipa",
|
|
StorageID: "new_storage_id",
|
|
Version: "2.0.0",
|
|
SelfService: ptr.Bool(true),
|
|
DisplayName: ptr.String("super foo"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Delete in-house app, display name should be deleted
|
|
require.NoError(t, ds.DeleteInHouseApp(ctx, installerID))
|
|
_, err = ds.getSoftwareTitleDisplayName(ctx, 0, titleID)
|
|
require.ErrorContains(t, err, "not found")
|
|
|
|
// Delete ipadOS installer
|
|
var ipadID uint
|
|
require.NoError(t, sqlx.GetContext(ctx, ds.reader(ctx), &ipadID, `SELECT id FROM in_house_apps LIMIT 1`))
|
|
require.NoError(t, ds.DeleteInHouseApp(ctx, ipadID))
|
|
|
|
// Add ipa, regular installer, vpp with names
|
|
installerID, titleID, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
|
|
require.NoError(t, err)
|
|
err = ds.SaveInHouseAppUpdates(ctx, &fleet.UpdateSoftwareInstallerPayload{
|
|
TitleID: titleID,
|
|
InstallerID: installerID,
|
|
Filename: "foo.ipa",
|
|
Version: "1.0.0",
|
|
DisplayName: ptr.String("super foo"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
payloadPkg := fleet.UploadSoftwareInstallerPayload{
|
|
UserID: user1.ID,
|
|
Title: "bar",
|
|
Filename: "bar.pkg",
|
|
InstallScript: "echo install",
|
|
BundleIdentifier: "com.bar",
|
|
Version: "1.2.3",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
}
|
|
_, pkgTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payloadPkg)
|
|
require.NoError(t, err)
|
|
|
|
err = ds.SaveInstallerUpdates(ctx, &fleet.UpdateSoftwareInstallerPayload{
|
|
DisplayName: ptr.String("update1"),
|
|
TitleID: pkgTitleID,
|
|
InstallerFile: &fleet.TempFileReader{},
|
|
InstallScript: new(string),
|
|
PreInstallQuery: new(string),
|
|
PostInstallScript: new(string),
|
|
SelfService: ptr.Bool(false),
|
|
UninstallScript: new(string),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
test.CreateInsertGlobalVPPToken(t, ds)
|
|
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
|
|
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: "darwin"}, DisplayName: ptr.String("VPP 1")},
|
|
Name: "vpp1",
|
|
BundleIdentifier: "com.app.vpp1",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
getAllDisplayNames := func() []string {
|
|
var names []string
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
err := sqlx.SelectContext(ctx, q, &names, `SELECT display_name FROM software_title_display_names`)
|
|
require.NoError(t, err)
|
|
return nil
|
|
})
|
|
return names
|
|
}
|
|
|
|
names := getAllDisplayNames()
|
|
require.Len(t, names, 3)
|
|
|
|
// Batch set in-house apps, previous names should be deleted
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, ptr.Uint(0), []*fleet.UploadSoftwareInstallerPayload{
|
|
{
|
|
StorageID: "ipa1storageid",
|
|
Filename: "ipa1.ipa",
|
|
Title: "App Name",
|
|
Source: "ios_apps",
|
|
Version: "1.0.0",
|
|
UserID: user1.ID,
|
|
Platform: "ios",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: "com.ipa1",
|
|
SelfService: true,
|
|
DisplayName: "Yeehaw",
|
|
},
|
|
{
|
|
StorageID: "ipa1storageid",
|
|
Filename: "ipa1.ipa",
|
|
Title: "App Name",
|
|
Source: "ipados_apps",
|
|
Version: "1.0.0",
|
|
UserID: user1.ID,
|
|
Platform: "ipados",
|
|
URL: "https://example.com/1",
|
|
ValidatedLabels: &fleet.LabelIdentsWithScope{},
|
|
BundleIdentifier: "com.ipa1",
|
|
SelfService: true,
|
|
DisplayName: "Yeehaw",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
names = getAllDisplayNames()
|
|
require.Len(t, names, 4) // installer and vpp display names aren't deleted
|
|
require.NotContains(t, names, "super foo") // previous in-house display names are deleted
|
|
|
|
// Delete all in-house apps
|
|
err = ds.BatchSetInHouseAppsInstallers(ctx, ptr.Uint(0), []*fleet.UploadSoftwareInstallerPayload{})
|
|
require.NoError(t, err)
|
|
names = getAllDisplayNames()
|
|
require.Len(t, names, 2)
|
|
}
|