Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
57364cb
Add Apple managed app config validator + change Configuration field t…
jkatz01 Apr 30, 2026
6ea9e95
Simplify Apple app config validator: raw token scan via variables.Find
jkatz01 Apr 30, 2026
c62b816
Add validator tests for plist value types and ASCII control chars
jkatz01 Apr 30, 2026
aeefd52
Add VPP and in-house datastore methods for iOS/iPadOS managed app con…
jkatz01 Apr 29, 2026
213c303
Drop teamID from in-house app configuration methods
jkatz01 Apr 29, 2026
dcb4e8d
Address review feedback
jkatz01 Apr 29, 2026
4c36d7a
Address second-pass review
jkatz01 Apr 30, 2026
f2aa51b
Switch Android datastore method signatures from json.RawMessage to []…
jkatz01 Apr 30, 2026
ddbd002
Test that literal JSON null token is rejected by Apple app config val…
jkatz01 Apr 30, 2026
678de49
Wire in-house app managed configuration through datastore
jkatz01 May 1, 2026
f46e584
Drop "plist" qualifier from VPPAppTeam/VPPAppStoreApp configuration c…
jkatz01 May 1, 2026
5c47fc3
Drop "plist" qualifier from in-house app configuration comments
jkatz01 May 1, 2026
f190ee7
Wire iOS/iPadOS managed app config through the service layer
jkatz01 Apr 30, 2026
94b0800
Trim comments and fold redundant switch cases in iOS/iPadOS wiring
jkatz01 Apr 30, 2026
7614cc0
Rename decodeAppleAppConfiguration to appleConfigFromRawMessage
jkatz01 Apr 30, 2026
b44fb6f
Cover macOS silent-drop and iOS update validator in VPP config test
jkatz01 Apr 30, 2026
3cd4353
Return iOS / iPadOS VPP configuration as a JSON string, not base64
jkatz01 May 1, 2026
b36c418
Add unit tests for VPPAppStoreApp JSON marshalling
jkatz01 May 1, 2026
43be7bb
Emit Android VPP configuration as a JSON object on the wire, not base64
jkatz01 May 1, 2026
483b7fe
Drop addAppStoreAppRequest.UnmarshalJSON, inline iOS unwrap in service
jkatz01 May 1, 2026
3e473bc
Move VPP configuration response encoding from type to service layer
jkatz01 May 1, 2026
38458f6
Collapse multi-line VPP comments to single lines
jkatz01 May 1, 2026
cb14431
Wire managed app configuration through in-house app upload and update
jkatz01 May 1, 2026
619c1f8
Wire iOS / iPadOS managed app configuration through fleetctl gitops
jkatz01 May 1, 2026
d09109b
Drop "plist" qualifier from VPP/in-house service-layer comments and e…
jkatz01 May 1, 2026
d40ab82
Wire iOS / iPadOS in-house app managed configuration through fleetctl…
jkatz01 May 1, 2026
1da4c58
reject non-XML plist formats
cdcme May 7, 2026
e381178
make sure ipados gets config when one is set
cdcme May 7, 2026
f087e97
clear config when we get empty bytes
cdcme May 7, 2026
e4b6e00
add test coverage for clearing config
cdcme May 7, 2026
8b994e7
inject config dict into InstallApplication command
cdcme May 7, 2026
125ac23
lint, test fixes
cdcme May 7, 2026
d91fd79
fix assertion
cdcme May 7, 2026
4ba5258
fix gitops integration test
cdcme May 7, 2026
b03a863
fix lint
cdcme May 7, 2026
186d87d
Merge branch 'iosmac/43964-datastore-methods' into iosmac/43965-servi…
cdcme May 7, 2026
a513474
Merge branch 'iosmac/43965-service-wiring' into iosmac/43969-gitops
cdcme May 7, 2026
74006ed
Merge branch 'iosmac/43969-gitops' into iosmac/43966-config-injection
cdcme May 7, 2026
aba1d82
fix linter error
cdcme May 8, 2026
f310847
Merge branch 'iosmac/43963-validation' into iosmac/43964-datastore-me…
cdcme May 8, 2026
db4c051
Merge branch 'iosmac/43964-datastore-methods' into iosmac/43965-servi…
cdcme May 8, 2026
15e623d
Merge branch 'iosmac/43965-service-wiring' into iosmac/43969-gitops
cdcme May 8, 2026
f0f4048
Merge branch 'iosmac/43969-gitops' into iosmac/43966-config-injection
cdcme May 8, 2026
7c4258f
Strip plist wrapper before inlining Configuration; add BulkGet*Config…
cdcme May 11, 2026
106fcc1
fix tests per feedback
cdcme May 12, 2026
ee71374
merge main
cdcme May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 55 additions & 93 deletions server/datastore/mysql/activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/jmoiron/sqlx"
Expand Down Expand Up @@ -1368,57 +1369,6 @@ WHERE
id = ?
`

const insCmdStmt = `
INSERT INTO
nano_commands
(command_uuid, request_type, command, subtype)
SELECT
ua.execution_id,
'InstallApplication',
CONCAT(:raw_cmd_part1, :manifest_url, :raw_cmd_part2, ua.execution_id, :raw_cmd_part3),
:subtype
FROM
upcoming_activities ua
INNER JOIN in_house_app_upcoming_activities ihua
ON ihua.upcoming_activity_id = ua.id
WHERE
ua.host_id = :host_id AND
ua.execution_id IN (:execution_ids)
`

rawCmdPart1 := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Command</key>
<dict>
<key>InstallAsManaged</key>
<true/>
<key>ManagementFlags</key>
<integer>%d</integer>
<key>ChangeManagementState</key>
<string>Managed</string>
<key>InstallAsManaged</key>
<true />
<key>Options</key>
<dict>
<key>PurchaseMethod</key>
<integer>1</integer>
</dict>
<key>RequestType</key>
<string>InstallApplication</string>
<key>ManifestURL</key>
<string>`

const rawCmdPart2 = `</string>
</dict>
<key>CommandUUID</key>
<string>`

const rawCmdPart3 = `</string>
</dict>
</plist>`

const insNanoQueueStmt = `
INSERT INTO
nano_enrollment_queue
Expand Down Expand Up @@ -1451,15 +1401,6 @@ ORDER BY
return ctxerr.Wrap(ctx, err, "get host uuid")
}

// Set management flags based on platform
if fleet.IsAppleMobilePlatform(hostData.Platform) {
// Remove app upon MDM removal
rawCmdPart1 = fmt.Sprintf(rawCmdPart1, 1) // Mobile devices use management flag 1
} else {
// Keep app upon MDM removal
rawCmdPart1 = fmt.Sprintf(rawCmdPart1, 0) // macOS devices use management flag 0
}

// insert the host in-house app row
stmt, args, err := sqlx.In(insStmt, hostID, execIDs)
if err != nil {
Expand All @@ -1479,50 +1420,71 @@ ORDER BY
tid = *hostData.TeamID
}

// Get the title ID for the in-house app being installed
var titleID uint
getTitleIDStmt := `
// Pull the (execution_id, in_house_app_id, software_title_id) tuples for
// each pending activation so we can build a per-app InstallApplication
// command in Go and inject the managed-app-configuration dict.
const pendingStmt = `
SELECT
ihua.software_title_id
ua.execution_id,
ihua.in_house_app_id,
ihua.software_title_id
FROM
upcoming_activities ua
INNER JOIN in_house_app_upcoming_activities ihua
ON ihua.upcoming_activity_id = ua.id
upcoming_activities ua
INNER JOIN in_house_app_upcoming_activities ihua
ON ihua.upcoming_activity_id = ua.id
WHERE
ua.host_id = ? AND
ua.execution_id IN (?)
ua.host_id = ? AND ua.execution_id IN (?)
`

stmt, args, err = sqlx.In(getTitleIDStmt, hostID, execIDs)
type ihPending struct {
ExecutionID string `db:"execution_id"`
InHouseAppID uint `db:"in_house_app_id"`
SoftwareTitle uint `db:"software_title_id"`
}
stmt, args, err = sqlx.In(pendingStmt, hostID, execIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "prepare get in-house app title id")
return ctxerr.Wrap(ctx, err, "prepare pending in-house install lookup")
}

if err := sqlx.GetContext(ctx, tx, &titleID, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "get in-house app title id")
var pending []ihPending
if err := sqlx.SelectContext(ctx, tx, &pending, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "list pending in-house installs")
}

