mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
DDMV: Support Fleet variables in DDM (#43222)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #43047 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] 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 See https://github.com/fleetdm/fleet/issues/42960#issuecomment-4244206563 and subsequent comments. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Apple DDM declarations support a vetted subset of Fleet variables with per-host substitution; premium license required. Declaration tokens and resend behavior now reflect variable changes; unresolved host substitutions mark that host’s declaration as failed. * **Bug Fixes** * Clearer errors for unsupported or license-restricted Fleet variables and more consistent DDM resend/update semantics when variables change. * **Tests** * Added extensive unit and integration tests covering Fleet variable validation, substitution, token changes, resends, and failure states. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
8d2684447c
commit
2a8803884b
32 changed files with 1312 additions and 130 deletions
1
changes/43222-support-fleet-variables-in-ddm
Normal file
1
changes/43222-support-fleet-variables-in-ddm
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Added support for Fleet variables in Apple's declaration profiles (DDM).
|
||||
|
|
@ -202,7 +202,7 @@ func TestApplyAsGitOpsDeprecatedKeys(t *testing.T) {
|
|||
ds.SetAsideLabelsFunc = func(ctx context.Context, notOnTeamID *uint, names []string, user fleet.User) error {
|
||||
return nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ func TestApplyTeamSpecs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
|
|
@ -755,7 +755,7 @@ func TestApplyAppConfig(t *testing.T) {
|
|||
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
|
||||
}
|
||||
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
|
|
@ -1486,7 +1486,7 @@ func TestApplyAsGitOps(t *testing.T) {
|
|||
ds.SetAsideLabelsFunc = func(ctx context.Context, notOnTeamID *uint, names []string, user fleet.User) error {
|
||||
return nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2610,7 +2610,7 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
|
|||
require.ElementsMatch(t, names, []string{fleet.BuiltinLabelMacOS14Plus})
|
||||
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1637,13 +1637,13 @@ func TestGitOpsFullTeam(t *testing.T) {
|
|||
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, profile fleet.MDMAppleConfigProfile, vars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
|
||||
return &profile, nil
|
||||
}
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
return declaration, nil
|
||||
}
|
||||
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
||||
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
|
|
@ -5869,7 +5869,7 @@ func TestGitOpsAppleOSUpdates(t *testing.T) {
|
|||
defaultTeamConfig = config
|
||||
return nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
return &fleet.MDMAppleDeclaration{DeclarationUUID: "test-uuid"}, nil
|
||||
}
|
||||
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
|
||||
|
|
@ -6154,7 +6154,7 @@ func TestGitOpsWindowsOSUpdates(t *testing.T) {
|
|||
defaultTeamConfig = config
|
||||
return nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
return &fleet.MDMAppleDeclaration{DeclarationUUID: "test-uuid"}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ func setupEmptyGitOpsMocks(ds *mock.Store) {
|
|||
) (fleet.MDMProfilesUpdates, error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
return &fleet.MDMAppleDeclaration{}, nil
|
||||
}
|
||||
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ func SetupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
|
|||
savedTeams[team.Name] = &team
|
||||
return team, nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (
|
||||
*fleet.MDMAppleDeclaration, error,
|
||||
) {
|
||||
declaration.DeclarationUUID = uuid.NewString()
|
||||
|
|
|
|||
|
|
@ -4222,7 +4222,7 @@ labels:
|
|||
- name: Test Fleet Label
|
||||
label_membership_type: dynamic
|
||||
query: SELECT 1
|
||||
|
||||
|
||||
`, fleetName)
|
||||
|
||||
fullFleetFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
|
|
@ -4518,3 +4518,44 @@ settings:
|
|||
require.Empty(t, teamMeta.LabelsExcludeAny)
|
||||
require.Empty(t, teamMeta.LabelsIncludeAll)
|
||||
}
|
||||
|
||||
func (s *enterpriseIntegrationGitopsTestSuite) TestFleetGitopsDDMUnsupportedFleetVariable() {
|
||||
t := s.T()
|
||||
user := s.createGitOpsUser(t)
|
||||
fleetctlConfig := s.createFleetctlConfig(t, user)
|
||||
|
||||
// Create a DDM declaration with an unsupported Fleet variable
|
||||
declDir := t.TempDir()
|
||||
declFile := path.Join(declDir, "decl-unsupported-var.json")
|
||||
err := os.WriteFile(declFile, []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Identifier": "com.example.unsupported-var",
|
||||
"Payload": {"Value": "$FLEET_VAR_BOZO"}
|
||||
}`), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
require.NoError(t, err)
|
||||
_, err = globalFile.WriteString(fmt.Sprintf(`
|
||||
agent_options:
|
||||
controls:
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
- path: %s
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_URL
|
||||
org_info:
|
||||
org_name: Fleet
|
||||
secrets:
|
||||
policies:
|
||||
queries:
|
||||
`, declFile))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Setenv("FLEET_URL", s.Server.URL)
|
||||
|
||||
// Applying a DDM declaration with an unsupported Fleet variable should fail
|
||||
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name()})
|
||||
require.ErrorContains(t, err, "Fleet variable $FLEET_VAR_BOZO is not supported in DDM profiles")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,3 +222,43 @@ func (s *integrationGitopsTestSuite) TestFleetGitopsWithFleetSecrets() {
|
|||
require.NoError(t, err)
|
||||
assert.Contains(t, string(winProfile.SyncML), "${FLEET_SECRET_"+secretName2+"}")
|
||||
}
|
||||
|
||||
func (s *integrationGitopsTestSuite) TestFleetGitopsDDMFleetVarsRequiresPremium() {
|
||||
t := s.T()
|
||||
fleetctlConfig := s.createFleetctlConfig()
|
||||
|
||||
// Create a DDM declaration with a Fleet variable
|
||||
declDir := t.TempDir()
|
||||
declFile := path.Join(declDir, "decl-fleetvar.json")
|
||||
err := os.WriteFile(declFile, []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Identifier": "com.example.fleetvar-test",
|
||||
"Payload": {"Value": "$FLEET_VAR_HOST_HARDWARE_SERIAL"}
|
||||
}`), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
|
||||
require.NoError(t, err)
|
||||
_, err = globalFile.WriteString(fmt.Sprintf(`
|
||||
agent_options:
|
||||
controls:
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
- path: %s
|
||||
org_settings:
|
||||
server_settings:
|
||||
server_url: $FLEET_URL
|
||||
org_info:
|
||||
org_name: Fleet
|
||||
secrets:
|
||||
policies:
|
||||
queries:
|
||||
`, declFile))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Setenv("FLEET_URL", s.Server.URL)
|
||||
|
||||
// Applying a DDM declaration with Fleet variables should fail without a premium license
|
||||
_, err = fleetctl.RunAppNoChecks([]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name()})
|
||||
require.ErrorContains(t, err, "missing or invalid license")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1338,7 +1338,7 @@ func (svc *Service) mdmAppleEditedAppleOSUpdates(ctx context.Context, teamID *ui
|
|||
{LabelName: labelName, LabelID: lblIDs[labelName]},
|
||||
}
|
||||
|
||||
decl, err := svc.ds.SetOrUpdateMDMAppleDeclaration(ctx, d)
|
||||
decl, err := svc.ds.SetOrUpdateMDMAppleDeclaration(ctx, d, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ func TestGetOrCreatePreassignTeam(t *testing.T) {
|
|||
require.ElementsMatch(t, names, []string{fleet.BuiltinLabelMacOS14Plus})
|
||||
return map[string]uint{names[0]: 1}, nil
|
||||
}
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ INSERT INTO
|
|||
}
|
||||
if _, err := batchSetProfileVariableAssociationsDB(ctx, tx, []fleet.MDMProfileUUIDFleetVariables{
|
||||
{ProfileUUID: profUUID, FleetVariables: usesFleetVars},
|
||||
}, "darwin"); err != nil {
|
||||
}, "darwin", false); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting darwin profile variable associations")
|
||||
}
|
||||
|
||||
|
|
@ -5103,7 +5103,7 @@ WHERE h.uuid = ? AND (
|
|||
}
|
||||
|
||||
func (ds *Datastore) batchSetMDMAppleDeclarations(ctx context.Context, tx sqlx.ExtContext, tmID *uint,
|
||||
incomingDeclarations []*fleet.MDMAppleDeclaration,
|
||||
incomingDeclarations []*fleet.MDMAppleDeclaration, profilesVariablesByIdentifier []fleet.MDMProfileIdentifierFleetVariables,
|
||||
) (updatedDB bool, err error) {
|
||||
// First, build a list of names (which are usually filenames) for the incoming declarations.
|
||||
// We will keep the existing ones if there's a match and no change.
|
||||
|
|
@ -5151,7 +5151,12 @@ func (ds *Datastore) batchSetMDMAppleDeclarations(ctx context.Context, tx sqlx.E
|
|||
return false, ctxerr.Wrap(ctx, err, "update declaration label associations")
|
||||
}
|
||||
|
||||
return deletedDeclarations || insertedOrUpdatedDeclarations || updatedLabels, nil
|
||||
updatedVars, err := ds.updateDeclarationsVariableAssociations(ctx, tx, incomingDeclarationsMap, teamIDOrZero, profilesVariablesByIdentifier)
|
||||
if err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "update declaration variable associations")
|
||||
}
|
||||
|
||||
return deletedDeclarations || insertedOrUpdatedDeclarations || updatedLabels || updatedVars, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) updateDeclarationsLabelAssociations(ctx context.Context, tx sqlx.ExtContext,
|
||||
|
|
@ -5219,6 +5224,53 @@ func (ds *Datastore) updateDeclarationsLabelAssociations(ctx context.Context, tx
|
|||
return updatedDB, err
|
||||
}
|
||||
|
||||
func (ds *Datastore) updateDeclarationsVariableAssociations(ctx context.Context, tx sqlx.ExtContext,
|
||||
incomingDeclarationsMap map[string]*fleet.MDMAppleDeclaration, teamID uint,
|
||||
profilesVariablesByIdentifier []fleet.MDMProfileIdentifierFleetVariables,
|
||||
) (updatedDB bool, err error) {
|
||||
if len(incomingDeclarationsMap) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
incomingNames := make([]string, 0, len(incomingDeclarationsMap))
|
||||
for _, p := range incomingDeclarationsMap {
|
||||
incomingNames = append(incomingNames, p.Name)
|
||||
}
|
||||
|
||||
// Load declaration UUIDs by name (same approach as updateDeclarationsLabelAssociations)
|
||||
currentDecls, err := ds.getExistingDeclarations(ctx, tx, incomingNames, teamID)
|
||||
if err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "load declarations for variable associations")
|
||||
}
|
||||
|
||||
// Build a map from identifier to variables, filtering for declaration entries only
|
||||
varsByIdentifier := make(map[string][]fleet.FleetVarName, len(profilesVariablesByIdentifier))
|
||||
for _, pv := range profilesVariablesByIdentifier {
|
||||
if name, ok := strings.CutPrefix(pv.Identifier, fleet.MDMAppleDeclarationUUIDPrefix); ok {
|
||||
varsByIdentifier[name] = pv.FleetVariables
|
||||
}
|
||||
}
|
||||
|
||||
// Map declaration UUIDs to their variables, including declarations without
|
||||
// variables so stale associations get cleared.
|
||||
var profilesVarsToUpsert []fleet.MDMProfileUUIDFleetVariables
|
||||
for _, decl := range currentDecls {
|
||||
vars := varsByIdentifier[decl.Identifier]
|
||||
profilesVarsToUpsert = append(profilesVarsToUpsert, fleet.MDMProfileUUIDFleetVariables{
|
||||
ProfileUUID: decl.DeclarationUUID,
|
||||
FleetVariables: vars,
|
||||
})
|
||||
}
|
||||
|
||||
if len(profilesVarsToUpsert) > 0 {
|
||||
if updatedDB, err = batchSetProfileVariableAssociationsDB(ctx, tx, profilesVarsToUpsert, "darwin", true); err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "inserting declaration variable associations")
|
||||
}
|
||||
}
|
||||
|
||||
return updatedDB, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) insertOrUpdateDeclarations(ctx context.Context, tx sqlx.ExtContext, incomingDeclarations []*fleet.MDMAppleDeclaration,
|
||||
teamID uint,
|
||||
) (updatedDB bool, err error) {
|
||||
|
|
@ -5310,6 +5362,7 @@ func (ds *Datastore) getExistingDeclarations(ctx context.Context, tx sqlx.ExtCon
|
|||
const loadExistingDecls = `
|
||||
SELECT
|
||||
name,
|
||||
identifier,
|
||||
declaration_uuid,
|
||||
raw_json
|
||||
FROM
|
||||
|
|
@ -5340,7 +5393,7 @@ func (ds *Datastore) teamIDPtrToUint(tmID *uint) uint {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (ds *Datastore) NewMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (ds *Datastore) NewMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
const stmt = `
|
||||
INSERT INTO mdm_apple_declarations (
|
||||
declaration_uuid,
|
||||
|
|
@ -5360,10 +5413,10 @@ INSERT INTO mdm_apple_declarations (
|
|||
)
|
||||
)`
|
||||
|
||||
return ds.insertOrUpsertMDMAppleDeclaration(ctx, stmt, declaration)
|
||||
return ds.insertOrUpsertMDMAppleDeclaration(ctx, stmt, declaration, usesFleetVars)
|
||||
}
|
||||
|
||||
func (ds *Datastore) SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (ds *Datastore) SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
const stmt = `
|
||||
INSERT INTO mdm_apple_declarations (
|
||||
declaration_uuid,
|
||||
|
|
@ -5387,10 +5440,10 @@ ON DUPLICATE KEY UPDATE
|
|||
uploaded_at = IF(raw_json = VALUES(raw_json) AND name = VALUES(name) AND IFNULL(secrets_updated_at = VALUES(secrets_updated_at), TRUE), uploaded_at, NOW(6)),
|
||||
raw_json = VALUES(raw_json)`
|
||||
|
||||
return ds.insertOrUpsertMDMAppleDeclaration(ctx, stmt, declaration)
|
||||
return ds.insertOrUpsertMDMAppleDeclaration(ctx, stmt, declaration, usesFleetVars)
|
||||
}
|
||||
|
||||
func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insOrUpsertStmt string, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insOrUpsertStmt string, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
declUUID := fleet.MDMAppleDeclarationUUIDPrefix + uuid.NewString()
|
||||
|
||||
var tmID uint
|
||||
|
|
@ -5455,6 +5508,12 @@ func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insO
|
|||
return ctxerr.Wrap(ctx, err, "inserting mdm declaration label associations")
|
||||
}
|
||||
|
||||
if _, err := batchSetProfileVariableAssociationsDB(ctx, tx, []fleet.MDMProfileUUIDFleetVariables{
|
||||
{ProfileUUID: declUUID, FleetVariables: usesFleetVars},
|
||||
}, "darwin", true); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting declaration variable associations")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -5613,7 +5672,7 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont
|
|||
func (ds *Datastore) MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) {
|
||||
const stmt = `
|
||||
SELECT
|
||||
COALESCE(MD5(CONCAT(COUNT(0), GROUP_CONCAT(HEX(mad.token)
|
||||
COALESCE(MD5(CONCAT(COUNT(0), GROUP_CONCAT(CONCAT(HEX(mad.token), IFNULL(hmad.variables_updated_at, ''))
|
||||
ORDER BY
|
||||
mad.uploaded_at DESC, mad.declaration_uuid ASC separator ''))), '') AS token,
|
||||
COALESCE(MAX(mad.created_at), NOW()) AS latest_created_timestamp
|
||||
|
|
@ -5643,7 +5702,8 @@ func (ds *Datastore) MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID s
|
|||
const stmt = `
|
||||
SELECT
|
||||
HEX(mad.token) as token,
|
||||
mad.identifier, mad.declaration_uuid, status, operation_type, mad.uploaded_at
|
||||
mad.identifier, mad.declaration_uuid, status, operation_type, mad.uploaded_at,
|
||||
hmad.variables_updated_at
|
||||
FROM
|
||||
host_mdm_apple_declarations hmad
|
||||
JOIN mdm_apple_declarations mad ON mad.declaration_uuid = hmad.declaration_uuid
|
||||
|
|
@ -5665,7 +5725,7 @@ func (ds *Datastore) MDMAppleDDMDeclarationsResponse(ctx context.Context, identi
|
|||
// declarations are removed, but the join would provide an extra layer of safety.
|
||||
const stmt = `
|
||||
SELECT
|
||||
mad.raw_json, HEX(mad.token) as token
|
||||
mad.declaration_uuid, mad.raw_json, HEX(mad.token) as token, hmad.variables_updated_at
|
||||
FROM
|
||||
host_mdm_apple_declarations hmad
|
||||
JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid
|
||||
|
|
@ -5783,14 +5843,15 @@ func mdmAppleBatchSetPendingHostDeclarationsDB(
|
|||
) (updatedDB bool, err error) {
|
||||
baseStmt := `
|
||||
INSERT INTO host_mdm_apple_declarations
|
||||
(host_uuid, status, operation_type, token, secrets_updated_at, declaration_uuid, declaration_identifier, declaration_name)
|
||||
(host_uuid, status, operation_type, token, secrets_updated_at, variables_updated_at, declaration_uuid, declaration_identifier, declaration_name)
|
||||
VALUES
|
||||
%s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
operation_type = VALUES(operation_type),
|
||||
token = VALUES(token),
|
||||
secrets_updated_at = VALUES(secrets_updated_at)
|
||||
secrets_updated_at = VALUES(secrets_updated_at),
|
||||
variables_updated_at = VALUES(variables_updated_at)
|
||||
`
|
||||
|
||||
profilesToInsert := make(map[string]*fleet.MDMAppleHostDeclaration)
|
||||
|
|
@ -5806,6 +5867,7 @@ func mdmAppleBatchSetPendingHostDeclarationsDB(
|
|||
COALESCE(detail, '') AS detail,
|
||||
token,
|
||||
secrets_updated_at,
|
||||
variables_updated_at,
|
||||
declaration_uuid,
|
||||
declaration_identifier,
|
||||
declaration_name
|
||||
|
|
@ -5859,18 +5921,19 @@ func mdmAppleBatchSetPendingHostDeclarationsDB(
|
|||
|
||||
generateValueArgs := func(d *fleet.MDMAppleHostDeclaration) (string, []any) {
|
||||
profilesToInsert[fmt.Sprintf("%s\n%s", d.HostUUID, d.DeclarationUUID)] = &fleet.MDMAppleHostDeclaration{
|
||||
HostUUID: d.HostUUID,
|
||||
DeclarationUUID: d.DeclarationUUID,
|
||||
Name: d.Name,
|
||||
Identifier: d.Identifier,
|
||||
Status: status,
|
||||
OperationType: d.OperationType,
|
||||
Detail: d.Detail,
|
||||
Token: d.Token,
|
||||
SecretsUpdatedAt: d.SecretsUpdatedAt,
|
||||
HostUUID: d.HostUUID,
|
||||
DeclarationUUID: d.DeclarationUUID,
|
||||
Name: d.Name,
|
||||
Identifier: d.Identifier,
|
||||
Status: status,
|
||||
OperationType: d.OperationType,
|
||||
Detail: d.Detail,
|
||||
Token: d.Token,
|
||||
SecretsUpdatedAt: d.SecretsUpdatedAt,
|
||||
VariablesUpdatedAt: d.VariablesUpdatedAt,
|
||||
}
|
||||
valuePart := "(?, ?, ?, ?, ?, ?, ?, ?),"
|
||||
args := []any{d.HostUUID, status, d.OperationType, d.Token, d.SecretsUpdatedAt, d.DeclarationUUID, d.Identifier, d.Name}
|
||||
valuePart := "(?, ?, ?, ?, ?, ?, ?, ?, ?),"
|
||||
args := []any{d.HostUUID, status, d.OperationType, d.Token, d.SecretsUpdatedAt, d.VariablesUpdatedAt, d.DeclarationUUID, d.Identifier, d.Name}
|
||||
return valuePart, args
|
||||
}
|
||||
|
||||
|
|
@ -6000,13 +6063,65 @@ func mdmAppleGetHostsWithChangedDeclarationsDB(ctx context.Context, tx sqlx.ExtC
|
|||
if err := sqlx.SelectContext(ctx, tx, &decls, stmt, args...); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "running sql statement")
|
||||
}
|
||||
|
||||
// For declarations that use Fleet variables and are being installed, set
|
||||
// VariablesUpdatedAt so the host row tracks when variable values were last
|
||||
// computed. This allows re-delivery when variable values change.
|
||||
if err := setVariablesUpdatedAtForDeclarations(ctx, tx, decls); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "setting variables_updated_at for declarations")
|
||||
}
|
||||
|
||||
return decls, nil
|
||||
}
|
||||
|
||||
// setVariablesUpdatedAtForDeclarations looks up which declarations have Fleet
|
||||
// variables and sets VariablesUpdatedAt on the install entries.
|
||||
func setVariablesUpdatedAtForDeclarations(ctx context.Context, tx sqlx.ExtContext, decls []*fleet.MDMAppleHostDeclaration) error {
|
||||
var installDeclUUIDs []string
|
||||
for _, d := range decls {
|
||||
if d.OperationType == fleet.MDMOperationTypeInstall {
|
||||
installDeclUUIDs = append(installDeclUUIDs, d.DeclarationUUID)
|
||||
}
|
||||
}
|
||||
if len(installDeclUUIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stmt, args, err := sqlx.In(
|
||||
`SELECT DISTINCT apple_declaration_uuid FROM mdm_configuration_profile_variables WHERE apple_declaration_uuid IN (?)`,
|
||||
installDeclUUIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "sqlx.In declaration variables lookup")
|
||||
}
|
||||
|
||||
var declsWithVars []string
|
||||
if err := sqlx.SelectContext(ctx, tx, &declsWithVars, stmt, args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "selecting declarations with variables")
|
||||
}
|
||||
|
||||
if len(declsWithVars) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
hasVars := make(map[string]struct{}, len(declsWithVars))
|
||||
for _, uuid := range declsWithVars {
|
||||
hasVars[uuid] = struct{}{}
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
for _, d := range decls {
|
||||
if _, ok := hasVars[d.DeclarationUUID]; ok && d.OperationType == fleet.MDMOperationTypeInstall {
|
||||
d.VariablesUpdatedAt = &now
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MDMAppleStoreDDMStatusReport updates the status of the host's declarations.
|
||||
func (ds *Datastore) MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error {
|
||||
getHostDeclarationsStmt := `
|
||||
SELECT host_uuid, status, operation_type, HEX(token) as token, secrets_updated_at, declaration_uuid, declaration_identifier, declaration_name
|
||||
SELECT host_uuid, status, operation_type, HEX(token) as token, secrets_updated_at, variables_updated_at, declaration_uuid, declaration_identifier, declaration_name
|
||||
FROM host_mdm_apple_declarations
|
||||
WHERE host_uuid = ?
|
||||
`
|
||||
|
|
@ -6042,7 +6157,7 @@ ON DUPLICATE KEY UPDATE
|
|||
for _, c := range current {
|
||||
// Skip updates for 'remove' operations because it is possible that IT admin removed a profile and then re-added it.
|
||||
// Pending removes are cleaned up after we update status of installs.
|
||||
if u, ok := updatesByToken[c.Token]; ok && u.OperationType != fleet.MDMOperationTypeRemove {
|
||||
if u, ok := updatesByToken[fleet.EffectiveDDMToken(c.Token, c.VariablesUpdatedAt)]; ok && u.OperationType != fleet.MDMOperationTypeRemove {
|
||||
insertVals.WriteString("(?, ?, ?, ?, ?, ?, ?, UNHEX(?), ?),")
|
||||
args = append(args, hostUUID, c.DeclarationUUID, u.Status, u.OperationType, u.Detail, c.Identifier, c.Name, c.Token,
|
||||
c.SecretsUpdatedAt)
|
||||
|
|
@ -6069,6 +6184,12 @@ ON DUPLICATE KEY UPDATE
|
|||
return ctxerr.Wrap(ctx, err, "updating host declarations")
|
||||
}
|
||||
|
||||
func (ds *Datastore) SetHostMDMAppleDeclarationStatus(ctx context.Context, hostUUID string, declarationUUID string, status *fleet.MDMDeliveryStatus, detail string, variablesUpdatedAt *time.Time) error {
|
||||
stmt := `UPDATE host_mdm_apple_declarations SET status = ?, detail = ?, variables_updated_at = COALESCE(?, variables_updated_at) WHERE host_uuid = ? AND declaration_uuid = ?`
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, stmt, status, detail, variablesUpdatedAt, hostUUID, declarationUUID)
|
||||
return ctxerr.Wrap(ctx, err, "set host declaration status")
|
||||
}
|
||||
|
||||
func (ds *Datastore) MDMAppleSetPendingDeclarationsAs(ctx context.Context, hostUUID string, status *fleet.MDMDeliveryStatus, detail string) error {
|
||||
stmt := `
|
||||
UPDATE host_mdm_apple_declarations
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ func testMDMAppleBatchSetHostDeclarationState(t *testing.T, ds *Datastore) {
|
|||
Name: "Test Declaration " + string(rune('A'+i)),
|
||||
Identifier: "com.example.test.declaration." + string(rune('A'+i)),
|
||||
RawJSON: []byte(`{"Type":"com.apple.test.declaration","Identifier":"com.example.test.declaration.` + string(rune('A'+i)) + `"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
removeDeclarations := make([]*fleet.MDMAppleDeclaration, 3)
|
||||
|
|
@ -189,7 +189,7 @@ func testMDMAppleBatchSetHostDeclarationState(t *testing.T, ds *Datastore) {
|
|||
Name: "Test Install Declaration " + string(rune('A'+i)),
|
||||
Identifier: "com.example.test.install." + string(rune('A'+i)),
|
||||
RawJSON: []byte(`{"Type":"com.apple.test.declaration","Identifier":"com.example.test.install.` + string(rune('A'+i)) + `"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ func testDeleteMDMAppleConfigProfile(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
testDecl := declForTest("D1", "D1", "{}")
|
||||
dbDecl, err := ds.NewMDMAppleDeclaration(ctx, testDecl)
|
||||
dbDecl, err := ds.NewMDMAppleDeclaration(ctx, testDecl, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// delete for a non-existing team does nothing
|
||||
|
|
@ -5966,7 +5966,7 @@ func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) {
|
|||
Identifier: "decl-1",
|
||||
Name: "decl-1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "decl-1"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
updates, err := ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -6005,7 +6005,7 @@ func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) {
|
|||
Identifier: "decl-2",
|
||||
Name: "decl-2",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "decl-2"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -6043,7 +6043,7 @@ func testMDMAppleSetPendingDeclarationsAs(t *testing.T, ds *Datastore) {
|
|||
Identifier: fmt.Sprintf("decl-%d", i),
|
||||
Name: fmt.Sprintf("decl-%d", i),
|
||||
RawJSON: json.RawMessage(fmt.Sprintf(`{"Identifier": "decl-%d"}`, i)),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -6094,7 +6094,7 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
Identifier: "i1",
|
||||
Name: "d1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// try to create same name, different identifier fails
|
||||
|
|
@ -6102,7 +6102,7 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
Identifier: "i1b",
|
||||
Name: "d1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.Error(t, err)
|
||||
var existsErr *existsError
|
||||
require.ErrorAs(t, err, &existsErr)
|
||||
|
|
@ -6112,7 +6112,7 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
Identifier: "i1",
|
||||
Name: "d1b",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorAs(t, err, &existsErr)
|
||||
|
||||
|
|
@ -6122,7 +6122,7 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
Name: "d1",
|
||||
TeamID: &tm1.ID,
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, d1.DeclarationUUID, d1tm1.DeclarationUUID)
|
||||
|
||||
|
|
@ -6136,7 +6136,7 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
Name: "d1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}},
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, d1.DeclarationUUID, d1Ori.DeclarationUUID)
|
||||
require.NotEqual(t, d1.DeclarationUUID, d1tm1.DeclarationUUID)
|
||||
|
|
@ -6152,7 +6152,7 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
Name: "d1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l2.Name, LabelID: l2.ID}},
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, d1.DeclarationUUID, d1Ori.DeclarationUUID)
|
||||
|
||||
|
|
@ -6168,7 +6168,7 @@ func testSetOrUpdateMDMAppleDDMDeclaration(t *testing.T, ds *Datastore) {
|
|||
TeamID: &tm1.ID,
|
||||
RawJSON: json.RawMessage(`{"Identifier": "i1b"}`),
|
||||
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{LabelName: l1.Name, LabelID: l1.ID}},
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, d1tm1B.DeclarationUUID, d1tm1.DeclarationUUID)
|
||||
|
||||
|
|
@ -6197,7 +6197,7 @@ func testDeleteMDMAppleDeclarationWithPendingInstalls(t *testing.T, ds *Datastor
|
|||
Identifier: "decl-1",
|
||||
Name: "decl-1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "decl-1"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
|
|
@ -9709,8 +9709,8 @@ func testSetMDMAppleProfilesWithVariables(t *testing.T, ds *Datastore) {
|
|||
&profA,
|
||||
&profB,
|
||||
}, nil, nil, nil, []fleet.MDMProfileIdentifierFleetVariables{
|
||||
{Identifier: profA.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsernameLocalPart}},
|
||||
{Identifier: profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername}},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profA.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsernameLocalPart}},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.MDMProfilesUpdates{AppleConfigProfile: true}, updates)
|
||||
|
|
@ -9727,9 +9727,9 @@ func testSetMDMAppleProfilesWithVariables(t *testing.T, ds *Datastore) {
|
|||
&profB,
|
||||
&profD,
|
||||
}, nil, nil, nil, []fleet.MDMProfileIdentifierFleetVariables{
|
||||
{Identifier: profA.Identifier, FleetVariables: nil},
|
||||
{Identifier: profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}},
|
||||
{Identifier: profD.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profA.Identifier, FleetVariables: nil},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profD.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.MDMProfilesUpdates{AppleConfigProfile: true}, updates)
|
||||
|
|
@ -9754,9 +9754,9 @@ func testSetMDMAppleProfilesWithVariables(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
nil,
|
||||
[]fleet.MDMProfileIdentifierFleetVariables{
|
||||
{Identifier: profA.Identifier, FleetVariables: nil},
|
||||
{Identifier: profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}},
|
||||
{Identifier: profD.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profA.Identifier, FleetVariables: nil},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profB.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profD.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.MDMProfilesUpdates{AppleConfigProfile: true, AppleDeclaration: true, WindowsConfigProfile: true}, updates)
|
||||
|
|
@ -9769,7 +9769,7 @@ func testSetMDMAppleProfilesWithVariables(t *testing.T, ds *Datastore) {
|
|||
updates, err = ds.BatchSetMDMProfiles(ctx, &tm1.ID, []*fleet.MDMAppleConfigProfile{
|
||||
&profE,
|
||||
}, nil, nil, nil, []fleet.MDMProfileIdentifierFleetVariables{
|
||||
{Identifier: profE.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}},
|
||||
{Identifier: fleet.MDMAppleProfileUUIDPrefix + profE.Identifier, FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.MDMProfilesUpdates{AppleConfigProfile: true}, updates)
|
||||
|
|
|
|||
|
|
@ -4974,7 +4974,7 @@ func testListHostsProfileUUIDAndStatus(t *testing.T, ds *Datastore) {
|
|||
// no team Apple declaration profile //
|
||||
/////////////////////////////////////
|
||||
|
||||
noTeamDeclaration, err := ds.NewMDMAppleDeclaration(ctx, declForTest("test-decleration", "com.fleetdm.fleet.mdm.test-decl", "{}"))
|
||||
noTeamDeclaration, err := ds.NewMDMAppleDeclaration(ctx, declForTest("test-decleration", "com.fleetdm.fleet.mdm.test-decl", "{}"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verified status
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macPro
|
|||
return ctxerr.Wrap(ctx, err, "batch set apple profiles")
|
||||
}
|
||||
|
||||
if updates.AppleDeclaration, err = ds.batchSetMDMAppleDeclarations(ctx, tx, tmID, macDeclarations); err != nil {
|
||||
if updates.AppleDeclaration, err = ds.batchSetMDMAppleDeclarations(ctx, tx, tmID, macDeclarations, profilesVariablesByIdentifier); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "batch set apple declarations")
|
||||
}
|
||||
|
||||
|
|
@ -2021,18 +2021,21 @@ func batchSetProfileVariableAssociationsDB(
|
|||
tx sqlx.ExtContext,
|
||||
profileVariablesByUUID []fleet.MDMProfileUUIDFleetVariables,
|
||||
platform string,
|
||||
forAppleDeclarations bool,
|
||||
) (didUpdate bool, err error) {
|
||||
if len(profileVariablesByUUID) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var platformPrefix string
|
||||
switch platform {
|
||||
case "darwin":
|
||||
platformPrefix = "apple"
|
||||
case "windows":
|
||||
platformPrefix = "windows"
|
||||
case "android":
|
||||
var columnName string
|
||||
switch {
|
||||
case platform == "darwin" && forAppleDeclarations:
|
||||
columnName = "apple_declaration_uuid"
|
||||
case platform == "darwin":
|
||||
columnName = "apple_profile_uuid"
|
||||
case platform == "windows":
|
||||
columnName = "windows_profile_uuid"
|
||||
case platform == "android":
|
||||
return false, nil // Early return here, to avoid failing but still utilizing the shared batchSet method.
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported platform %s", platform)
|
||||
|
|
@ -2050,7 +2053,7 @@ func batchSetProfileVariableAssociationsDB(
|
|||
}
|
||||
|
||||
// delete variables associated with those profiles
|
||||
clearVarsForProfilesStmt := fmt.Sprintf(`DELETE FROM mdm_configuration_profile_variables WHERE %s_profile_uuid IN (?)`, platformPrefix)
|
||||
clearVarsForProfilesStmt := fmt.Sprintf(`DELETE FROM mdm_configuration_profile_variables WHERE %s IN (?)`, columnName)
|
||||
clearVarsForProfilesStmt, args, err := sqlx.In(clearVarsForProfilesStmt, profileUUIDsToDelete)
|
||||
if err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "sqlx.In delete variables for profiles")
|
||||
|
|
@ -2117,13 +2120,13 @@ func batchSetProfileVariableAssociationsDB(
|
|||
executeUpsertBatch := func(valuePart string, args []any) error {
|
||||
stmt := fmt.Sprintf(`
|
||||
INSERT INTO mdm_configuration_profile_variables (
|
||||
%s_profile_uuid,
|
||||
%s,
|
||||
fleet_variable_id
|
||||
)
|
||||
VALUES %s
|
||||
ON DUPLICATE KEY UPDATE
|
||||
fleet_variable_id = VALUES(fleet_variable_id)
|
||||
`, platformPrefix, strings.TrimSuffix(valuePart, ","))
|
||||
`, columnName, strings.TrimSuffix(valuePart, ","))
|
||||
|
||||
_, err := tx.ExecContext(ctx, stmt, args...)
|
||||
return err
|
||||
|
|
@ -2574,12 +2577,23 @@ func (ds *Datastore) batchSetLabelAndVariableAssociations(ctx context.Context, t
|
|||
return false, ctxerr.Wrap(ctx, err, fmt.Sprintf("inserting %s profile label associations", platform))
|
||||
}
|
||||
|
||||
// save fleet variables associated with Windows profiles (both new and updated)
|
||||
// save fleet variables associated with profiles (both new and updated)
|
||||
// Note: currentProfiles contains all incoming profiles (new AND updated), not just new ones
|
||||
// Process ALL profiles to ensure stale variable associations are cleared for profiles that no longer have variables
|
||||
var varPrefix string
|
||||
switch platform {
|
||||
case "darwin":
|
||||
varPrefix = fleet.MDMAppleProfileUUIDPrefix
|
||||
case "windows":
|
||||
varPrefix = fleet.MDMWindowsProfileUUIDPrefix
|
||||
case "android":
|
||||
varPrefix = fleet.MDMAndroidProfileUUIDPrefix
|
||||
}
|
||||
profileVariablesByName := make(map[string][]fleet.FleetVarName, len(profilesVariablesByIdentifier))
|
||||
for _, pv := range profilesVariablesByIdentifier {
|
||||
profileVariablesByName[pv.Identifier] = pv.FleetVariables
|
||||
if name, ok := strings.CutPrefix(pv.Identifier, varPrefix); ok {
|
||||
profileVariablesByName[name] = pv.FleetVariables
|
||||
}
|
||||
}
|
||||
|
||||
// collect ALL profile UUIDs, including those without variables (to clear stale associations)
|
||||
|
|
@ -2595,7 +2609,7 @@ func (ds *Datastore) batchSetLabelAndVariableAssociations(ctx context.Context, t
|
|||
|
||||
if len(profilesVarsToUpsert) > 0 {
|
||||
var didUpdateVariableAssociations bool
|
||||
if didUpdateVariableAssociations, err = batchSetProfileVariableAssociationsDB(ctx, tx, profilesVarsToUpsert, platform); err != nil {
|
||||
if didUpdateVariableAssociations, err = batchSetProfileVariableAssociationsDB(ctx, tx, profilesVarsToUpsert, platform, false); err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, fmt.Sprintf("inserting %s profile variable associations", platform))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2655,7 +2655,7 @@ INSERT INTO
|
|||
FleetVariables: usesFleetVars,
|
||||
},
|
||||
}
|
||||
if _, err := batchSetProfileVariableAssociationsDB(ctx, tx, profilesVarsToUpsert, "windows"); err != nil {
|
||||
if _, err := batchSetProfileVariableAssociationsDB(ctx, tx, profilesVarsToUpsert, "windows", false); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting windows profile variable associations")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2648,7 +2648,7 @@ func testMDMWindowsConfigProfilesWithFleetVars(t *testing.T, ds *Datastore) {
|
|||
|
||||
// Mock the profilesVariablesByIdentifier that would be passed from service layer
|
||||
profilesVars := []fleet.MDMProfileIdentifierFleetVariables{
|
||||
{Identifier: "team_profile_1", FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostUUID}},
|
||||
{Identifier: fleet.MDMWindowsProfileUUIDPrefix + "team_profile_1", FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostUUID}},
|
||||
}
|
||||
|
||||
_, err = ds.BatchSetMDMProfiles(ctx, ptr.Uint(1), nil, []*fleet.MDMWindowsConfigProfile{teamProf1WithVarsAgain, teamProf2NoChange}, nil, nil, profilesVars)
|
||||
|
|
@ -3680,7 +3680,7 @@ func testSetMDMWindowsProfilesWithVariables(t *testing.T, ds *Datastore) {
|
|||
_, err := batchSetProfileVariableAssociationsDB(ctx, ds.writer(ctx), []fleet.MDMProfileUUIDFleetVariables{
|
||||
{ProfileUUID: globalProfiles[0], FleetVariables: nil},
|
||||
{ProfileUUID: globalProfiles[1], FleetVariables: nil},
|
||||
}, "windows")
|
||||
}, "windows", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkProfileVariables(globalProfiles[0], 0, nil)
|
||||
|
|
@ -3690,7 +3690,7 @@ func testSetMDMWindowsProfilesWithVariables(t *testing.T, ds *Datastore) {
|
|||
_, err = batchSetProfileVariableAssociationsDB(ctx, ds.writer(ctx), []fleet.MDMProfileUUIDFleetVariables{
|
||||
{ProfileUUID: globalProfiles[0], FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarName(string(fleet.FleetVarDigiCertDataPrefix) + "ZZZ")}},
|
||||
{ProfileUUID: globalProfiles[1], FleetVariables: []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups}},
|
||||
}, "windows")
|
||||
}, "windows", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
checkProfileVariables(globalProfiles[0], 0, []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarDigiCertDataPrefix})
|
||||
|
|
|
|||
|
|
@ -1317,12 +1317,33 @@ func triggerResendProfilesUsingVariables(ctx context.Context, tx sqlx.ExtContext
|
|||
fv.name IN (:affected_vars)
|
||||
`
|
||||
|
||||
const declarationUpdateStatusQuery = `
|
||||
UPDATE
|
||||
host_mdm_apple_declarations hmad
|
||||
JOIN hosts h
|
||||
ON h.uuid = hmad.host_uuid
|
||||
JOIN mdm_apple_declarations mad
|
||||
ON (mad.team_id = h.team_id OR (COALESCE(mad.team_id, 0) = 0 AND h.team_id IS NULL)) AND
|
||||
mad.declaration_uuid = hmad.declaration_uuid
|
||||
JOIN mdm_configuration_profile_variables mcpv
|
||||
ON mcpv.apple_declaration_uuid = mad.declaration_uuid
|
||||
JOIN fleet_variables fv
|
||||
ON mcpv.fleet_variable_id = fv.id
|
||||
SET
|
||||
hmad.status = NULL
|
||||
WHERE
|
||||
h.id IN (:host_ids) AND
|
||||
hmad.operation_type = :operation_type_install AND
|
||||
hmad.status IS NOT NULL AND
|
||||
fv.name IN (:affected_vars)
|
||||
`
|
||||
|
||||
vars := make([]any, len(affectedVars))
|
||||
for i, v := range affectedVars {
|
||||
vars[i] = "FLEET_VAR_" + string(v)
|
||||
}
|
||||
|
||||
for _, query := range []string{appleUpdateStatusQuery, windowsUpdateStatusQuery} {
|
||||
for _, query := range []string{appleUpdateStatusQuery, windowsUpdateStatusQuery, declarationUpdateStatusQuery} {
|
||||
updateStmt, args, err := sqlx.Named(query, map[string]any{
|
||||
"host_ids": hostIDs,
|
||||
"operation_type_install": fleet.MDMOperationTypeInstall,
|
||||
|
|
|
|||
|
|
@ -618,7 +618,7 @@ func testDeleteUsedSecretVariable(t *testing.T, ds *Datastore) {
|
|||
Identifier: "decl-1",
|
||||
Name: "decl-1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "${FLEET_SECRET_FOOBAR}"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Attempt to delete the variable, should fail.
|
||||
|
|
@ -640,7 +640,7 @@ func testDeleteUsedSecretVariable(t *testing.T, ds *Datastore) {
|
|||
Name: "decl-1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "${FLEET_SECRET_FOOBAR}"}`),
|
||||
TeamID: &foobarTeam.ID,
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Attempt to delete the variable, should fail.
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ func testTeamsGetSetDelete(t *testing.T, ds *Datastore) {
|
|||
Name: "decl-1",
|
||||
TeamID: &team.ID,
|
||||
RawJSON: json.RawMessage(`{"Type": "com.apple.configuration.test", "Identifier": "decl-1"}`),
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
teamLabel, err := ds.NewLabel(t.Context(), &fleet.Label{
|
||||
|
|
|
|||
|
|
@ -720,9 +720,25 @@ type MDMAppleDeclaration struct {
|
|||
LabelsIncludeAny []ConfigurationProfileLabel `db:"-" json:"labels_include_any,omitempty"`
|
||||
LabelsExcludeAny []ConfigurationProfileLabel `db:"-" json:"labels_exclude_any,omitempty"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
|
||||
SecretsUpdatedAt *time.Time `db:"secrets_updated_at" json:"-"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
|
||||
SecretsUpdatedAt *time.Time `db:"secrets_updated_at" json:"-"`
|
||||
VariablesUpdatedAt *time.Time `db:"variables_updated_at" json:"-"`
|
||||
}
|
||||
|
||||
// EffectiveDDMToken computes the per-declaration token that incorporates both
|
||||
// the static content hash and the host-specific variables_updated_at timestamp.
|
||||
// When variablesUpdatedAt is nil (declaration has no Fleet variables), the
|
||||
// effective token equals the static token unchanged.
|
||||
func EffectiveDDMToken(staticToken string, variablesUpdatedAt *time.Time) string {
|
||||
if variablesUpdatedAt == nil {
|
||||
return staticToken
|
||||
}
|
||||
// Must match MySQL's DATETIME(6) string representation used in
|
||||
// MDMAppleDDMDeclarationsToken's IFNULL(hmad.variables_updated_at, '').
|
||||
hasher := md5.New() // nolint:gosec // used for declarative management token
|
||||
hasher.Write([]byte(staticToken + variablesUpdatedAt.Format("2006-01-02 15:04:05.000000")))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
type MDMAppleRawDeclaration struct {
|
||||
|
|
@ -815,11 +831,16 @@ type MDMAppleHostDeclaration struct {
|
|||
|
||||
// SecretsUpdatedAt is the timestamp when the secrets were last updated or when this declaration was uploaded.
|
||||
SecretsUpdatedAt *time.Time `db:"secrets_updated_at" json:"-"`
|
||||
|
||||
// VariablesUpdatedAt tracks when the Fleet variable values for this host
|
||||
// were last computed. Non-null only for declarations that use Fleet variables.
|
||||
VariablesUpdatedAt *time.Time `db:"variables_updated_at" json:"-"`
|
||||
}
|
||||
|
||||
func (p MDMAppleHostDeclaration) Equal(other MDMAppleHostDeclaration) bool {
|
||||
statusEqual := p.Status == nil && other.Status == nil || p.Status != nil && other.Status != nil && *p.Status == *other.Status
|
||||
secretsEqual := p.SecretsUpdatedAt == nil && other.SecretsUpdatedAt == nil || p.SecretsUpdatedAt != nil && other.SecretsUpdatedAt != nil && p.SecretsUpdatedAt.Equal(*other.SecretsUpdatedAt)
|
||||
varsEqual := p.VariablesUpdatedAt == nil && other.VariablesUpdatedAt == nil || p.VariablesUpdatedAt != nil && other.VariablesUpdatedAt != nil && p.VariablesUpdatedAt.Equal(*other.VariablesUpdatedAt)
|
||||
return statusEqual &&
|
||||
p.HostUUID == other.HostUUID &&
|
||||
p.DeclarationUUID == other.DeclarationUUID &&
|
||||
|
|
@ -828,7 +849,8 @@ func (p MDMAppleHostDeclaration) Equal(other MDMAppleHostDeclaration) bool {
|
|||
p.OperationType == other.OperationType &&
|
||||
p.Detail == other.Detail &&
|
||||
p.Token == other.Token &&
|
||||
secretsEqual
|
||||
secretsEqual &&
|
||||
varsEqual
|
||||
}
|
||||
|
||||
func NewMDMAppleDeclaration(raw []byte, teamID *uint, name string, declType, ident string) *MDMAppleDeclaration {
|
||||
|
|
@ -896,6 +918,10 @@ type MDMAppleDDMDeclarationItem struct {
|
|||
Status *string `db:"status"`
|
||||
OperationType *string `db:"operation_type"`
|
||||
UploadedAt time.Time `db:"uploaded_at"`
|
||||
// VariablesUpdatedAt is not part of the DDM profile, but part of the host-ddm tuple, as the variables'
|
||||
// values depend on the host. It is used to compute the token for the DDM for a specific host, as the
|
||||
// ServerToken field is just for the static token of the DDM.
|
||||
VariablesUpdatedAt *time.Time `db:"variables_updated_at"`
|
||||
}
|
||||
|
||||
// MDMAppleDDMDeclarationResponse represents a declaration in the datastore. It is used for the DDM
|
||||
|
|
|
|||
|
|
@ -555,6 +555,8 @@ func TestMDMAppleHostDeclarationEqual(t *testing.T) {
|
|||
fieldsInEqualMethod++
|
||||
items[1].SecretsUpdatedAt = items[0].SecretsUpdatedAt
|
||||
fieldsInEqualMethod++
|
||||
items[1].VariablesUpdatedAt = items[0].VariablesUpdatedAt
|
||||
fieldsInEqualMethod++
|
||||
assert.Equal(t, fieldsInEqualMethod, numberOfFields, "MDMAppleHostDeclaration.Equal needs to be updated for new/updated field(s)")
|
||||
assert.True(t, items[0].Equal(items[1]))
|
||||
|
||||
|
|
|
|||
|
|
@ -1764,6 +1764,10 @@ type Datastore interface {
|
|||
// It also takes care of cleaning up all host declarations that are
|
||||
// pending removal.
|
||||
MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID string, updates []*MDMAppleHostDeclaration) error
|
||||
// SetHostMDMAppleDeclarationStatus updates the status and detail of a
|
||||
// single declaration for a host. If variablesUpdatedAt is non-nil, it also
|
||||
// sets the variables_updated_at timestamp.
|
||||
SetHostMDMAppleDeclarationStatus(ctx context.Context, hostUUID string, declarationUUID string, status *MDMDeliveryStatus, detail string, variablesUpdatedAt *time.Time) error
|
||||
// MDMAppleSetPendingDeclarationsAs updates all ("pending", "install")
|
||||
// declarations for a host to be ("verifying", status), where status is
|
||||
// the provided value.
|
||||
|
|
@ -2013,10 +2017,10 @@ type Datastore interface {
|
|||
macDeclarations []*MDMAppleDeclaration, androidProfiles []*MDMAndroidConfigProfile, profilesVariables []MDMProfileIdentifierFleetVariables) (updates MDMProfilesUpdates, err error)
|
||||
|
||||
// NewMDMAppleDeclaration creates and returns a new MDM Apple declaration.
|
||||
NewMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration) (*MDMAppleDeclaration, error)
|
||||
NewMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration, usesFleetVars []FleetVarName) (*MDMAppleDeclaration, error)
|
||||
|
||||
// SetOrUpdateMDMAppleDeclaration upserts the MDM Apple declaration.
|
||||
SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration) (*MDMAppleDeclaration, error)
|
||||
SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration, usesFleetVars []FleetVarName) (*MDMAppleDeclaration, error)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Host Script Results
|
||||
|
|
|
|||
|
|
@ -1231,7 +1231,7 @@ type HostMDMCommand struct {
|
|||
// MDMProfileUUIDFleetVariables represents the Fleet variables used by a
|
||||
// profile identified by its UUID.
|
||||
type MDMProfileUUIDFleetVariables struct {
|
||||
// ProfileUUID is the UUID of the profile.
|
||||
// ProfileUUID is the UUID of the profile or declaration.
|
||||
ProfileUUID string
|
||||
// FleetVariables is the (deduplicated) list of Fleet variables used by the
|
||||
// profile, without the "FLEET_VAR_" prefix (as returned by
|
||||
|
|
@ -1242,8 +1242,10 @@ type MDMProfileUUIDFleetVariables struct {
|
|||
// MDMProfileIdentifierFleetVariables represents the Fleet variables used by a
|
||||
// profile identified by its identifier.
|
||||
type MDMProfileIdentifierFleetVariables struct {
|
||||
// Identifier is the identifier of the profile (which is unique by team for
|
||||
// Apple profiles).
|
||||
// Identifier is the identifier of the profile. Because the profile identifier is not guaranteed
|
||||
// to be unique across platforms and types of profiles (e.g. Apple profiles vs declarations vs Windows
|
||||
// profiles), it must be prefixed with the same letter used for the UUID prefix of the profile type.
|
||||
// E.g. fleet.MDMAppleDeclarationUUIDPrefix.
|
||||
Identifier string
|
||||
// FleetVariables is the (deduplicated) list of Fleet variables used by the
|
||||
// profile, without the "FLEET_VAR_" prefix (as returned by
|
||||
|
|
|
|||
|
|
@ -1159,6 +1159,8 @@ type MDMAppleHostDeclarationsGetAndClearResyncFunc func(ctx context.Context) (ho
|
|||
|
||||
type MDMAppleStoreDDMStatusReportFunc func(ctx context.Context, hostUUID string, updates []*fleet.MDMAppleHostDeclaration) error
|
||||
|
||||
type SetHostMDMAppleDeclarationStatusFunc func(ctx context.Context, hostUUID string, declarationUUID string, status *fleet.MDMDeliveryStatus, detail string, variablesUpdatedAt *time.Time) error
|
||||
|
||||
type MDMAppleSetPendingDeclarationsAsFunc func(ctx context.Context, hostUUID string, status *fleet.MDMDeliveryStatus, detail string) error
|
||||
|
||||
type MDMAppleSetRemoveDeclarationsAsPendingFunc func(ctx context.Context, hostUUID string, declarationUUIDs []string) error
|
||||
|
|
@ -1297,9 +1299,9 @@ type SetOrUpdateMDMWindowsConfigProfileFunc func(ctx context.Context, cp fleet.M
|
|||
|
||||
type BatchSetMDMProfilesFunc func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, profilesVariables []fleet.MDMProfileIdentifierFleetVariables) (updates fleet.MDMProfilesUpdates, err error)
|
||||
|
||||
type NewMDMAppleDeclarationFunc func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error)
|
||||
type NewMDMAppleDeclarationFunc func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error)
|
||||
|
||||
type SetOrUpdateMDMAppleDeclarationFunc func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error)
|
||||
type SetOrUpdateMDMAppleDeclarationFunc func(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error)
|
||||
|
||||
type NewHostScriptExecutionRequestFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error)
|
||||
|
||||
|
|
@ -3568,6 +3570,9 @@ type DataStore struct {
|
|||
MDMAppleStoreDDMStatusReportFunc MDMAppleStoreDDMStatusReportFunc
|
||||
MDMAppleStoreDDMStatusReportFuncInvoked bool
|
||||
|
||||
SetHostMDMAppleDeclarationStatusFunc SetHostMDMAppleDeclarationStatusFunc
|
||||
SetHostMDMAppleDeclarationStatusFuncInvoked bool
|
||||
|
||||
MDMAppleSetPendingDeclarationsAsFunc MDMAppleSetPendingDeclarationsAsFunc
|
||||
MDMAppleSetPendingDeclarationsAsFuncInvoked bool
|
||||
|
||||
|
|
@ -8603,6 +8608,13 @@ func (s *DataStore) MDMAppleStoreDDMStatusReport(ctx context.Context, hostUUID s
|
|||
return s.MDMAppleStoreDDMStatusReportFunc(ctx, hostUUID, updates)
|
||||
}
|
||||
|
||||
func (s *DataStore) SetHostMDMAppleDeclarationStatus(ctx context.Context, hostUUID string, declarationUUID string, status *fleet.MDMDeliveryStatus, detail string, variablesUpdatedAt *time.Time) error {
|
||||
s.mu.Lock()
|
||||
s.SetHostMDMAppleDeclarationStatusFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.SetHostMDMAppleDeclarationStatusFunc(ctx, hostUUID, declarationUUID, status, detail, variablesUpdatedAt)
|
||||
}
|
||||
|
||||
func (s *DataStore) MDMAppleSetPendingDeclarationsAs(ctx context.Context, hostUUID string, status *fleet.MDMDeliveryStatus, detail string) error {
|
||||
s.mu.Lock()
|
||||
s.MDMAppleSetPendingDeclarationsAsFuncInvoked = true
|
||||
|
|
@ -9086,18 +9098,18 @@ func (s *DataStore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProf
|
|||
return s.BatchSetMDMProfilesFunc(ctx, tmID, macProfiles, winProfiles, macDeclarations, androidProfiles, profilesVariables)
|
||||
}
|
||||
|
||||
func (s *DataStore) NewMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (s *DataStore) NewMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
s.mu.Lock()
|
||||
s.NewMDMAppleDeclarationFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.NewMDMAppleDeclarationFunc(ctx, declaration)
|
||||
return s.NewMDMAppleDeclarationFunc(ctx, declaration, usesFleetVars)
|
||||
}
|
||||
|
||||
func (s *DataStore) SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
func (s *DataStore) SetOrUpdateMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
s.mu.Lock()
|
||||
s.SetOrUpdateMDMAppleDeclarationFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.SetOrUpdateMDMAppleDeclarationFunc(ctx, declaration)
|
||||
return s.SetOrUpdateMDMAppleDeclarationFunc(ctx, declaration, usesFleetVars)
|
||||
}
|
||||
|
||||
func (s *DataStore) NewHostScriptExecutionRequest(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/cryptoutil"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/profiles"
|
||||
|
||||
nano_service "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service"
|
||||
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
|
||||
|
|
@ -74,6 +75,19 @@ var fleetVarsSupportedInAppleConfigProfiles = []fleet.FleetVarName{
|
|||
fleet.FleetVarHostUUID, fleet.FleetVarHostPlatform,
|
||||
}
|
||||
|
||||
// fleetVarsSupportedInDDMDeclarations is the list of Fleet variables
|
||||
// supported in Apple DDM declarations.
|
||||
var fleetVarsSupportedInDDMDeclarations = []fleet.FleetVarName{
|
||||
fleet.FleetVarHostHardwareSerial,
|
||||
fleet.FleetVarHostEndUserIDPUsername,
|
||||
fleet.FleetVarHostEndUserIDPUsernameLocalPart,
|
||||
fleet.FleetVarHostEndUserIDPGroups,
|
||||
fleet.FleetVarHostEndUserIDPDepartment,
|
||||
fleet.FleetVarHostEndUserIDPFullname,
|
||||
fleet.FleetVarHostUUID,
|
||||
fleet.FleetVarHostPlatform,
|
||||
}
|
||||
|
||||
type getMDMAppleCommandResultsRequest struct {
|
||||
CommandUUID string `query:"command_uuid,optional"`
|
||||
}
|
||||
|
|
@ -860,9 +874,11 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, dat
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Get license for team lookup and variable validation
|
||||
lic, _ := license.FromContext(ctx)
|
||||
|
||||
var teamName string
|
||||
if teamID > 0 {
|
||||
lic, _ := license.FromContext(ctx)
|
||||
if lic == nil || !lic.IsPremium() {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.ErrMissingLicense)
|
||||
}
|
||||
|
|
@ -874,7 +890,7 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, dat
|
|||
}
|
||||
|
||||
var tmID *uint
|
||||
if teamID >= 1 {
|
||||
if teamID > 0 {
|
||||
tmID = &teamID
|
||||
}
|
||||
|
||||
|
|
@ -888,8 +904,19 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, dat
|
|||
return nil, fleet.NewInvalidArgumentError("profile", err.Error())
|
||||
}
|
||||
|
||||
if err := validateDeclarationFleetVariables(dataWithSecrets); err != nil {
|
||||
return nil, err
|
||||
declVars, err := validateDeclarationFleetVariables(dataWithSecrets, lic)
|
||||
if err != nil {
|
||||
var badReqErr *fleet.BadRequestError
|
||||
if errors.As(err, &badReqErr) {
|
||||
badReqErr.Message = "Couldn't upload profile. " + badReqErr.Message
|
||||
err = badReqErr
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating declaration Fleet variables")
|
||||
}
|
||||
|
||||
varNames := make([]fleet.FleetVarName, 0, len(declVars))
|
||||
for _, v := range declVars {
|
||||
varNames = append(varNames, fleet.FleetVarName(v))
|
||||
}
|
||||
|
||||
// TODO(roberto): Maybe GetRawDeclarationValues belongs inside NewMDMAppleDeclaration? We can refactor this in a follow up.
|
||||
|
|
@ -918,7 +945,7 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, dat
|
|||
d.LabelsIncludeAll = validatedLabels
|
||||
}
|
||||
|
||||
decl, err := svc.ds.NewMDMAppleDeclaration(ctx, d)
|
||||
decl, err := svc.ds.NewMDMAppleDeclaration(ctx, d, varNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -948,11 +975,177 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, dat
|
|||
return decl, nil
|
||||
}
|
||||
|
||||
func validateDeclarationFleetVariables(contents string) error {
|
||||
if variables.Contains(contents) {
|
||||
return &fleet.BadRequestError{Message: "Fleet variables ($FLEET_VAR_*) are not currently supported in DDM profiles"}
|
||||
func validateDeclarationFleetVariables(contents string, lic license.LicenseChecker) ([]string, error) {
|
||||
fleetVars := variables.Find(contents)
|
||||
if len(fleetVars) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil
|
||||
|
||||
// Check premium license
|
||||
if lic == nil || !lic.IsPremium() {
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
// Validate against allowed list
|
||||
for _, fleetVar := range fleetVars {
|
||||
if !slices.Contains(fleetVarsSupportedInDDMDeclarations, fleet.FleetVarName(fleetVar)) {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("Fleet variable $FLEET_VAR_%s is not supported in DDM profiles.", fleetVar),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fleetVars, nil
|
||||
}
|
||||
|
||||
// jsonEscapeString returns the JSON-escaped interior of a string value
|
||||
// (without surrounding quotes), suitable for embedding inside a JSON string.
|
||||
func jsonEscapeString(s string) string {
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
// json.Marshal on a string should never fail, but return the
|
||||
// original string as a fallback.
|
||||
return s
|
||||
}
|
||||
// strip surrounding quotes
|
||||
return string(b[1 : len(b)-1])
|
||||
}
|
||||
|
||||
// replaceDeclarationFleetVariables replaces $FLEET_VAR_* placeholders in a
|
||||
// DDM declaration with host-specific values. Values are JSON-string-escaped
|
||||
// so they are safe inside JSON string fields.
|
||||
func (svc *MDMAppleDDMService) replaceDeclarationFleetVariables(
|
||||
ctx context.Context, contents string, hostUUID string,
|
||||
) (string, error) {
|
||||
fleetVars := variables.Find(contents)
|
||||
if len(fleetVars) == 0 {
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
var hostLite fleet.Host
|
||||
hostLite.UUID = hostUUID
|
||||
hostHydrated := false
|
||||
|
||||
hydrateHost := func() error {
|
||||
if hostHydrated {
|
||||
return nil
|
||||
}
|
||||
h, ok, err := profiles.HydrateHost(ctx, svc.ds, hostLite, func(n int) error {
|
||||
return fmt.Errorf("unexpected number of hosts (%d) for UUID %s", n, hostUUID)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("host not found for UUID %s", hostUUID)
|
||||
}
|
||||
hostLite = h
|
||||
hostHydrated = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var idpUser *fleet.HostEndUser
|
||||
resolveIDPUser := func(varName string) (*fleet.HostEndUser, error) {
|
||||
if idpUser != nil {
|
||||
return idpUser, nil
|
||||
}
|
||||
if err := hydrateHost(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := fleet.GetEndUsers(ctx, svc.ds, hostLite.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get end users for host: %w", err)
|
||||
}
|
||||
if len(users) == 0 || users[0].IdpUserName == "" {
|
||||
return nil, fmt.Errorf("There is no IdP username for this host. Fleet couldn't populate $FLEET_VAR_%s.", varName)
|
||||
}
|
||||
idpUser = &users[0]
|
||||
return idpUser, nil
|
||||
}
|
||||
|
||||
for _, fleetVar := range fleetVars {
|
||||
var value string
|
||||
switch fleet.FleetVarName(fleetVar) {
|
||||
case fleet.FleetVarHostUUID:
|
||||
value = hostUUID
|
||||
|
||||
case fleet.FleetVarHostHardwareSerial:
|
||||
if err := hydrateHost(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(hostLite.HardwareSerial) == "" {
|
||||
return "", fmt.Errorf("There is no serial number for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleetVar)
|
||||
}
|
||||
value = hostLite.HardwareSerial
|
||||
|
||||
case fleet.FleetVarHostPlatform:
|
||||
if err := hydrateHost(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
value = hostLite.Platform
|
||||
if value == "darwin" {
|
||||
value = "macos"
|
||||
}
|
||||
|
||||
case fleet.FleetVarHostEndUserIDPUsername:
|
||||
user, err := resolveIDPUser(fleetVar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
value = user.IdpUserName
|
||||
|
||||
case fleet.FleetVarHostEndUserIDPUsernameLocalPart:
|
||||
user, err := resolveIDPUser(fleetVar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
local, _, _ := strings.Cut(user.IdpUserName, "@")
|
||||
value = local
|
||||
|
||||
case fleet.FleetVarHostEndUserIDPGroups:
|
||||
user, err := resolveIDPUser(fleetVar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(user.IdpGroups) == 0 {
|
||||
return "", fmt.Errorf("There are no IdP groups for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleetVar)
|
||||
}
|
||||
value = strings.Join(user.IdpGroups, ",")
|
||||
|
||||
case fleet.FleetVarHostEndUserIDPDepartment:
|
||||
user, err := resolveIDPUser(fleetVar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if user.Department == "" {
|
||||
return "", fmt.Errorf("There is no IdP department for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleetVar)
|
||||
}
|
||||
value = user.Department
|
||||
|
||||
case fleet.FleetVarHostEndUserIDPFullname:
|
||||
user, err := resolveIDPUser(fleetVar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(user.IdpFullName) == "" {
|
||||
return "", fmt.Errorf("There is no IdP full name for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleetVar)
|
||||
}
|
||||
value = strings.TrimSpace(user.IdpFullName)
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("Fleet variable $FLEET_VAR_%s is not supported in DDM declarations.", fleetVar)
|
||||
}
|
||||
|
||||
contents = variables.Replace(contents, fleetVar, jsonEscapeString(value))
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
// markDeclarationFailed marks a DDM declaration as failed for a specific host.
|
||||
func (svc *MDMAppleDDMService) markDeclarationFailed(ctx context.Context, hostUUID string, d *fleet.MDMAppleDeclaration, detail string) error {
|
||||
status := fleet.MDMDeliveryFailed
|
||||
return svc.ds.SetHostMDMAppleDeclarationStatus(ctx, hostUUID, d.DeclarationUUID, &status, detail, nil)
|
||||
}
|
||||
|
||||
func (svc *Service) batchValidateDeclarationLabels(ctx context.Context, labelNames []string, teamID uint) (map[string]fleet.ConfigurationProfileLabel, error) {
|
||||
|
|
@ -5967,31 +6160,34 @@ func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, hostU
|
|||
}
|
||||
continue
|
||||
}
|
||||
effectiveToken := fleet.EffectiveDDMToken(d.ServerToken, d.VariablesUpdatedAt)
|
||||
configurations = append(configurations, fleet.MDMAppleDDMManifest{
|
||||
Identifier: d.Identifier,
|
||||
ServerToken: d.ServerToken,
|
||||
ServerToken: effectiveToken,
|
||||
})
|
||||
activations = append(activations, fleet.MDMAppleDDMManifest{
|
||||
Identifier: fmt.Sprintf("%s.activation", d.Identifier),
|
||||
ServerToken: d.ServerToken,
|
||||
ServerToken: effectiveToken,
|
||||
})
|
||||
}
|
||||
|
||||
// Calculate token based on count and concatenated tokens for install items
|
||||
var count int
|
||||
type tokenSorting struct {
|
||||
token string
|
||||
uploadedAt time.Time
|
||||
declarationUUID string
|
||||
token string
|
||||
variablesUpdatedAt *time.Time
|
||||
uploadedAt time.Time
|
||||
declarationUUID string
|
||||
}
|
||||
var tokens []tokenSorting
|
||||
for _, d := range di {
|
||||
if d.OperationType != nil && *d.OperationType == string(fleet.MDMOperationTypeInstall) {
|
||||
// Extract d.ServerToken and order by d.UploadedAt descending and then by d.DeclarationUUID ascending
|
||||
sorting := tokenSorting{
|
||||
token: d.ServerToken,
|
||||
uploadedAt: d.UploadedAt,
|
||||
declarationUUID: d.DeclarationUUID,
|
||||
token: d.ServerToken,
|
||||
variablesUpdatedAt: d.VariablesUpdatedAt,
|
||||
uploadedAt: d.UploadedAt,
|
||||
declarationUUID: d.DeclarationUUID,
|
||||
}
|
||||
tokens = append(tokens, sorting)
|
||||
count++
|
||||
|
|
@ -6007,6 +6203,11 @@ func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, hostU
|
|||
var tokenBuilder strings.Builder
|
||||
for _, t := range tokens {
|
||||
tokenBuilder.WriteString(t.token)
|
||||
if t.variablesUpdatedAt != nil {
|
||||
// Must match MySQL's DATETIME(6) string representation used in
|
||||
// MDMAppleDDMDeclarationsToken's IFNULL(hmad.variables_updated_at, '').
|
||||
tokenBuilder.WriteString(t.variablesUpdatedAt.Format("2006-01-02 15:04:05.000000"))
|
||||
}
|
||||
}
|
||||
|
||||
var token string
|
||||
|
|
@ -6080,7 +6281,7 @@ func (svc *MDMAppleDDMService) handleActivationDeclaration(ctx context.Context,
|
|||
},
|
||||
"ServerToken": "%s",
|
||||
"Type": "com.apple.activation.simple"
|
||||
}`, parts[2], references, d.Token)
|
||||
}`, parts[2], references, fleet.EffectiveDDMToken(d.Token, d.VariablesUpdatedAt))
|
||||
|
||||
return []byte(response), nil
|
||||
}
|
||||
|
|
@ -6099,11 +6300,21 @@ func (svc *MDMAppleDDMService) handleConfigurationDeclaration(ctx context.Contex
|
|||
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("expanding embedded secrets for identifier:%s hostUUID:%s", parts[2], hostUUID))
|
||||
}
|
||||
|
||||
// Replace Fleet variables with host-specific values
|
||||
expanded, err = svc.replaceDeclarationFleetVariables(ctx, expanded, hostUUID)
|
||||
if err != nil {
|
||||
// Mark this declaration as failed for this host, return empty 200
|
||||
if err := svc.markDeclarationFailed(ctx, hostUUID, d, err.Error()); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "mark declaration as failed")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var tempd map[string]any
|
||||
if err := json.Unmarshal([]byte(expanded), &tempd); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "unmarshaling stored declaration")
|
||||
}
|
||||
tempd["ServerToken"] = d.Token
|
||||
tempd["ServerToken"] = fleet.EffectiveDDMToken(d.Token, d.VariablesUpdatedAt) //nolint:nilaway // tempd is non-nil after successful json.Unmarshal
|
||||
|
||||
b, err := json.Marshal(tempd)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func TestDeclarativeManagement_DeclarationItems(t *testing.T) {
|
|||
TeamID: nil,
|
||||
RawJSON: []byte(fmt.Sprintf(`{"Type":"com.apple.test.declaration","Identifier":"%s"}`, identifier)),
|
||||
}
|
||||
declaration, err := ds.NewMDMAppleDeclaration(context.Background(), declaration)
|
||||
declaration, err := ds.NewMDMAppleDeclaration(context.Background(), declaration, nil)
|
||||
require.NoError(t, err)
|
||||
return declaration
|
||||
}
|
||||
|
|
|
|||
|
|
@ -887,7 +887,7 @@ func TestNewMDMAppleDeclaration(t *testing.T) {
|
|||
_, err = svc.NewMDMAppleDeclaration(ctx, 0, b, nil, "name", fleet.LabelsIncludeAll)
|
||||
assert.ErrorContains(t, err, "Only configuration declarations (com.apple.configuration.) are supported")
|
||||
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
return d, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
|
|
@ -973,7 +973,7 @@ func TestNewMDMAppleDeclarationSkipValidation(t *testing.T) {
|
|||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
||||
return s, nil, nil
|
||||
}
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
return d, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
|
|
@ -1016,7 +1016,7 @@ func TestNewMDMAppleDeclarationSkipValidation(t *testing.T) {
|
|||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
||||
return s, nil, nil
|
||||
}
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
return d, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
|
|
@ -1058,7 +1058,7 @@ func TestNewMDMAppleDeclarationSkipValidation(t *testing.T) {
|
|||
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, s string) (string, *time.Time, error) {
|
||||
return s, nil, nil
|
||||
}
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
ds.NewMDMAppleDeclarationFunc = func(ctx context.Context, d *fleet.MDMAppleDeclaration, usesFleetVars []fleet.FleetVarName) (*fleet.MDMAppleDeclaration, error) {
|
||||
return d, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
|
|
@ -6338,6 +6338,103 @@ func TestValidateConfigProfileFleetVariables(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateDeclarationFleetVariables(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
premiumLic := &fleet.LicenseInfo{Tier: fleet.TierPremium}
|
||||
freeLic := &fleet.LicenseInfo{Tier: fleet.TierFree}
|
||||
|
||||
// helper to create a simple DDM declaration JSON with a value field
|
||||
makeDecl := func(value string) string {
|
||||
return fmt.Sprintf(`{"Type": "com.apple.configuration.management.test", "Identifier": "com.example.test", "Payload": {"Value": %q}}`, value)
|
||||
}
|
||||
|
||||
t.Run("no variables, free license", func(t *testing.T) {
|
||||
vars, err := validateDeclarationFleetVariables(makeDecl("static-value"), freeLic)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, vars)
|
||||
})
|
||||
|
||||
t.Run("supported variable with premium license", func(t *testing.T) {
|
||||
vars, err := validateDeclarationFleetVariables(makeDecl("$FLEET_VAR_HOST_HARDWARE_SERIAL"), premiumLic)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"HOST_HARDWARE_SERIAL"}, vars)
|
||||
})
|
||||
|
||||
t.Run("supported variable with braces", func(t *testing.T) {
|
||||
vars, err := validateDeclarationFleetVariables(makeDecl("${FLEET_VAR_HOST_UUID}"), premiumLic)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"HOST_UUID"}, vars)
|
||||
})
|
||||
|
||||
t.Run("multiple supported variables", func(t *testing.T) {
|
||||
vars, err := validateDeclarationFleetVariables(
|
||||
makeDecl(`["$FLEET_VAR_HOST_HARDWARE_SERIAL", "$FLEET_VAR_HOST_END_USER_IDP_USERNAME"]`), premiumLic)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{"HOST_HARDWARE_SERIAL", "HOST_END_USER_IDP_USERNAME"}, vars)
|
||||
})
|
||||
|
||||
t.Run("all supported variables", func(t *testing.T) {
|
||||
// Build the declaration content and expected results from the allowed list
|
||||
var jsonVars, expectedVars []string
|
||||
for _, v := range fleetVarsSupportedInDDMDeclarations {
|
||||
jsonVars = append(jsonVars, fmt.Sprintf(`"$FLEET_VAR_%s"`, v))
|
||||
expectedVars = append(expectedVars, string(v))
|
||||
}
|
||||
vars, err := validateDeclarationFleetVariables(
|
||||
makeDecl("["+strings.Join(jsonVars, ", ")+"]"), premiumLic)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, expectedVars, vars)
|
||||
})
|
||||
|
||||
t.Run("supported variable without premium license", func(t *testing.T) {
|
||||
_, err := validateDeclarationFleetVariables(makeDecl("$FLEET_VAR_HOST_HARDWARE_SERIAL"), freeLic)
|
||||
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
||||
})
|
||||
|
||||
t.Run("supported variable with nil license", func(t *testing.T) {
|
||||
_, err := validateDeclarationFleetVariables(makeDecl("$FLEET_VAR_HOST_UUID"), nil)
|
||||
require.ErrorIs(t, err, fleet.ErrMissingLicense)
|
||||
})
|
||||
|
||||
t.Run("unsupported variable", func(t *testing.T) {
|
||||
_, err := validateDeclarationFleetVariables(makeDecl("$FLEET_VAR_NDES_SCEP_CHALLENGE"), premiumLic)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Fleet variable $FLEET_VAR_NDES_SCEP_CHALLENGE is not supported in DDM profiles")
|
||||
})
|
||||
|
||||
t.Run("supported and unsupported variables", func(t *testing.T) {
|
||||
_, err := validateDeclarationFleetVariables(
|
||||
makeDecl(`["$FLEET_VAR_HOST_UUID", "$FLEET_VAR_DIGICERT_DATA_myCA"]`), premiumLic)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Fleet variable $FLEET_VAR_DIGICERT_DATA_myCA is not supported in DDM profiles")
|
||||
})
|
||||
}
|
||||
|
||||
func TestJSONEscapeString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"plain string", "hello", "hello"},
|
||||
{"with double quotes", `say "hi"`, `say \"hi\"`},
|
||||
{"with backslash", `path\to\file`, `path\\to\\file`},
|
||||
{"with newline", "line1\nline2", `line1\nline2`},
|
||||
{"with tab", "col1\tcol2", `col1\tcol2`},
|
||||
{"empty string", "", ""},
|
||||
{"unicode", "café ☕", "café ☕"},
|
||||
{"control chars", "a\x00b", `a\u0000b`},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, jsonEscapeString(tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed testdata/profiles/digicert-validation.mobileconfig
|
||||
var digiCertValidationMobileconfig string
|
||||
|
||||
|
|
|
|||
|
|
@ -7723,6 +7723,26 @@ func (s *integrationTestSuite) TestPremiumEndpointsWithoutLicense() {
|
|||
createHostAndDeviceToken(t, s.ds, "some-token")
|
||||
s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "some-token"), nil, http.StatusPaymentRequired)
|
||||
|
||||
// uploading a DDM declaration with a Fleet variable returns a license error
|
||||
// (single profile upload endpoint)
|
||||
ddmWithFleetVar := []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Identifier": "com.example.fleetvar-test",
|
||||
"Payload": {"Value": "$FLEET_VAR_HOST_HARDWARE_SERIAL"}
|
||||
}`)
|
||||
body, headers := generateNewProfileMultipartRequest(t, "fleetvar-test.json", ddmWithFleetVar, s.token, nil)
|
||||
res = s.DoRawWithHeaders("POST", "/api/latest/fleet/configuration_profiles", body.Bytes(), http.StatusPaymentRequired, headers)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Requires Fleet Premium license")
|
||||
|
||||
// uploading a DDM declaration with a Fleet variable returns a license error
|
||||
// (batch profiles endpoint)
|
||||
res = s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "N1", Contents: ddmWithFleetVar},
|
||||
}}, http.StatusPaymentRequired)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Requires Fleet Premium license")
|
||||
|
||||
// software titles
|
||||
// a normal request works fine
|
||||
var resp listSoftwareTitlesResponse
|
||||
|
|
@ -15052,7 +15072,7 @@ func (s *integrationTestSuite) TestSecretVariablesInUse() {
|
|||
Name: "decl-1",
|
||||
RawJSON: json.RawMessage(`{"Identifier": "${FLEET_SECRET_NAME1}"}`),
|
||||
TeamID: &foobarTeam.ID,
|
||||
})
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res = s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/custom_variables/%d", firstVariableID), nil, http.StatusConflict)
|
||||
|
|
|
|||
|
|
@ -1302,6 +1302,569 @@ func (s *integrationMDMTestSuite) TestDDMTransactionRecording() {
|
|||
})
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestAppleDDMFleetVariables() {
|
||||
t := s.T()
|
||||
ctx := t.Context()
|
||||
|
||||
// === Setup ===
|
||||
|
||||
// Create two MDM-enrolled hosts
|
||||
host1, mdmDevice1 := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
_, mdmDevice2 := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
|
||||
// Set host1's serial to a value with characters that need JSON escaping,
|
||||
// to verify that variable substitution produces valid JSON.
|
||||
host1.HardwareSerial = `SER"IAL\123`
|
||||
err := s.ds.UpdateHost(ctx, host1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a team and transfer host1 into it; host2 stays global (control)
|
||||
team := &fleet.Team{Name: t.Name() + "team1"}
|
||||
var createTeamResp teamResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
|
||||
require.NotZero(t, createTeamResp.Team.ID)
|
||||
team = createTeamResp.Team
|
||||
|
||||
s.Do("POST", "/api/v1/fleet/hosts/transfer",
|
||||
addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{host1.ID}}, http.StatusOK)
|
||||
|
||||
// Helper: read declaration from DB by name
|
||||
getDeclaration := func(t *testing.T, name string) fleet.MDMAppleDeclaration {
|
||||
stmt := `
|
||||
SELECT
|
||||
declaration_uuid,
|
||||
team_id,
|
||||
identifier,
|
||||
name,
|
||||
raw_json,
|
||||
HEX(token) as token,
|
||||
created_at,
|
||||
uploaded_at
|
||||
FROM mdm_apple_declarations
|
||||
WHERE name = ?`
|
||||
|
||||
var decl fleet.MDMAppleDeclaration
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &decl, stmt, name)
|
||||
})
|
||||
return decl
|
||||
}
|
||||
|
||||
// Helper: read variables_updated_at for a host/declaration pair
|
||||
getHostDeclVarsUpdatedAt := func(t *testing.T, hostUUID, declUUID string) *time.Time {
|
||||
var result []time.Time
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.SelectContext(ctx, q, &result,
|
||||
`SELECT variables_updated_at FROM host_mdm_apple_declarations WHERE host_uuid = ? AND declaration_uuid = ? AND variables_updated_at IS NOT NULL`,
|
||||
hostUUID, declUUID)
|
||||
})
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &result[0]
|
||||
}
|
||||
|
||||
checkNoCommands := func(d *mdmtest.TestAppleMDMClient) {
|
||||
cmd, err := d.Idle()
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, cmd)
|
||||
}
|
||||
|
||||
checkDDMSync := func(d *mdmtest.TestAppleMDMClient) {
|
||||
cmd, err := d.Idle()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cmd)
|
||||
require.Equal(t, "DeclarativeManagement", cmd.Command.RequestType)
|
||||
cmd, err = d.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, cmd)
|
||||
_, err = d.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
checkDeclarationItemsResp := func(t *testing.T, r fleet.MDMAppleDDMDeclarationItemsResponse, expectedDeclTok string,
|
||||
expectedDeclsByToken map[string]fleet.MDMAppleDeclaration,
|
||||
) {
|
||||
require.Equal(t, expectedDeclTok, r.DeclarationsToken)
|
||||
require.NotEmpty(t, r.Declarations.Activations)
|
||||
require.Empty(t, r.Declarations.Assets)
|
||||
require.Empty(t, r.Declarations.Management)
|
||||
require.Len(t, r.Declarations.Configurations, len(expectedDeclsByToken))
|
||||
for _, m := range r.Declarations.Configurations {
|
||||
d, ok := expectedDeclsByToken[m.ServerToken]
|
||||
require.True(t, ok, "server token %x not found for %s", m.ServerToken, m.Identifier)
|
||||
require.Equal(t, d.Identifier, m.Identifier)
|
||||
}
|
||||
}
|
||||
|
||||
teamIDStr := fmt.Sprintf("%d", team.ID)
|
||||
|
||||
// Declaration payloads
|
||||
declWithUUID := []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Payload": {"Echo": "$FLEET_VAR_HOST_UUID"},
|
||||
"Identifier": "com.fleet.var.uuid"
|
||||
}`)
|
||||
declWithSerial := []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Payload": {"Echo": "$FLEET_VAR_HOST_HARDWARE_SERIAL"},
|
||||
"Identifier": "com.fleet.var.serial"
|
||||
}`)
|
||||
declPlain := []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Payload": {"Echo": "static-value"},
|
||||
"Identifier": "com.fleet.plain"
|
||||
}`)
|
||||
|
||||
// === Failing upload (unsupported variable) ===
|
||||
|
||||
badDecl := []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Payload": {"Echo": "$FLEET_VAR_NDES_SCEP_CHALLENGE"},
|
||||
"Identifier": "com.fleet.bad"
|
||||
}`)
|
||||
badReq := batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "BadDecl.json", Contents: badDecl},
|
||||
}}
|
||||
badRes := s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", badReq, http.StatusBadRequest,
|
||||
"team_id", teamIDStr)
|
||||
errMsg := extractServerErrorText(badRes.Body)
|
||||
require.Contains(t, errMsg, "$FLEET_VAR_NDES_SCEP_CHALLENGE is not supported in DDM")
|
||||
|
||||
// === Upload declarations with and without variables ===
|
||||
|
||||
profilesReq := batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "VarUUID.json", Contents: declWithUUID},
|
||||
{Name: "VarSerial.json", Contents: declWithSerial},
|
||||
{Name: "Plain.json", Contents: declPlain},
|
||||
}}
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent,
|
||||
"team_id", teamIDStr)
|
||||
|
||||
// Verify raw JSON stored as-is (variables not expanded in storage)
|
||||
dbDeclUUID := getDeclaration(t, "VarUUID.json")
|
||||
assert.Contains(t, string(dbDeclUUID.RawJSON), "$FLEET_VAR_HOST_UUID")
|
||||
dbDeclSerial := getDeclaration(t, "VarSerial.json")
|
||||
assert.Contains(t, string(dbDeclSerial.RawJSON), "$FLEET_VAR_HOST_HARDWARE_SERIAL")
|
||||
dbDeclPlain := getDeclaration(t, "Plain.json")
|
||||
assert.Contains(t, string(dbDeclPlain.RawJSON), "static-value")
|
||||
|
||||
// === First sync — verify variable substitution ===
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
checkDDMSync(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
|
||||
// Host1 fetches tokens
|
||||
r, err := mdmDevice1.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
tokens := parseTokensResp(t, r)
|
||||
lastSyncDeclToken := tokens.SyncTokens.DeclarationsToken
|
||||
require.NotEmpty(t, lastSyncDeclToken)
|
||||
|
||||
// Fetch individual declarations and verify substitution
|
||||
var gotParsed fleet.MDMAppleDDMDeclarationResponse
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("declaration/configuration/com.fleet.var.uuid")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed))
|
||||
assert.Contains(t, string(gotParsed.Payload), host1.UUID)
|
||||
assert.NotContains(t, string(gotParsed.Payload), "$FLEET_VAR")
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("declaration/configuration/com.fleet.var.serial")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed))
|
||||
assert.NotContains(t, string(gotParsed.Payload), "$FLEET_VAR")
|
||||
// Verify the serial (which contains " and \) is properly JSON-escaped:
|
||||
// the payload must be valid JSON and unmarshal to the original value.
|
||||
var serialPayload struct{ Echo string }
|
||||
require.NoError(t, json.Unmarshal(gotParsed.Payload, &serialPayload))
|
||||
assert.Equal(t, host1.HardwareSerial, serialPayload.Echo)
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("declaration/configuration/com.fleet.plain")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed))
|
||||
assert.Contains(t, string(gotParsed.Payload), "static-value")
|
||||
|
||||
// Verify variables_updated_at: set for variable decls, nil for plain
|
||||
varsUpdatedUUID := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclUUID.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedUUID)
|
||||
varsUpdatedSerial := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedSerial)
|
||||
varsUpdatedPlain := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclPlain.DeclarationUUID)
|
||||
require.Nil(t, varsUpdatedPlain)
|
||||
|
||||
// Build expected declaration-items map with effective tokens (incorporating variables_updated_at)
|
||||
declsByToken := map[string]fleet.MDMAppleDeclaration{
|
||||
fleet.EffectiveDDMToken(dbDeclUUID.Token, varsUpdatedUUID): {Identifier: "com.fleet.var.uuid"},
|
||||
fleet.EffectiveDDMToken(dbDeclSerial.Token, varsUpdatedSerial): {Identifier: "com.fleet.var.serial"},
|
||||
dbDeclPlain.Token: {Identifier: "com.fleet.plain"},
|
||||
}
|
||||
|
||||
// Host1 fetches declaration items
|
||||
r, err = mdmDevice1.DeclarativeManagement("declaration-items")
|
||||
require.NoError(t, err)
|
||||
itemsResp := parseDeclarationItemsResp(t, r)
|
||||
checkDeclarationItemsResp(t, itemsResp, lastSyncDeclToken, declsByToken)
|
||||
|
||||
// === No resend when unrelated declaration added ===
|
||||
|
||||
newDecl := []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Payload": {"Echo": "new-stuff"},
|
||||
"Identifier": "com.fleet.new"
|
||||
}`)
|
||||
profilesReqWithNew := batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "VarUUID.json", Contents: declWithUUID},
|
||||
{Name: "VarSerial.json", Contents: declWithSerial},
|
||||
{Name: "Plain.json", Contents: declPlain},
|
||||
{Name: "NewDecl.json", Contents: newDecl},
|
||||
}}
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReqWithNew, http.StatusNoContent,
|
||||
"team_id", teamIDStr)
|
||||
|
||||
dbNewDecl := getDeclaration(t, "NewDecl.json")
|
||||
assert.Contains(t, string(dbNewDecl.RawJSON), "new-stuff")
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// Host1 gets DDM sync (declaration set changed), host2 nothing
|
||||
checkDDMSync(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
tokens = parseTokensResp(t, r)
|
||||
lastSyncDeclToken = tokens.SyncTokens.DeclarationsToken
|
||||
require.NotEmpty(t, lastSyncDeclToken)
|
||||
|
||||
declsByToken = map[string]fleet.MDMAppleDeclaration{
|
||||
fleet.EffectiveDDMToken(dbDeclUUID.Token, varsUpdatedUUID): {Identifier: "com.fleet.var.uuid"},
|
||||
fleet.EffectiveDDMToken(dbDeclSerial.Token, varsUpdatedSerial): {Identifier: "com.fleet.var.serial"},
|
||||
dbDeclPlain.Token: {Identifier: "com.fleet.plain"},
|
||||
dbNewDecl.Token: {Identifier: "com.fleet.new"},
|
||||
}
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("declaration-items")
|
||||
require.NoError(t, err)
|
||||
itemsResp = parseDeclarationItemsResp(t, r)
|
||||
checkDeclarationItemsResp(t, itemsResp, lastSyncDeclToken, declsByToken)
|
||||
|
||||
// variables_updated_at did NOT change for existing variable declarations
|
||||
varsUpdatedUUIDAfterAdd := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclUUID.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedUUIDAfterAdd)
|
||||
assert.Equal(t, *varsUpdatedUUID, *varsUpdatedUUIDAfterAdd)
|
||||
varsUpdatedSerialAfterAdd := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedSerialAfterAdd)
|
||||
assert.Equal(t, *varsUpdatedSerial, *varsUpdatedSerialAfterAdd)
|
||||
|
||||
// === No resend when unrelated declaration deleted ===
|
||||
|
||||
// new decl is not in profilesReq, so it will be deleted
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent,
|
||||
"team_id", teamIDStr)
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// Host1 gets DDM sync (declaration set changed), host2 nothing
|
||||
checkDDMSync(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
|
||||
declsByToken = map[string]fleet.MDMAppleDeclaration{
|
||||
fleet.EffectiveDDMToken(dbDeclUUID.Token, varsUpdatedUUID): {Identifier: "com.fleet.var.uuid"},
|
||||
fleet.EffectiveDDMToken(dbDeclSerial.Token, varsUpdatedSerial): {Identifier: "com.fleet.var.serial"},
|
||||
dbDeclPlain.Token: {Identifier: "com.fleet.plain"},
|
||||
}
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
tokens = parseTokensResp(t, r)
|
||||
lastSyncDeclToken = tokens.SyncTokens.DeclarationsToken
|
||||
require.NotEmpty(t, lastSyncDeclToken)
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("declaration-items")
|
||||
require.NoError(t, err)
|
||||
itemsResp = parseDeclarationItemsResp(t, r)
|
||||
checkDeclarationItemsResp(t, itemsResp, lastSyncDeclToken, declsByToken)
|
||||
|
||||
// variables_updated_at still unchanged
|
||||
varsUpdatedUUIDAfterDel := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclUUID.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedUUIDAfterDel)
|
||||
assert.Equal(t, *varsUpdatedUUID, *varsUpdatedUUIDAfterDel)
|
||||
varsUpdatedSerialAfterDel := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedSerialAfterDel)
|
||||
assert.Equal(t, *varsUpdatedSerial, *varsUpdatedSerialAfterDel)
|
||||
|
||||
// === No resend on no-op GitOps batch upload ===
|
||||
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent,
|
||||
"team_id", teamIDStr)
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// No commands for either host — nothing changed
|
||||
checkNoCommands(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
|
||||
// Token unchanged
|
||||
r, err = mdmDevice1.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
tokens = parseTokensResp(t, r)
|
||||
assert.Equal(t, lastSyncDeclToken, tokens.SyncTokens.DeclarationsToken)
|
||||
|
||||
// === Resend when variable values change ===
|
||||
|
||||
// Simulate variable value change: set status = NULL on variable declarations.
|
||||
// This is the same operation triggerResendProfilesUsingVariables performs.
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx,
|
||||
`UPDATE host_mdm_apple_declarations SET status = NULL
|
||||
WHERE host_uuid = ? AND declaration_uuid IN (?, ?)`,
|
||||
host1.UUID, dbDeclUUID.DeclarationUUID, dbDeclSerial.DeclarationUUID,
|
||||
)
|
||||
return err
|
||||
})
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// Host1 gets DDM sync, host2 nothing
|
||||
checkDDMSync(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
|
||||
// variables_updated_at for variable declarations was updated (newer)
|
||||
varsUpdatedUUIDAfterChange := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclUUID.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedUUIDAfterChange)
|
||||
assert.True(t, varsUpdatedUUIDAfterChange.After(*varsUpdatedUUID),
|
||||
"variables_updated_at should be newer after variable change, got %v vs original %v", varsUpdatedUUIDAfterChange, varsUpdatedUUID)
|
||||
varsUpdatedSerialAfterChange := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedSerialAfterChange)
|
||||
assert.True(t, varsUpdatedSerialAfterChange.After(*varsUpdatedSerial),
|
||||
"variables_updated_at should be newer after variable change, got %v vs original %v", varsUpdatedSerialAfterChange, varsUpdatedSerial)
|
||||
|
||||
// Plain declaration's variables_updated_at is still nil
|
||||
varsUpdatedPlainAfterChange := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclPlain.DeclarationUUID)
|
||||
require.Nil(t, varsUpdatedPlainAfterChange)
|
||||
|
||||
// Token changed
|
||||
r, err = mdmDevice1.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
tokens = parseTokensResp(t, r)
|
||||
assert.NotEqual(t, lastSyncDeclToken, tokens.SyncTokens.DeclarationsToken)
|
||||
|
||||
// Variables still substituted correctly
|
||||
r, err = mdmDevice1.DeclarativeManagement("declaration/configuration/com.fleet.var.uuid")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed))
|
||||
assert.Contains(t, string(gotParsed.Payload), host1.UUID)
|
||||
|
||||
// === Variable change on one host does not resend to teammate ===
|
||||
|
||||
// Create a third host on the same team as host1
|
||||
host3, mdmDevice3 := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
||||
s.Do("POST", "/api/v1/fleet/hosts/transfer",
|
||||
addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{host3.ID}}, http.StatusOK)
|
||||
|
||||
// Let host3 complete its initial DDM sync
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
checkDDMSync(mdmDevice3)
|
||||
checkNoCommands(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
|
||||
// Record host3's variables_updated_at after initial sync
|
||||
host3InitVarsUpdatedUUID := getHostDeclVarsUpdatedAt(t, host3.UUID, dbDeclUUID.DeclarationUUID)
|
||||
require.NotNil(t, host3InitVarsUpdatedUUID)
|
||||
host3InitVarsUpdatedSerial := getHostDeclVarsUpdatedAt(t, host3.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, host3InitVarsUpdatedSerial)
|
||||
|
||||
// Verify stable state: no-op batch upload triggers no commands for anyone
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReq, http.StatusNoContent,
|
||||
"team_id", teamIDStr)
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
checkNoCommands(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
checkNoCommands(mdmDevice3)
|
||||
|
||||
// Simulate variable change for host1 only
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx,
|
||||
`UPDATE host_mdm_apple_declarations SET status = NULL
|
||||
WHERE host_uuid = ? AND declaration_uuid IN (?, ?)`,
|
||||
host1.UUID, dbDeclUUID.DeclarationUUID, dbDeclSerial.DeclarationUUID,
|
||||
)
|
||||
return err
|
||||
})
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// Only host1 gets DDM sync; host3 (same team) and host2 (global) do not
|
||||
checkDDMSync(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
checkNoCommands(mdmDevice3)
|
||||
|
||||
// Verify host3's variables_updated_at was not changed by host1's resend
|
||||
varsUpdatedUUIDHost3 := getHostDeclVarsUpdatedAt(t, host3.UUID, dbDeclUUID.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedUUIDHost3)
|
||||
assert.Equal(t, *host3InitVarsUpdatedUUID, *varsUpdatedUUIDHost3)
|
||||
varsUpdatedSerialHost3 := getHostDeclVarsUpdatedAt(t, host3.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedSerialHost3)
|
||||
assert.Equal(t, *host3InitVarsUpdatedSerial, *varsUpdatedSerialHost3)
|
||||
|
||||
// host3 fetches its own declarations — variables are correctly substituted
|
||||
// with host3's own values
|
||||
r, err = mdmDevice3.DeclarativeManagement("declaration/configuration/com.fleet.var.uuid")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed))
|
||||
assert.Contains(t, string(gotParsed.Payload), host3.UUID)
|
||||
assert.NotContains(t, string(gotParsed.Payload), host1.UUID)
|
||||
|
||||
r, err = mdmDevice3.DeclarativeManagement("declaration/configuration/com.fleet.var.serial")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed))
|
||||
assert.Contains(t, string(gotParsed.Payload), host3.HardwareSerial)
|
||||
assert.NotContains(t, string(gotParsed.Payload), host1.HardwareSerial)
|
||||
|
||||
// === Failed variable resolution (no IdP user for host) ===
|
||||
|
||||
declWithIdpUsername := []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Payload": {"Echo": "$FLEET_VAR_HOST_END_USER_IDP_USERNAME"},
|
||||
"Identifier": "com.fleet.var.idpusername"
|
||||
}`)
|
||||
profilesReqWithIdp := batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "VarUUID.json", Contents: declWithUUID},
|
||||
{Name: "VarSerial.json", Contents: declWithSerial},
|
||||
{Name: "Plain.json", Contents: declPlain},
|
||||
{Name: "VarIdpUsername.json", Contents: declWithIdpUsername},
|
||||
}}
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReqWithIdp, http.StatusNoContent,
|
||||
"team_id", teamIDStr)
|
||||
|
||||
dbDeclIdpUsername := getDeclaration(t, "VarIdpUsername.json")
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// Host1 gets DDM sync (declaration set changed)
|
||||
checkDDMSync(mdmDevice1)
|
||||
checkNoCommands(mdmDevice2)
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
tokens = parseTokensResp(t, r)
|
||||
lastSyncDeclToken = tokens.SyncTokens.DeclarationsToken
|
||||
require.NotEmpty(t, lastSyncDeclToken)
|
||||
|
||||
// Get current variables_updated_at for host1's declarations (may have changed since earlier captures)
|
||||
latestVarsUpdatedUUID := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclUUID.DeclarationUUID)
|
||||
latestVarsUpdatedSerial := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclSerial.DeclarationUUID)
|
||||
latestVarsUpdatedIdp := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclIdpUsername.DeclarationUUID)
|
||||
|
||||
declsByToken = map[string]fleet.MDMAppleDeclaration{
|
||||
fleet.EffectiveDDMToken(dbDeclUUID.Token, latestVarsUpdatedUUID): {Identifier: "com.fleet.var.uuid"},
|
||||
fleet.EffectiveDDMToken(dbDeclSerial.Token, latestVarsUpdatedSerial): {Identifier: "com.fleet.var.serial"},
|
||||
dbDeclPlain.Token: {Identifier: "com.fleet.plain"},
|
||||
fleet.EffectiveDDMToken(dbDeclIdpUsername.Token, latestVarsUpdatedIdp): {Identifier: "com.fleet.var.idpusername"},
|
||||
}
|
||||
|
||||
r, err = mdmDevice1.DeclarativeManagement("declaration-items")
|
||||
require.NoError(t, err)
|
||||
itemsResp = parseDeclarationItemsResp(t, r)
|
||||
checkDeclarationItemsResp(t, itemsResp, lastSyncDeclToken, declsByToken)
|
||||
|
||||
// Host1 fetches the IdP username declaration — variable resolution fails
|
||||
// because no IdP user exists for the host. The server returns an empty 200
|
||||
// and marks the declaration as failed.
|
||||
_, err = mdmDevice1.DeclarativeManagement("declaration/configuration/com.fleet.var.idpusername")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the declaration is marked as failed with the expected detail message
|
||||
var hostDecl fleet.MDMAppleHostDeclaration
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &hostDecl,
|
||||
`SELECT status, detail FROM host_mdm_apple_declarations WHERE host_uuid = ? AND declaration_uuid = ?`,
|
||||
host1.UUID, dbDeclIdpUsername.DeclarationUUID)
|
||||
})
|
||||
require.NotNil(t, hostDecl.Status)
|
||||
assert.Equal(t, fleet.MDMDeliveryFailed, *hostDecl.Status)
|
||||
assert.Contains(t, hostDecl.Detail, "There is no IdP username for this host")
|
||||
assert.Contains(t, hostDecl.Detail, "$FLEET_VAR_HOST_END_USER_IDP_USERNAME")
|
||||
|
||||
// === Updating variable declaration to non-variable clears variables_updated_at ===
|
||||
|
||||
// Drain host3's pending DDM sync from the IdP batch upload above
|
||||
checkDDMSync(mdmDevice3)
|
||||
|
||||
// Capture current token for comparison
|
||||
r, err = mdmDevice1.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
tokens = parseTokensResp(t, r)
|
||||
lastSyncDeclToken = tokens.SyncTokens.DeclarationsToken
|
||||
require.NotEmpty(t, lastSyncDeclToken)
|
||||
|
||||
// Verify variables_updated_at is non-nil for VarUUID and VarSerial on both team hosts
|
||||
preVarsUpdatedUUIDHost1 := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclUUID.DeclarationUUID)
|
||||
require.NotNil(t, preVarsUpdatedUUIDHost1)
|
||||
preVarsUpdatedUUIDHost3 := getHostDeclVarsUpdatedAt(t, host3.UUID, dbDeclUUID.DeclarationUUID)
|
||||
require.NotNil(t, preVarsUpdatedUUIDHost3)
|
||||
preVarsUpdatedSerialHost1 := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, preVarsUpdatedSerialHost1)
|
||||
preVarsUpdatedSerialHost3 := getHostDeclVarsUpdatedAt(t, host3.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, preVarsUpdatedSerialHost3)
|
||||
|
||||
// Update VarUUID.json to remove the variable (same name/identifier, static content)
|
||||
declUUIDNowStatic := []byte(`{
|
||||
"Type": "com.apple.configuration.management.test",
|
||||
"Payload": {"Echo": "static-uuid-replacement"},
|
||||
"Identifier": "com.fleet.var.uuid"
|
||||
}`)
|
||||
profilesReqVarRemoved := batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
|
||||
{Name: "VarUUID.json", Contents: declUUIDNowStatic},
|
||||
{Name: "VarSerial.json", Contents: declWithSerial},
|
||||
{Name: "Plain.json", Contents: declPlain},
|
||||
{Name: "VarIdpUsername.json", Contents: declWithIdpUsername},
|
||||
}}
|
||||
s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", profilesReqVarRemoved, http.StatusNoContent,
|
||||
"team_id", teamIDStr)
|
||||
|
||||
// Re-read the declaration from DB — content and token should have changed
|
||||
dbDeclUUIDUpdated := getDeclaration(t, "VarUUID.json")
|
||||
assert.Contains(t, string(dbDeclUUIDUpdated.RawJSON), "static-uuid-replacement")
|
||||
assert.NotContains(t, string(dbDeclUUIDUpdated.RawJSON), "$FLEET_VAR")
|
||||
assert.NotEqual(t, dbDeclUUID.Token, dbDeclUUIDUpdated.Token)
|
||||
// Declaration UUID stays the same (updated in place)
|
||||
assert.Equal(t, dbDeclUUID.DeclarationUUID, dbDeclUUIDUpdated.DeclarationUUID)
|
||||
|
||||
s.awaitTriggerProfileSchedule(t)
|
||||
|
||||
// Both team hosts get DDM sync (declaration content changed)
|
||||
checkDDMSync(mdmDevice1)
|
||||
checkDDMSync(mdmDevice3)
|
||||
// Global host gets nothing
|
||||
checkNoCommands(mdmDevice2)
|
||||
|
||||
// Token changed (declaration requires re-delivery)
|
||||
r, err = mdmDevice1.DeclarativeManagement("tokens")
|
||||
require.NoError(t, err)
|
||||
tokens = parseTokensResp(t, r)
|
||||
assert.NotEqual(t, lastSyncDeclToken, tokens.SyncTokens.DeclarationsToken)
|
||||
|
||||
// variables_updated_at for VarUUID.json is now NULL (no more variables)
|
||||
varsUpdatedUUIDAfterRemoval := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclUUIDUpdated.DeclarationUUID)
|
||||
assert.Nil(t, varsUpdatedUUIDAfterRemoval, "variables_updated_at should be NULL after removing variable from declaration (host1)")
|
||||
|
||||
varsUpdatedUUIDAfterRemovalHost3 := getHostDeclVarsUpdatedAt(t, host3.UUID, dbDeclUUIDUpdated.DeclarationUUID)
|
||||
assert.Nil(t, varsUpdatedUUIDAfterRemovalHost3, "variables_updated_at should be NULL after removing variable from declaration (host3)")
|
||||
|
||||
// VarSerial.json still has variables — variables_updated_at unchanged on both hosts
|
||||
varsUpdatedSerialAfterRemoval := getHostDeclVarsUpdatedAt(t, host1.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedSerialAfterRemoval)
|
||||
assert.Equal(t, *preVarsUpdatedSerialHost1, *varsUpdatedSerialAfterRemoval, "VarSerial variables_updated_at should be unchanged on host1")
|
||||
varsUpdatedSerialAfterRemovalHost3 := getHostDeclVarsUpdatedAt(t, host3.UUID, dbDeclSerial.DeclarationUUID)
|
||||
require.NotNil(t, varsUpdatedSerialAfterRemovalHost3)
|
||||
assert.Equal(t, *preVarsUpdatedSerialHost3, *varsUpdatedSerialAfterRemovalHost3, "VarSerial variables_updated_at should be unchanged on host3")
|
||||
}
|
||||
|
||||
func declarationForTest(identifier string) []byte {
|
||||
return []byte(fmt.Sprintf(`
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2293,23 +2293,30 @@ func validateFleetVariables(ctx context.Context, ds fleet.Datastore, appConfig *
|
|||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating config profile Fleet variables")
|
||||
}
|
||||
profileVarsByProfIdentifier[p.Identifier] = profileVars
|
||||
profileVarsByProfIdentifier[fleet.MDMAppleProfileUUIDPrefix+p.Identifier] = profileVars
|
||||
}
|
||||
for _, p := range windowsProfiles {
|
||||
windowsVars, err := validateWindowsProfileFleetVariables(string(p.SyncML), lic, groupedCAs)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating Windows profile Fleet variables")
|
||||
}
|
||||
// Collect Fleet variables for Windows profiles (use unique Name as identifier for Windows)
|
||||
if len(windowsVars) > 0 {
|
||||
profileVarsByProfIdentifier[p.Name] = windowsVars
|
||||
profileVarsByProfIdentifier[fleet.MDMWindowsProfileUUIDPrefix+p.Name] = windowsVars
|
||||
}
|
||||
}
|
||||
for _, p := range appleDecls {
|
||||
err = validateDeclarationFleetVariables(string(p.RawJSON))
|
||||
declVars, err := validateDeclarationFleetVariables(string(p.RawJSON), lic)
|
||||
if err != nil {
|
||||
var badReqErr *fleet.BadRequestError
|
||||
if errors.As(err, &badReqErr) {
|
||||
badReqErr.Message = "Couldn't set profile. " + badReqErr.Message
|
||||
err = badReqErr
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating declaration Fleet variables")
|
||||
}
|
||||
if len(declVars) > 0 {
|
||||
profileVarsByProfIdentifier[fleet.MDMAppleDeclarationUUIDPrefix+p.Identifier] = declVars
|
||||
}
|
||||
}
|
||||
return profileVarsByProfIdentifier, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue