implement DDM cron and protocol bits (#17791)

for #17399
This commit is contained in:
Roberto Dip
2024-03-25 17:32:27 -03:00
committed by GitHub
parent d3c843801e
commit 95df7e2b0b
11 changed files with 628 additions and 269 deletions
+3
View File
@@ -1033,6 +1033,9 @@ func newMDMProfileManager(
schedule.WithJob("manage_apple_profiles", func(ctx context.Context) error {
return service.ReconcileAppleProfiles(ctx, ds, commander, logger)
}),
schedule.WithJob("manage_apple_declarations", func(ctx context.Context) error {
return service.ReconcileAppleDeclarations(ctx, ds, commander, logger)
}),
schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error {
return service.ReconcileWindowsProfiles(ctx, ds, logger)
}),
+226 -147
View File
@@ -16,8 +16,8 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
@@ -240,7 +240,7 @@ SELECT
FROM
mdm_apple_declarations
WHERE
declaration_uuid = ? AND category = 'com.apple.configuration'`
declaration_uuid = ?`
var res fleet.MDMAppleDeclaration
err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, declUUID)
@@ -1490,6 +1490,8 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
return nil
}
appleMDMProfilesDesiredStateQuery := generateDesiredStateQuery("profile")
// TODO(mna): the conditions here (and in toRemoveStmt) are subtly different
// than the ones in ListMDMAppleProfilesToInstall/Remove, so I'm keeping
// those statements distinct to avoid introducing a subtle bug, but we should
@@ -1720,20 +1722,30 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
return nil
}
const appleMDMProfilesDesiredStateQuery = `
-- non label-based profiles
// mdmEntityTypeToTable tracks what table should be used in the templates for
// SQL statements based on the given entity type.
var mdmEntityTypeToTable = map[string]string{
"declaration": "declaration",
"profile": "configuration_profile",
}
// generateDesiredStateQuery generates a query string that represents the
// desired state of an Apple entity based on its type (profile or declaration)
func generateDesiredStateQuery(entityType string) string {
return fmt.Sprintf(`
-- non label-based entities
SELECT
macp.profile_uuid,
mae.%[1]s_uuid,
h.uuid as host_uuid,
macp.identifier as profile_identifier,
macp.name as profile_name,
macp.checksum as checksum,
0 as count_profile_labels,
mae.identifier as %[1]s_identifier,
mae.name as %[1]s_name,
mae.checksum as checksum,
0 as count_%[1]s_labels,
0 as count_host_labels
FROM
mdm_apple_configuration_profiles macp
mdm_apple_%[2]ss mae
JOIN hosts h
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
ON h.team_id = mae.team_id OR (h.team_id IS NULL AND mae.team_id = 0)
JOIN nano_enrollments ne
ON ne.device_id = h.uuid
WHERE
@@ -1742,81 +1754,146 @@ const appleMDMProfilesDesiredStateQuery = `
ne.type = 'Device' AND
NOT EXISTS (
SELECT 1
FROM mdm_configuration_profile_labels mcpl
WHERE mcpl.apple_profile_uuid = macp.profile_uuid
FROM mdm_%[2]s_labels mel
WHERE mel.apple_%[1]s_uuid = mae.%[1]s_uuid
) AND
( %s )
( %[3]s )
UNION
-- label-based profiles where the host is a member of all the labels
-- label-based entities where the host is a member of all the labels
SELECT
macp.profile_uuid,
mae.%[1]s_uuid,
h.uuid as host_uuid,
macp.identifier as profile_identifier,
macp.name as profile_name,
macp.checksum as checksum,
COUNT(*) as count_profile_labels,
mae.identifier as %[1]s_identifier,
mae.name as %[1]s_name,
mae.checksum as checksum,
COUNT(*) as count_%[1]s_labels,
COUNT(lm.label_id) as count_host_labels
FROM
mdm_apple_configuration_profiles macp
mdm_apple_%[2]ss mae
JOIN hosts h
ON h.team_id = macp.team_id OR (h.team_id IS NULL AND macp.team_id = 0)
ON h.team_id = mae.team_id OR (h.team_id IS NULL AND mae.team_id = 0)
JOIN nano_enrollments ne
ON ne.device_id = h.uuid
JOIN mdm_configuration_profile_labels mcpl
ON mcpl.apple_profile_uuid = macp.profile_uuid
JOIN mdm_%[2]s_labels mel
ON mel.apple_%[1]s_uuid = mae.%[1]s_uuid
LEFT OUTER JOIN label_membership lm
ON lm.label_id = mcpl.label_id AND lm.host_id = h.id
ON lm.label_id = mel.label_id AND lm.host_id = h.id
WHERE
h.platform = 'darwin' AND
ne.enabled = 1 AND
ne.type = 'Device' AND
( %s )
( %[3]s )
GROUP BY
macp.profile_uuid, h.uuid, macp.identifier, macp.name, macp.checksum
mae.%[1]s_uuid, h.uuid, mae.identifier, mae.name, mae.checksum
HAVING
count_profile_labels > 0 AND count_host_labels = count_profile_labels
`
count_%[1]s_labels > 0 AND count_host_labels = count_%[1]s_labels
`, entityType, mdmEntityTypeToTable[entityType], "%s")
}
// generateEntitiesToInstallQuery is a set difference between:
//
// - Set A (ds), the "desired state", can be obtained from a JOIN between
// mdm_apple_x and hosts.
//
// - Set B, the "current state" given by host_mdm_apple_x.
//
// A - B gives us the entities that need to be installed:
//
// - entities that are in A but not in B
//
// - entities which contents have changed, but their identifier are
// the same (by checking the checksums)
//
// - entities that are in A and in B, but with an operation type of
// "remove", regardless of the status. (technically, if status is NULL then
// the entity should be already installed - it has not been queued for
// remove yet -, and same if status is failed, but the proper thing to do
// with it would be to remove the row, not return it as "to install". For
// simplicity of implementation here (and to err on the safer side - the
// entity's content could've changed), we'll return it as "to install" for
// now, which will cause the row to be updated with the correct operation
// type and status).
//
// - entities that are in A and in B, with an operation type of "install"
// and a NULL status. Other statuses mean that the operation is already in
// flight (pending), the operation has been completed but is still subject
// to independent verification by Fleet (verifying), or has reached a terminal
// state (failed or verified). If the entity's content is edited, all
// relevant hosts will be marked as status NULL so that it gets
// re-installed.
//
// Note that for label-based entities, only fully-satisfied entities are
// considered for installation. This means that a broken label-based entity,
// where one of the labels does not exist anymore, will not be considered for
// installation.
func generateEntitiesToInstallQuery(entityType string) string {
return fmt.Sprintf(`
( %[3]s ) as ds
LEFT JOIN host_mdm_apple_%[1]ss hmae
ON hmae.%[1]s_uuid = ds.%[1]s_uuid AND hmae.host_uuid = ds.host_uuid
WHERE
-- entity has been updated
( hmae.checksum != ds.checksum ) OR
-- entity in A but not in B
( hmae.%[1]s_uuid IS NULL AND hmae.host_uuid IS NULL ) OR
-- entities in A and B but with operation type "remove"
( hmae.host_uuid IS NOT NULL AND ( hmae.operation_type = ? OR hmae.operation_type IS NULL ) ) OR
-- entities in A and B with operation type "install" and NULL status
( hmae.host_uuid IS NOT NULL AND hmae.operation_type = ? AND hmae.status IS NULL )
`, entityType, mdmEntityTypeToTable[entityType], fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE"))
}
// generateEntitiesToRemoveQuery is a set difference between:
//
// - Set A (ds), the "desired state", can be obtained from a JOIN between
// mdm_apple_configuration_x and hosts.
//
// - Set B, the "current state" given by host_mdm_apple_x.
//
// B - A gives us the entities that need to be removed:
//
// - entities that are in B but not in A, except those with operation type
// "remove" and a terminal state (failed) or a state indicating
// that the operation is in flight (pending) or the operation has been completed
// but is still subject to independent verification by Fleet (verifying)
// or the operation has been completed and independenly verified by Fleet (verified).
//
// Any other case are entities that are in both B and A, and as such are
// processed by the generateEntitiesToInstallQuery query (since they are in
// both, their desired state is necessarily to be installed).
//
// Note that for label-based entities, only those that are fully-sastisfied
// by the host are considered for install (are part of the desired state used
// to compute the ones to remove). However, as a special case, a broken
// label-based entity will NOT be removed from a host where it was
// previously installed. However, if a host used to satisfy a label-based
// entity but no longer does (and that label-based entity is not "broken"),
// the entity will be removed from the host.
func generateEntitiesToRemoveQuery(entityType string) string {
return fmt.Sprintf(`
( %[3]s ) as ds
RIGHT JOIN host_mdm_apple_%[1]ss hmae
ON hmae.%[1]s_uuid = ds.%[1]s_uuid AND hmae.host_uuid = ds.host_uuid
WHERE
-- entities that are in B but not in A
ds.%[1]s_uuid IS NULL AND ds.host_uuid IS NULL AND
-- except "remove" operations in a terminal state or already pending
( hmae.operation_type IS NULL OR hmae.operation_type != ? OR hmae.status IS NULL ) AND
-- except "would be removed" entities if they are a broken label-based entities
NOT EXISTS (
SELECT 1
FROM mdm_%[2]s_labels mcpl
WHERE
mcpl.apple_%[1]s_uuid = hmae.%[1]s_uuid AND
mcpl.label_id IS NULL
)
`, entityType, mdmEntityTypeToTable[entityType], fmt.Sprintf(generateDesiredStateQuery(entityType), "TRUE", "TRUE"))
}
func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
// The query below is a set difference between:
//
// - Set A (ds), the "desired state", can be obtained from a JOIN between
// mdm_apple_configuration_profiles and hosts.
//
// - Set B, the "current state" given by host_mdm_apple_profiles.
//
// A - B gives us the profiles that need to be installed:
//
// - profiles that are in A but not in B
//
// - profiles which contents have changed, but their identifier are
// the same (by checking the checksums)
//
// - profiles that are in A and in B, but with an operation type of
// "remove", regardless of the status. (technically, if status is NULL then
// the profile should be already installed - it has not been queued for
// remove yet -, and same if status is failed, but the proper thing to do
// with it would be to remove the row, not return it as "to install". For
// simplicity of implementation here (and to err on the safer side - the
// profile's content could've changed), we'll return it as "to install" for
// now, which will cause the row to be updated with the correct operation
// type and status).
//
// - profiles that are in A and in B, with an operation type of "install"
// and a NULL status. Other statuses mean that the operation is already in
// flight (pending), the operation has been completed but is still subject
// to independent verification by Fleet (verifying), or has reached a terminal
// state (failed or verified). If the profile's content is edited, all
// relevant hosts will be marked as status NULL so that it gets
// re-installed.
//
// Note that for label-based profiles, only fully-satisfied profiles are
// considered for installation. This means that a broken label-based profile,
// where one of the labels does not exist anymore, will not be considered for
// installation.
query := fmt.Sprintf(`
SELECT
ds.profile_uuid,
@@ -1824,82 +1901,26 @@ func (ds *Datastore) ListMDMAppleProfilesToInstall(ctx context.Context) ([]*flee
ds.profile_identifier,
ds.profile_name,
ds.checksum
FROM ( %s ) as ds
LEFT JOIN host_mdm_apple_profiles hmap
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
WHERE
-- profile has been updated
( hmap.checksum != ds.checksum ) OR
-- profiles in A but not in B
( hmap.profile_uuid IS NULL AND hmap.host_uuid IS NULL ) OR
-- profiles in A and B but with operation type "remove"
( hmap.host_uuid IS NOT NULL AND ( hmap.operation_type = ? OR hmap.operation_type IS NULL ) ) OR
-- profiles in A and B with operation type "install" and NULL status
( hmap.host_uuid IS NOT NULL AND hmap.operation_type = ? AND hmap.status IS NULL )
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "TRUE", "TRUE"))
FROM %s `,
generateEntitiesToInstallQuery("profile"))
var profiles []*fleet.MDMAppleProfilePayload
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeRemove, fleet.MDMOperationTypeInstall)
return profiles, err
}
func (ds *Datastore) ListMDMAppleProfilesToRemove(ctx context.Context) ([]*fleet.MDMAppleProfilePayload, error) {
// The query below is a set difference between:
//
// - Set A (ds), the "desired state", can be obtained from a JOIN between
// mdm_apple_configuration_profiles and hosts.
//
// - Set B, the "current state" given by host_mdm_apple_profiles.
//
// B - A gives us the profiles that need to be removed:
//
// - profiles that are in B but not in A, except those with operation type
// "remove" and a terminal state (failed) or a state indicating
// that the operation is in flight (pending) or the operation has been completed
// but is still subject to independent verification by Fleet (verifying)
// or the operation has been completed and independenly verified by Fleet (verified).
//
// Any other case are profiles that are in both B and A, and as such are
// processed by the ListMDMAppleProfilesToInstall method (since they are in
// both, their desired state is necessarily to be installed).
//
// Note that for label-based profiles, only those that are fully-sastisfied
// by the host are considered for install (are part of the desired state used
// to compute the ones to remove). However, as a special case, a broken
// label-based profile will NOT be removed from a host where it was
// previously installed. However, if a host used to satisfy a label-based
// profile but no longer does (and that label-based profile is not "broken"),
// the profile will be removed from the host.
query := fmt.Sprintf(`
SELECT
hmap.profile_uuid,
hmap.profile_identifier,
hmap.profile_name,
hmap.host_uuid,
hmap.checksum,
hmap.operation_type,
COALESCE(hmap.detail, '') as detail,
hmap.status,
hmap.command_uuid
FROM ( %s ) as ds
RIGHT JOIN host_mdm_apple_profiles hmap
ON hmap.profile_uuid = ds.profile_uuid AND hmap.host_uuid = ds.host_uuid
WHERE
-- profiles that are in B but not in A
ds.profile_uuid IS NULL AND ds.host_uuid IS NULL AND
-- except "remove" operations in a terminal state or already pending
( hmap.operation_type IS NULL OR hmap.operation_type != ? OR hmap.status IS NULL ) AND
-- except "would be removed" profiles if they are a broken label-based profile
NOT EXISTS (
SELECT 1
FROM mdm_configuration_profile_labels mcpl
WHERE
mcpl.apple_profile_uuid = hmap.profile_uuid AND
mcpl.label_id IS NULL
)
`, fmt.Sprintf(appleMDMProfilesDesiredStateQuery, "TRUE", "TRUE"))
hmae.profile_uuid,
hmae.profile_identifier,
hmae.profile_name,
hmae.host_uuid,
hmae.checksum,
hmae.operation_type,
COALESCE(hmae.detail, '') as detail,
hmae.status,
hmae.command_uuid
FROM %s`, generateEntitiesToRemoveQuery("profile"))
var profiles []*fleet.MDMAppleProfilePayload
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, query, fleet.MDMOperationTypeRemove)
return profiles, err
@@ -3166,14 +3187,13 @@ INSERT INTO mdm_apple_declarations (
declaration_uuid,
identifier,
name,
category,
raw_json,
checksum,
uploaded_at,
team_id
)
VALUES (
?,?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP(),?
?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP(),?
)
ON DUPLICATE KEY UPDATE
uploaded_at = IF(checksum = VALUES(checksum) AND name = VALUES(name), uploaded_at, CURRENT_TIMESTAMP()),
@@ -3283,7 +3303,6 @@ WHERE
declUUID,
d.Identifier,
d.Name,
d.Category,
d.RawJSON,
checksum,
declTeamID); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "insert") {
@@ -3320,11 +3339,10 @@ INSERT INTO mdm_apple_declarations (
team_id,
identifier,
name,
category,
raw_json,
checksum,
uploaded_at)
(SELECT ?,?,?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP() FROM DUAL WHERE
(SELECT ?,?,?,?,?,UNHEX(?),CURRENT_TIMESTAMP() FROM DUAL WHERE
NOT EXISTS (
SELECT 1 FROM mdm_windows_configuration_profiles WHERE name = ? AND team_id = ?
) AND NOT EXISTS (
@@ -3339,7 +3357,7 @@ INSERT INTO mdm_apple_declarations (
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
res, err := tx.ExecContext(ctx, stmt,
declUUID, tmID, declaration.Identifier, declaration.Name, declaration.Category, declaration.RawJSON, checksum, declaration.Name, tmID, declaration.Name, tmID)
declUUID, tmID, declaration.Identifier, declaration.Name, declaration.RawJSON, checksum, declaration.Name, tmID, declaration.Name, tmID)
if err != nil {
switch {
case isDuplicate(err):
@@ -3473,8 +3491,7 @@ func (ds *Datastore) MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID s
const stmt = `
SELECT
HEX(mad.checksum) as checksum,
mad.identifier,
mad.category
mad.identifier
FROM
host_mdm_apple_declarations hmad
JOIN mdm_apple_declarations mad ON mad.declaration_uuid = hmad.declaration_uuid
@@ -3489,7 +3506,7 @@ WHERE
return res, nil
}
func (ds *Datastore) MDMAppleDDMDeclarationsResponse(ctx context.Context, declarationType fleet.MDMAppleDeclarationCategory, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error) {
func (ds *Datastore) MDMAppleDDMDeclarationsResponse(ctx context.Context, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error) {
// TODO: When hosts table is indexed by uuid, consider joining on hosts to ensure that the
// declaration for the host's current team is returned. In the case where the specified
// identifier is not unique to the team, the cron should ensure that any conflicting
@@ -3501,15 +3518,77 @@ FROM
host_mdm_apple_declarations hmad
JOIN mdm_apple_declarations mad ON hmad.declaration_uuid = mad.declaration_uuid
WHERE
host_uuid = ? AND identifier = ? AND category = ? AND operation_type = ?`
host_uuid = ? AND identifier = ? AND operation_type = ?`
var res fleet.MDMAppleDeclaration
if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, hostUUID, identifier, declarationType, fleet.MDMOperationTypeInstall); err != nil {
if err := sqlx.GetContext(ctx, ds.reader(ctx), &res, stmt, hostUUID, identifier, fleet.MDMOperationTypeInstall); err != nil {
if err == sql.ErrNoRows {
return nil, notFound(string(declarationType)).WithName(identifier)
return nil, notFound("MDMAppleDeclaration").WithName(identifier)
}
return nil, ctxerr.Wrap(ctx, err, "get ddm declarations response")
}
return &res, nil
}
func (ds *Datastore) MDMAppleBatchInsertHostDeclarations(ctx context.Context, changedDeclarations []*fleet.MDMAppleHostDeclaration) error {
baseStmt := `
INSERT INTO host_mdm_apple_declarations
(host_uuid, status, operation_type, checksum, declaration_uuid, declaration_identifier, declaration_name)
VALUES
%s
ON DUPLICATE KEY UPDATE
status = VALUES(status),
operation_type = VALUES(operation_type),
checksum = VALUES(checksum)
`
var placeholders strings.Builder
var args []any
for _, d := range changedDeclarations {
placeholders.WriteString("(?, 'pending', ?, ?, ?, ?, ?),")
args = append(args, d.HostUUID, d.OperationType, d.Checksum, d.DeclarationUUID, d.Identifier, d.Name)
}
_, err := ds.writer(ctx).ExecContext(
ctx,
fmt.Sprintf(baseStmt, strings.TrimSuffix(placeholders.String(), ",")),
args...,
)
return ctxerr.Wrap(ctx, err, "inserting changed host declaration state")
}
func (ds *Datastore) MDMAppleGetHostsWithChangedDeclarations(ctx context.Context) ([]*fleet.MDMAppleHostDeclaration, error) {
stmt := fmt.Sprintf(`
(
SELECT
ds.host_uuid,
'install' as operation_type,
ds.checksum,
ds.declaration_uuid,
ds.declaration_identifier as identifier,
ds.declaration_name as name
FROM
%s
)
UNION ALL
(
SELECT
hmae.host_uuid,
'remove' as operation_type,
hmae.checksum,
hmae.declaration_uuid,
hmae.declaration_identifier as identifier,
hmae.declaration_name as name
FROM
%s
)
`,
generateEntitiesToInstallQuery("declaration"),
generateEntitiesToRemoveQuery("declaration"),
)
var decls []*fleet.MDMAppleHostDeclaration
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &decls, stmt, fleet.MDMOperationTypeRemove, fleet.MDMOperationTypeInstall, fleet.MDMOperationTypeRemove); err != nil {
return nil, ctxerr.Wrap(ctx, err, "running sql statement")
}
return decls, nil
}
-2
View File
@@ -1062,7 +1062,6 @@ func expectAppleDeclarations(
require.Equal(t, wantD.Name, gotD.Name)
require.Equal(t, wantD.Identifier, gotD.Identifier)
require.Equal(t, wantD.Labels, gotD.Labels)
require.Equal(t, wantD.Category, gotD.Category)
}
return m
}
@@ -1258,7 +1257,6 @@ func declForTest(name, identifier, payloadContent string, labels ...*fleet.Label
decl := &fleet.MDMAppleDeclaration{
RawJSON: declBytes,
Category: fleet.MDMAppleDeclarativeConfiguration,
Identifier: fmt.Sprintf("com.fleet.config%s", identifier),
Name: name,
}
+1 -2
View File
@@ -182,7 +182,6 @@ FROM (
uploaded_at
FROM mdm_apple_declarations
WHERE team_id = ?
AND category <> ?
) as combined_profiles
`
@@ -202,7 +201,7 @@ FROM (
fleetNames = append(fleetNames, k)
}
args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames, globalOrTeamID, fleet.MDMAppleDeclarativeActivation}
args := []any{globalOrTeamID, fleetIdentifiers, globalOrTeamID, fleetNames, globalOrTeamID}
stmt, args := appendListOptionsWithCursorToSQL(selectStmt, args, &opt)
stmt, args, err := sqlx.In(stmt, args...)
@@ -11,24 +11,6 @@ func init() {
func Up_20240314150853(tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE mdm_apple_declaration_categories (
declaration_category varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (declaration_category)
)
`)
if err != nil {
return fmt.Errorf("creating mdm_apple_declaration_categories table: %w", err)
}
_, err = tx.Exec(`
INSERT INTO mdm_apple_declaration_categories
VALUES ('com.apple.configuration'), ('com.apple.activation')
`)
if err != nil {
return fmt.Errorf("inserting default values into mdm_apple_declaration_categories table: %w", err)
}
_, err = tx.Exec(`
CREATE TABLE mdm_apple_declarations (
-- declaration_uuid is used as the primary key of the declaration
declaration_uuid varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
@@ -42,10 +24,6 @@ CREATE TABLE mdm_apple_declarations (
-- name is the name of the declaration
name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
-- category is the category of the declaration (activation,
-- declaration, management or asset)
category varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
-- raw_json contains a JSON blob with the declaration contents
raw_json json NOT NULL,
@@ -57,8 +35,7 @@ CREATE TABLE mdm_apple_declarations (
PRIMARY KEY (declaration_uuid),
UNIQUE KEY idx_mdm_apple_declaration_team_identifier (team_id, identifier),
UNIQUE KEY idx_mdm_apple_declaration_team_name (team_id, name),
CONSTRAINT mdm_apple_declaration_category FOREIGN KEY (category) REFERENCES mdm_apple_declaration_categories (declaration_category) ON DELETE CASCADE
UNIQUE KEY idx_mdm_apple_declaration_team_name (team_id, name)
)
`)
if err != nil {
+1 -12
View File
@@ -660,29 +660,18 @@ CREATE TABLE `mdm_apple_declaration_activation_references` (
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `mdm_apple_declaration_categories` (
`declaration_category` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`declaration_category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO `mdm_apple_declaration_categories` VALUES ('com.apple.activation'),('com.apple.configuration');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `mdm_apple_declarations` (
`declaration_uuid` varchar(37) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
`team_id` int(10) unsigned NOT NULL DEFAULT '0',
`identifier` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`category` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`raw_json` json NOT NULL,
`checksum` binary(16) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`uploaded_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`declaration_uuid`),
UNIQUE KEY `idx_mdm_apple_declaration_team_identifier` (`team_id`,`identifier`),
UNIQUE KEY `idx_mdm_apple_declaration_team_name` (`team_id`,`name`),
KEY `mdm_apple_declaration_category` (`category`),
CONSTRAINT `mdm_apple_declaration_category` FOREIGN KEY (`category`) REFERENCES `mdm_apple_declaration_categories` (`declaration_category`) ON DELETE CASCADE
UNIQUE KEY `idx_mdm_apple_declaration_team_name` (`team_id`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
+7 -27
View File
@@ -450,6 +450,8 @@ const (
DEPAssignProfileResponseFailed DEPAssignProfileResponseStatus = "FAILED"
)
const MDMAppleDeclarationUUIDPrefix = "d"
// NanoEnrollment represents a row in the nano_enrollments table managed by
// nanomdm. It is meant to be used internally by the server, not to be returned
// as part of endpoints, and as a precaution its json-encoding is explicitly
@@ -533,25 +535,6 @@ type SCEPIdentityAssociation struct {
RenewCommandUUID string `db:"renew_command_uuid"`
}
// MDMAppleDeclarationCategory is the type for the supported declaration types.
type MDMAppleDeclarationCategory string
const (
// MDMAppleConfigurationDeclaration is the value for [configuration][1] declarations
//
// [1]: https://developer.apple.com/documentation/devicemanagement/declarations#3813088
MDMAppleDeclarativeConfiguration MDMAppleDeclarationCategory = "com.apple.configuration"
// MDMAppleActivationConfiguration is the value for [activation][1] declarations
//
// [1]: https://developer.apple.com/documentation/devicemanagement/declarations#3829708
MDMAppleDeclarativeActivation MDMAppleDeclarationCategory = "com.apple.activation"
// MDMAppleDeclarationUUIDPrefix is the prefix used to differentiate declaration uuids
// from legacy Apple profile uuids and Windows profile uuids.
MDMAppleDeclarationUUIDPrefix = "d"
)
// MDMAppleDeclaration represents a DDM JSON declaration.
type MDMAppleDeclaration struct {
// DeclarationUUID is the unique identifier of the declaration in
@@ -572,10 +555,6 @@ type MDMAppleDeclaration struct {
// Fleet requires that Name must be unique in combination with the Identifier and TeamID.
Name string `db:"name" json:"name"`
// Category is the category of the declaration, at the moment we
// only support configurations and activations.
Category MDMAppleDeclarationCategory `db:"category"`
// RawJSON is the raw JSON content of the declaration
RawJSON json.RawMessage `db:"raw_json" json:"-"`
@@ -604,7 +583,6 @@ var ForbiddenDeclTypes = map[string]struct{}{
"com.apple.configuration.account.google": {},
"com.apple.configuration.account.ldap": {},
"com.apple.configuration.account.mail": {},
"com.apple.configuration.management.test": {},
"com.apple.configuration.screensharing.connection": {},
"com.apple.configuration.security.certificate": {},
"com.apple.configuration.security.identity": {},
@@ -629,7 +607,7 @@ func (r *MDMAppleRawDeclaration) ValidateUserProvided() error {
return NewInvalidArgumentError(r.Type, "Declaration profile cant include status subscription type. To get hosts vitals, please use queries and policies.")
}
if !strings.HasPrefix(r.Type, string(MDMAppleDeclarativeConfiguration)) {
if !strings.HasPrefix(r.Type, "com.apple.configuration") {
return NewInvalidArgumentError(r.Type, "Only configuration declarations (com.apple.configuration) are supported.")
}
@@ -671,12 +649,15 @@ type MDMAppleHostDeclaration struct {
// Detail contains any messages that must be surfaced to the user,
// either by the MDM protocol or the Fleet server.
Detail string `db:"detail" json:"detail"`
// Checksum contains the MD5 checksum of the declaration JSON uploaded
// by the IT admin. Fleet uses this value as the ServerToken.
Checksum string `db:"checksum" json:"-"`
}
func NewMDMAppleDeclaration(raw []byte, teamID *uint, name string, declType, ident string) *MDMAppleDeclaration {
var decl MDMAppleDeclaration
decl.Category = MDMAppleDeclarationCategory(strings.Join(strings.Split(declType, ".")[:3], "."))
decl.Identifier = ident
decl.Name = name
decl.RawJSON = raw
@@ -733,7 +714,6 @@ type MDMAppleDDMManifest struct {
// https://developer.apple.com/documentation/devicemanagement/declarationitemsresponse
type MDMAppleDDMDeclarationItem struct {
Identifier string `db:"identifier"`
Category string `db:"category"`
ServerToken string `db:"checksum"`
}
+10 -1
View File
@@ -1161,7 +1161,16 @@ type Datastore interface {
// MDMAppleDDMDeclarationItems returns the declaration items for the specified host UUID.
MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID string) ([]MDMAppleDDMDeclarationItem, error)
// MDMAppleDDMDeclarationPayload returns the declaration payload for the specified identifier and team.
MDMAppleDDMDeclarationsResponse(ctx context.Context, declarationType MDMAppleDeclarationCategory, identifier string, hostUUID string) (*MDMAppleDeclaration, error)
MDMAppleDDMDeclarationsResponse(ctx context.Context, identifier string, hostUUID string) (*MDMAppleDeclaration, error)
// MDMAppleGetHostsWithChangedDeclarations returns a
// MDMAppleHostDeclaration item for each (host x declaration) pair that
// needs an status change, this includes declarations to install and
// declarations to be removed. Those can be differentiated by the
// OperationType field on each struct.
MDMAppleGetHostsWithChangedDeclarations(ctx context.Context) ([]*MDMAppleHostDeclaration, error)
// MDMAppleBatchInsertHostDeclarations tracks the current status of all
// the host declarations provided.
MDMAppleBatchInsertHostDeclarations(ctx context.Context, changedDeclarations []*MDMAppleHostDeclaration) error
///////////////////////////////////////////////////////////////////////////////
// Microsoft MDM
+27 -3
View File
@@ -762,7 +762,11 @@ type MDMAppleDDMDeclarationsTokenFunc func(ctx context.Context, hostUUID string)
type MDMAppleDDMDeclarationItemsFunc func(ctx context.Context, hostUUID string) ([]fleet.MDMAppleDDMDeclarationItem, error)
type MDMAppleDDMDeclarationsResponseFunc func(ctx context.Context, declarationType fleet.MDMAppleDeclarationCategory, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error)
type MDMAppleDDMDeclarationsResponseFunc func(ctx context.Context, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error)
type MDMAppleGetHostsWithChangedDeclarationsFunc func(ctx context.Context) ([]*fleet.MDMAppleHostDeclaration, error)
type MDMAppleBatchInsertHostDeclarationsFunc func(ctx context.Context, changedDeclarations []*fleet.MDMAppleHostDeclaration) error
type WSTEPStoreCertificateFunc func(ctx context.Context, name string, crt *x509.Certificate) error
@@ -1988,6 +1992,12 @@ type DataStore struct {
MDMAppleDDMDeclarationsResponseFunc MDMAppleDDMDeclarationsResponseFunc
MDMAppleDDMDeclarationsResponseFuncInvoked bool
MDMAppleGetHostsWithChangedDeclarationsFunc MDMAppleGetHostsWithChangedDeclarationsFunc
MDMAppleGetHostsWithChangedDeclarationsFuncInvoked bool
MDMAppleBatchInsertHostDeclarationsFunc MDMAppleBatchInsertHostDeclarationsFunc
MDMAppleBatchInsertHostDeclarationsFuncInvoked bool
WSTEPStoreCertificateFunc WSTEPStoreCertificateFunc
WSTEPStoreCertificateFuncInvoked bool
@@ -4751,11 +4761,25 @@ func (s *DataStore) MDMAppleDDMDeclarationItems(ctx context.Context, hostUUID st
return s.MDMAppleDDMDeclarationItemsFunc(ctx, hostUUID)
}
func (s *DataStore) MDMAppleDDMDeclarationsResponse(ctx context.Context, declarationType fleet.MDMAppleDeclarationCategory, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error) {
func (s *DataStore) MDMAppleDDMDeclarationsResponse(ctx context.Context, identifier string, hostUUID string) (*fleet.MDMAppleDeclaration, error) {
s.mu.Lock()
s.MDMAppleDDMDeclarationsResponseFuncInvoked = true
s.mu.Unlock()
return s.MDMAppleDDMDeclarationsResponseFunc(ctx, declarationType, identifier, hostUUID)
return s.MDMAppleDDMDeclarationsResponseFunc(ctx, identifier, hostUUID)
}
func (s *DataStore) MDMAppleGetHostsWithChangedDeclarations(ctx context.Context) ([]*fleet.MDMAppleHostDeclaration, error) {
s.mu.Lock()
s.MDMAppleGetHostsWithChangedDeclarationsFuncInvoked = true
s.mu.Unlock()
return s.MDMAppleGetHostsWithChangedDeclarationsFunc(ctx)
}
func (s *DataStore) MDMAppleBatchInsertHostDeclarations(ctx context.Context, changedDeclarations []*fleet.MDMAppleHostDeclaration) error {
s.mu.Lock()
s.MDMAppleBatchInsertHostDeclarationsFuncInvoked = true
s.mu.Unlock()
return s.MDMAppleBatchInsertHostDeclarationsFunc(ctx, changedDeclarations)
}
func (s *DataStore) WSTEPStoreCertificate(ctx context.Context, name string, crt *x509.Certificate) error {
+91 -21
View File
@@ -2710,6 +2710,55 @@ func ensureFleetdConfig(ctx context.Context, ds fleet.Datastore, logger kitlog.L
return nil
}
func ReconcileAppleDeclarations(
ctx context.Context,
ds fleet.Datastore,
commander *apple_mdm.MDMAppleCommander,
logger kitlog.Logger,
) error {
// once all the declarations are in place, compute the desired state
// and find which hosts need a DDM sync.
changedDeclarations, err := ds.MDMAppleGetHostsWithChangedDeclarations(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "find hosts with changed declarations")
}
if len(changedDeclarations) == 0 {
logger.Log("msg", "no hosts with changed declarations")
return nil
}
// a host might have more than one declaration to sync, we do this to
// collect unique host UUIDs in order to send a single command to each
// host in the next step
uuidMap := map[string]struct{}{}
for _, d := range changedDeclarations {
uuidMap[d.HostUUID] = struct{}{}
}
uuids := make([]string, 0, len(uuidMap))
for uuid := range uuidMap {
uuids = append(uuids, uuid)
}
// mark the host declarations as pending, this serves two purposes:
//
// - support the APIs/methods that track host status (summaries, filters, etc)
//
// - support the DDM endpoints, which use data from the
// `host_mdm_apple_declarations` table to compute which declarations to
// serve
if err := ds.MDMAppleBatchInsertHostDeclarations(ctx, changedDeclarations); err != nil {
return ctxerr.Wrap(ctx, err, "batch insert mdm apple host declarations")
}
// send a DeclarativeManagement command to start a sync
if err := commander.DeclarativeManagement(ctx, uuids, uuid.NewString()); err != nil {
return ctxerr.Wrap(ctx, err, "issuing DeclarativeManagement command")
}
return nil
}
func ReconcileAppleProfiles(
ctx context.Context,
ds fleet.Datastore,
@@ -3233,16 +3282,11 @@ func (svc *MDMAppleDDMService) handleDeclarationItems(ctx context.Context, hostU
activations := []fleet.MDMAppleDDMManifest{}
configurations := []fleet.MDMAppleDDMManifest{}
for _, d := range di {
manifest := fleet.MDMAppleDDMManifest{Identifier: d.Identifier, ServerToken: d.ServerToken}
switch d.Category {
case string(fleet.MDMAppleDeclarativeActivation):
activations = append(activations, manifest)
case string(fleet.MDMAppleDeclarativeConfiguration):
configurations = append(configurations, manifest)
default:
level.Debug(svc.logger).Log("msg", "unrecognized declaration category", "category", d.Category)
return nil, ctxerr.New(ctx, "unrecognized declaration category")
}
configurations = append(configurations, fleet.MDMAppleDDMManifest(d))
activations = append(activations, fleet.MDMAppleDDMManifest{
Identifier: fmt.Sprintf("%s.activation", d.Identifier),
ServerToken: d.ServerToken,
})
}
// TODO: Look for ways to optimize the declaration item query so that we don't have to get the declarations token separately.
@@ -3274,11 +3318,43 @@ func (svc *MDMAppleDDMService) handleDeclarationsResponse(ctx context.Context, e
}
level.Debug(svc.logger).Log("msg", "parsed declarations request", "type", parts[1], "identifier", parts[2])
d, err := svc.ds.MDMAppleDDMDeclarationsResponse(
ctx, fleet.MDMAppleDeclarationCategory("com.apple."+parts[1]),
parts[2],
hostUUID,
)
switch parts[1] {
case "activation":
return svc.handleActivationDeclaration(ctx, parts, hostUUID)
case "configuration":
return svc.handleConfigurationDeclaration(ctx, parts, hostUUID)
default:
return nil, newNotFoundError()
}
}
func (svc *MDMAppleDDMService) handleActivationDeclaration(ctx context.Context, parts []string, hostUUID string) ([]byte, error) {
references := strings.TrimSuffix(parts[2], ".activation")
// ensure the declaration for the requested activation stil exists
d, err := svc.ds.MDMAppleDDMDeclarationsResponse(ctx, references, hostUUID)
if err != nil {
if fleet.IsNotFound(err) {
return nil, nano_service.NewHTTPStatusError(http.StatusNotFound, err)
}
return nil, ctxerr.Wrap(ctx, err, "getting linked configuration for activation declaration")
}
response := fmt.Sprintf(`
{
"Identifier": "%s",
"Payload": {
"StandardConfigurations": ["%s"]
},
"ServerToken": "%s",
"Type": "com.apple.activation.simple"
}`, parts[2], references, d.Checksum)
return []byte(response), nil
}
func (svc *MDMAppleDDMService) handleConfigurationDeclaration(ctx context.Context, parts []string, hostUUID string) ([]byte, error) {
d, err := svc.ds.MDMAppleDDMDeclarationsResponse(ctx, parts[2], hostUUID)
if err != nil {
if fleet.IsNotFound(err) {
return nil, nano_service.NewHTTPStatusError(http.StatusNotFound, err)
@@ -3286,12 +3362,6 @@ func (svc *MDMAppleDDMService) handleDeclarationsResponse(ctx context.Context, e
return nil, ctxerr.Wrap(ctx, err, "getting declaration response")
}
// unmarshall into a temporary map in order to add the token.
// we do this at this stage because tokens are purely managed by Fleet,
// and we don't want to store a modified version of what's provided by
// the IT admin.
//
// This mimics what we do for CommandUUID, but can be revisited.
var tempd map[string]any
if err := json.Unmarshal(d.RawJSON, &tempd); err != nil {
return nil, ctxerr.Wrap(ctx, err, "unmarshaling stored declaration")
+261 -30
View File
@@ -2755,9 +2755,14 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
mdmDeviceA := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)
err := mdmDeviceA.Enroll()
require.NoError(t, err)
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), 0)
mdmDeviceB := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo)
err = mdmDeviceB.Enroll()
require.NoError(t, err)
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), 0)
// Find the ID of Fleet's MDM solution
var mdmID uint
@@ -2786,23 +2791,6 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
}
}
// Activities are generated for each device
activities := listActivitiesResponse{}
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities, "order_key", "created_at")
require.GreaterOrEqual(t, len(activities.Activities), 2)
details := []*json.RawMessage{}
for _, activity := range activities.Activities {
if activity.Type == "mdm_enrolled" {
require.Nil(t, activity.ActorID)
require.Nil(t, activity.ActorFullName)
details = append(details, activity.Details)
}
}
require.Len(t, details, 2)
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*details[len(details)-2]))
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), string(*details[len(details)-1]))
// set an enroll secret
var applyResp applyEnrollSecretSpecResponse
s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
@@ -2839,7 +2827,7 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
require.NoError(t, err)
// An activity is created
activities = listActivitiesResponse{}
activities := listActivitiesResponse{}
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)
found := false
@@ -2848,7 +2836,6 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
found = true
require.Nil(t, activity.ActorID)
require.Nil(t, activity.ActorFullName)
details = append(details, activity.Details)
require.JSONEq(t, fmt.Sprintf(`{"host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*activity.Details))
}
}
@@ -9472,9 +9459,9 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
var profileLabels []fleet.ConfigurationProfileLabel
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
stmt := `
SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, label_id
SELECT COALESCE(apple_profile_uuid, windows_profile_uuid) as profile_uuid, label_name, COALESCE(label_id, 0) as label_id
FROM mdm_configuration_profile_labels
UNION SELECT apple_declaration_uuid as profile_uuid, label_name, label_id
UNION SELECT apple_declaration_uuid as profile_uuid, label_name, COALESCE(label_id, 0) as label_id
FROM mdm_declaration_labels ORDER BY profile_uuid, label_name;`
return sqlx.SelectContext(context.Background(), q, &profileLabels, stmt)
})
@@ -12783,12 +12770,11 @@ INSERT INTO mdm_apple_declarations (
team_id,
identifier,
name,
category,
raw_json,
checksum,
created_at,
uploaded_at
) VALUES (?,?,?,?,?,?,UNHEX(?),?,?)`
) VALUES (?,?,?,?,?,UNHEX(?),?,?)`
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(context.Background(), stmt,
@@ -12796,7 +12782,6 @@ INSERT INTO mdm_apple_declarations (
decl.TeamID,
decl.Identifier,
decl.Name,
decl.Category,
decl.RawJSON,
calcChecksum(decl.RawJSON),
decl.CreatedAt,
@@ -12839,7 +12824,6 @@ INSERT INTO host_mdm_apple_declarations (
TeamID: ptr.Uint(0),
Identifier: "com.example",
Name: "Example",
Category: fleet.MDMAppleDeclarativeConfiguration,
RawJSON: json.RawMessage(`{
"Type": "com.apple.configuration.declaration-items.test",
"Payload": {"foo":"bar"},
@@ -12904,7 +12888,6 @@ INSERT INTO host_mdm_apple_declarations (
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotParsed))
require.EqualValues(t, wantParsed.Payload, gotParsed.Payload)
require.Equal(t, calcChecksum(expected.RawJSON), gotParsed.ServerToken)
require.Contains(t, gotParsed.Type, expected.Category)
require.Equal(t, expected.Identifier, gotParsed.Identifier)
// t.Logf("decoded: %+v", gotParsed)
}
@@ -12917,7 +12900,8 @@ INSERT INTO host_mdm_apple_declarations (
checkDeclarationItemsResp := func(t *testing.T, r fleet.MDMAppleDDMDeclarationItemsResponse, expectedDeclTok string, expectedDeclsByChecksum map[string]fleet.MDMAppleDeclaration) {
require.Equal(t, expectedDeclTok, r.DeclarationsToken)
require.Empty(t, r.Declarations.Activations)
// TODO(roberto): better assertions
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(expectedDeclsByChecksum))
@@ -12944,7 +12928,6 @@ INSERT INTO host_mdm_apple_declarations (
TeamID: ptr.Uint(0),
Identifier: "com.example2",
Name: "Example2",
Category: fleet.MDMAppleDeclarativeConfiguration,
RawJSON: json.RawMessage(`{
"Type": "com.apple.configuration.declaration-items.test",
"Payload": {"foo":"baz"},
@@ -12975,7 +12958,6 @@ INSERT INTO host_mdm_apple_declarations (
TeamID: ptr.Uint(0),
Identifier: "com.example3",
Name: "Example3",
Category: fleet.MDMAppleDeclarativeConfiguration,
RawJSON: json.RawMessage(`{
"Type": "com.apple.configuration.declaration-items.test",
"Payload": {"foo":"bang"},
@@ -13017,7 +12999,6 @@ INSERT INTO host_mdm_apple_declarations (
TeamID: ptr.Uint(0),
Identifier: "com.example4",
Name: "Example4",
Category: fleet.MDMAppleDeclarativeConfiguration,
RawJSON: json.RawMessage(`{
"Type": "com.apple.configuration.test",
"Payload": {"foo":"bar"},
@@ -13045,3 +13026,253 @@ INSERT INTO host_mdm_apple_declarations (
assertDeclarationResponse(r, want)
})
}
func (s *integrationMDMTestSuite) TestAppleDDMReconciliation() {
t := s.T()
ctx := context.Background()
// TODO: use config logger or take into account FLEET_INTEGRATION_TESTS_DISABLE_LOG
logger := kitlog.NewJSONLogger(os.Stdout)
// TODO: use endpoints once those are available.
addDeclaration := func(identifier string, teamID uint) {
stmt := `
INSERT INTO mdm_apple_declarations
(declaration_uuid, team_id, identifier, name, raw_json, checksum)
VALUES
(UUID(), ?, ?, UUID(), ?, HEX(MD5(raw_json)) )`
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, stmt, teamID, identifier, declarationForTest(identifier))
return err
})
}
deleteDeclaration := func(identifier string, teamID uint) {
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations WHERE team_id = ? AND identifier = ?", teamID, identifier)
return err
})
}
// create a team
teamName := t.Name() + "team1"
team := &fleet.Team{
Name: teamName,
}
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)
team = createTeamResp.Team
t.Cleanup(func() {
// delete declarations to not affect other tests
deleteDeclaration("I2", 0)
deleteDeclaration("I1", team.ID)
deleteDeclaration("I2", team.ID)
deleteDeclaration("I3", team.ID)
})
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)
}
// create a windows host
_, err := s.ds.NewHost(context.Background(), &fleet.Host{
ID: 1,
OsqueryHostID: ptr.String("non-macos-host"),
NodeKey: ptr.String("non-macos-host"),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local.non.macos", t.Name()),
Platform: "windows",
})
require.NoError(t, err)
// create a windows host that's enrolled in MDM
_, _ = createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
// create a linux host
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
ID: 2,
OsqueryHostID: ptr.String("linux-host"),
NodeKey: ptr.String("linux-host"),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local.linux", t.Name()),
Platform: "linux",
})
require.NoError(t, err)
// create a host that's not enrolled into MDM
_, err = s.ds.NewHost(context.Background(), &fleet.Host{
ID: 2,
OsqueryHostID: ptr.String("not-mdm-enrolled"),
NodeKey: ptr.String("not-mdm-enrolled"),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local.not.enrolled", t.Name()),
Platform: "darwin",
})
require.NoError(t, err)
// create a host and then enroll in MDM.
mdmHost, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)
// trigger the reconciler, no error
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// declarativeManagement command is not sent.
checkNoCommands(device)
// add global declarations
addDeclaration("I1", 0)
addDeclaration("I2", 0)
// reconcile again, this time new declarations were added
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// TODO: check command is pending
// declarativeManagement command is sent
checkDDMSync(device)
// reconcile again, commands for the uploaded declarations are already sent
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// no new commands are sent
checkNoCommands(device)
// delete a declaration
deleteDeclaration("I1", 0)
// reconcile again
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// a DDM sync is triggered
checkDDMSync(device)
// add a new host
_, deviceTwo := createHostThenEnrollMDM(s.ds, s.server.URL, t)
// reconcile again
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// DDM sync is triggered only for the new host
checkNoCommands(device)
checkDDMSync(deviceTwo)
// add device to the team
s.Do("POST", "/api/v1/fleet/hosts/transfer",
addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{mdmHost.ID}}, http.StatusOK)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// DDM sync is triggered only for the transferred host
// because the team doesn't have any declarations
checkDDMSync(device)
checkNoCommands(deviceTwo)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// nobody receives commands this time
checkNoCommands(device)
checkNoCommands(deviceTwo)
// add declarations to the team
addDeclaration("I1", team.ID)
addDeclaration("I2", team.ID)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// DDM sync is triggered for the host in the team
checkDDMSync(device)
checkNoCommands(deviceTwo)
// add a new host, this one belongs to the team
mdmHostThree, deviceThree := createHostThenEnrollMDM(s.ds, s.server.URL, t)
s.Do("POST", "/api/v1/fleet/hosts/transfer",
addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{mdmHostThree.ID}}, http.StatusOK)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// DDM sync is triggered only for the new host
checkNoCommands(device)
checkNoCommands(deviceTwo)
checkDDMSync(deviceThree)
// no new commands after another reconciliation
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
checkNoCommands(device)
checkNoCommands(deviceTwo)
checkNoCommands(deviceThree)
// TODO: use proper APIs for this
// add a new label + label declaration
addDeclaration("I3", team.ID)
label, err := s.ds.NewLabel(ctx, &fleet.Label{Name: t.Name(), Query: "select 1;"})
require.NoError(t, err)
// update label with host membership
mysql.ExecAdhocSQL(
t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
"INSERT IGNORE INTO label_membership (host_id, label_id) VALUES (?, ?)",
mdmHostThree.ID,
label.ID,
)
return err
},
)
// update declaration <-> label mapping
mysql.ExecAdhocSQL(
t, s.ds, func(db sqlx.ExtContext) error {
_, err := db.ExecContext(
context.Background(),
`INSERT INTO
mdm_declaration_labels (apple_declaration_uuid, label_name, label_id)
VALUES ((SELECT declaration_uuid FROM mdm_apple_declarations WHERE team_id = ? and identifier = ?), ?, ?)`,
team.ID,
"I3",
label.Name,
label.ID,
)
return err
},
)
// reconcile
err = ReconcileAppleDeclarations(ctx, s.ds, s.mdmCommander, logger)
require.NoError(t, err)
// DDM sync is triggered only for the host with the label
checkNoCommands(device)
checkNoCommands(deviceTwo)
checkDDMSync(deviceThree)
}
func declarationForTest(identifier string) []byte {
return []byte(fmt.Sprintf(`
{
"Type": "com.apple.configuration.management.test",
"Payload": {
"Echo": "foo"
},
"Identifier": "%s"
}`, identifier))
}