manifestURL := fmt.Sprintf("%s/api/latest/fleet/software/titles/%d/in_house_app/manifest?fleet_id=%d", appConfig.ServerSettings.ServerURL, titleID, tid)

// insert the nano command
namedArgs := map[string]any{
"manifest_url": manifestURL,
"raw_cmd_part1": rawCmdPart1,
"raw_cmd_part2": rawCmdPart2,
"raw_cmd_part3": rawCmdPart3,
"subtype": mdm.CommandSubtypeNone,
"host_id": hostID,
"execution_ids": execIDs,
if len(pending) == 0 {
return nil
}
stmt, args, err = sqlx.Named(insCmdStmt, namedArgs)
if err != nil {
return ctxerr.Wrap(ctx, err, "prepare insert nano commands")

// Bulk-fetch managed configurations for the in-house apps being installed.
// In-house Configuration is iOS/iPadOS-only; the builder drops it for
// macOS hosts anyway, but in_house_apps are always Apple-mobile so we just
// fetch unconditionally.
ids := make([]uint, 0, len(pending))
for _, p := range pending {
ids = append(ids, p.InHouseAppID)
}
stmt, args, err = sqlx.In(stmt, args...)
configsByAppID, err := ds.BulkGetInHouseAppConfigurationsTx(ctx, tx, ids)
if err != nil {
return ctxerr.Wrap(ctx, err, "expand IN arguments to insert nano commands")
return ctxerr.Wrap(ctx, err, "bulk get in-house app configurations")
}

// Build the InstallApplication plist for each pending activation, then do
// one batch INSERT into nano_commands.
insValues := make([]string, 0, len(pending))
insArgs := make([]any, 0, len(pending)*4)
for _, p := range pending {
manifestURL := fmt.Sprintf(
"%s/api/latest/fleet/software/titles/%d/in_house_app/manifest?fleet_id=%d",
appConfig.ServerSettings.ServerURL, p.SoftwareTitle, tid)
cmdBytes := apple_mdm.BuildInstallApplicationCommand(apple_mdm.InstallApplicationParams{
CommandUUID: p.ExecutionID,
HostPlatform: hostData.Platform,
ManifestURL: manifestURL,
Configuration: configsByAppID[p.InHouseAppID],
})
insValues = append(insValues, "(?, 'InstallApplication', ?, ?)")
insArgs = append(insArgs, p.ExecutionID, string(cmdBytes), mdm.CommandSubtypeNone)
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
insCmdStmt := `INSERT INTO nano_commands (command_uuid, request_type, command, subtype) VALUES ` +
strings.Join(insValues, ", ")
if _, err := tx.ExecContext(ctx, insCmdStmt, insArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "insert nano commands")
}

Expand Down
10 changes: 9 additions & 1 deletion server/datastore/mysql/in_house_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,14 @@ func (ds *Datastore) GetInHouseAppConfiguration(ctx context.Context, inHouseAppI
}

func (ds *Datastore) BulkGetInHouseAppConfigurations(ctx context.Context, inHouseAppIDs []uint) (map[uint][]byte, error) {
return ds.bulkGetInHouseAppConfigurations(ctx, ds.reader(ctx), inHouseAppIDs)
}

func (ds *Datastore) BulkGetInHouseAppConfigurationsTx(ctx context.Context, tx sqlx.QueryerContext, inHouseAppIDs []uint) (map[uint][]byte, error) {
return ds.bulkGetInHouseAppConfigurations(ctx, tx, inHouseAppIDs)
}

func (ds *Datastore) bulkGetInHouseAppConfigurations(ctx context.Context, q sqlx.QueryerContext, inHouseAppIDs []uint) (map[uint][]byte, error) {
if len(inHouseAppIDs) == 0 {
return nil, nil
}
Expand All @@ -1718,7 +1726,7 @@ WHERE in_house_app_id IN (?)
InHouseAppID uint `db:"in_house_app_id"`
Configuration []byte `db:"configuration"`
}
err = sqlx.SelectContext(ctx, ds.reader(ctx), &configs, stmt, args...)
err = sqlx.SelectContext(ctx, q, &configs, stmt, args...)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "bulk get in-house app configurations")
}
Expand Down
Loading
Loading