implement profile verification for windows (#15348)

for #14426.

In order to prevent import cycles and be able to use some type
definitions/constants I followed the same pattern we did for macOS by
creating a new package named `syncml`. This makes the changelog look
bigger than it actually is, so I split it into two commits to make it
easier to review:

-
[d7c233d](d7c233d54c)
moves the relevant bits to this new package
-
[7531a07](7531a0742b)
implements profile verification
This commit is contained in:
Roberto Dip 2023-11-30 09:17:07 -03:00 committed by GitHub
parent 120293e59c
commit b07fbdc1d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1911 additions and 842 deletions

View file

@ -726,7 +726,7 @@ the way that the Fleet server works.
}
}
if appCfg.MDM.EnabledAndConfigured {
if appCfg.MDM.EnabledAndConfigured || appCfg.MDM.WindowsEnabledAndConfigured {
if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
return newMDMProfileManager(
ctx,

View file

@ -23,7 +23,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/sso"
"github.com/fleetdm/fleet/v4/server/worker"
kitlog "github.com/go-kit/kit/log"
@ -1034,7 +1034,7 @@ func (svc *Service) mdmWindowsEnableOSUpdates(ctx context.Context, teamID *uint,
err := svc.ds.SetOrUpdateMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{
TeamID: teamID,
Name: microsoft_mdm.FleetWindowsOSUpdatesProfileName,
Name: syncml.FleetWindowsOSUpdatesProfileName,
SyncML: contents.Bytes(),
})
if err != nil {
@ -1045,6 +1045,6 @@ func (svc *Service) mdmWindowsEnableOSUpdates(ctx context.Context, teamID *uint,
}
func (svc *Service) mdmWindowsDisableOSUpdates(ctx context.Context, teamID *uint) error {
err := svc.ds.DeleteMDMWindowsConfigProfileByTeamAndName(ctx, teamID, microsoft_mdm.FleetWindowsOSUpdatesProfileName)
err := svc.ds.DeleteMDMWindowsConfigProfileByTeamAndName(ctx, teamID, syncml.FleetWindowsOSUpdatesProfileName)
return ctxerr.Wrap(ctx, err, "delete Windows OS updates profile")
}

View file

@ -13,6 +13,7 @@ import (
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
@ -220,10 +221,10 @@ func (c *TestWindowsMDMClient) SendResponse() (map[string]fleet.ProtoCmdOperatio
}
var msg fleet.SyncML
msg.Xmlns = microsoft_mdm.SyncCmdNamespace
msg.Xmlns = syncml.SyncCmdNamespace
msg.SyncHdr = fleet.SyncHdr{
VerDTD: microsoft_mdm.SyncMLSupportedVersion,
VerProto: microsoft_mdm.SyncMLVerProto,
VerDTD: syncml.SyncMLSupportedVersion,
VerProto: syncml.SyncMLVerProto,
SessionID: sessionID,
MsgID: fmt.Sprint(messageIDInt + 1),
Source: &fleet.LocURI{LocURI: &c.DeviceID},
@ -498,11 +499,11 @@ func (c *TestWindowsMDMClient) getToken() (binarySecToken string, tokenValueType
return "", "", err
}
tokenValueType = microsoft_mdm.BinarySecurityAzureEnroll
tokenValueType = syncml.BinarySecurityAzureEnroll
binarySecToken = base64.URLEncoding.EncodeToString([]byte(tokenString))
case fleet.WindowsMDMProgrammaticEnrollmentType:
var err error
tokenValueType = microsoft_mdm.BinarySecurityDeviceEnroll
tokenValueType = syncml.BinarySecurityDeviceEnroll
binarySecToken, err = fleet.GetEncodedBinarySecurityToken(c.enrollmentType, c.tokenIdentifier)
if err != nil {
return "", "", fmt.Errorf("generating encoded security token: %w", err)

View file

@ -1730,209 +1730,6 @@ func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, prof
return err
}
func (ds *Datastore) UpdateHostMDMProfilesVerification(ctx context.Context, hostUUID string, toVerify, toFail, toRetry []string) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if err := setMDMProfilesVerifiedDB(ctx, tx, hostUUID, toVerify); err != nil {
return err
}
if err := setMDMProfilesFailedDB(ctx, tx, hostUUID, toFail); err != nil {
return err
}
if err := setMDMProfilesRetryDB(ctx, tx, hostUUID, toRetry); err != nil {
return err
}
return nil
})
}
// setMDMProfilesRetryDB sets the status of the given identifiers to retry (nil) and increments the retry count
func setMDMProfilesRetryDB(ctx context.Context, tx sqlx.ExtContext, hostUUID string, identifiers []string) error {
if len(identifiers) == 0 {
return nil
}
stmt := `
UPDATE
host_mdm_apple_profiles
SET
status = NULL,
detail = '',
retries = retries + 1
WHERE
host_uuid = ?
AND operation_type = ?
AND profile_identifier IN(?)`
args := []interface{}{
hostUUID,
fleet.MDMOperationTypeInstall,
identifiers,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set retry host macOS profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting retry host macOS profiles")
}
return nil
}
// setMDMProfilesFailedDB sets the status of the given identifiers to failed if the current status
// is verifying or verified. It also sets the detail to a message indicating that the profile was
// either verifying or verified. Only profiles with the install operation type are updated.
func setMDMProfilesFailedDB(ctx context.Context, tx sqlx.ExtContext, hostUUID string, identifiers []string) error {
if len(identifiers) == 0 {
return nil
}
stmt := `
UPDATE
host_mdm_apple_profiles
SET
detail = if(status = ?, ?, ?),
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND profile_identifier IN(?)`
args := []interface{}{
fleet.MDMDeliveryVerifying,
fleet.HostMDMProfileDetailFailedWasVerifying,
fleet.HostMDMProfileDetailFailedWasVerified,
fleet.MDMDeliveryFailed,
hostUUID,
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified},
fleet.MDMOperationTypeInstall,
identifiers,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set failed host macOS profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting failed host macOS profiles")
}
return nil
}
// setMDMProfilesVerifiedDB sets the status of the given identifiers to verified if the current
// status is verifying. Only profiles with the install operation type are updated.
func setMDMProfilesVerifiedDB(ctx context.Context, tx sqlx.ExtContext, hostUUID string, identifiers []string) error {
if len(identifiers) == 0 {
return nil
}
stmt := `
UPDATE
host_mdm_apple_profiles
SET
detail = '',
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND profile_identifier IN(?)`
args := []interface{}{
fleet.MDMDeliveryVerified,
hostUUID,
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryFailed},
fleet.MDMOperationTypeInstall,
identifiers,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set verified host macOS profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting verified host macOS profiles")
}
return nil
}
func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
var teamID uint
if host.TeamID != nil {
teamID = *host.TeamID
}
stmt := `
SELECT
identifier,
earliest_install_date
FROM
mdm_apple_configuration_profiles macp
JOIN (
SELECT
checksum,
min(updated_at) AS earliest_install_date
FROM
mdm_apple_configuration_profiles
GROUP BY
checksum) cs
ON macp.checksum = cs.checksum
WHERE
macp.team_id = ?`
var rows []*fleet.ExpectedMDMProfile
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host %d", host.ID))
}
byIdentifier := make(map[string]*fleet.ExpectedMDMProfile, len(rows))
for _, r := range rows {
byIdentifier[r.Identifier] = r
}
return byIdentifier, nil
}
func (ds *Datastore) GetHostMDMProfilesRetryCounts(ctx context.Context, hostUUID string) ([]fleet.HostMDMProfileRetryCount, error) {
stmt := `
SELECT
profile_identifier,
retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?`
var dest []fleet.HostMDMProfileRetryCount
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, hostUUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry counts for host %s", hostUUID))
}
return dest, nil
}
func (ds *Datastore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, hostUUID, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
stmt := `
SELECT
profile_identifier, retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?
AND hmap.command_uuid = ?`
var dest fleet.HostMDMProfileRetryCount
if err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, hostUUID, cmdUUID); err != nil {
if err == sql.ErrNoRows {
return dest, notFound("HostMDMCommand").WithMessage(fmt.Sprintf("command uuid %s not found for host uuid %s", cmdUUID, hostUUID))
}
return dest, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry count for host %s command uuid %s", hostUUID, cmdUUID))
}
return dest, nil
}
func subqueryHostsMacOSSettingsStatusFailed() (string, []interface{}) {
sql := `
SELECT

View file

@ -4241,7 +4241,7 @@ func testMDMAppleDeleteHostDEPAssignments(t *testing.T, ds *Datastore) {
}
}
func TestMDMProfileVerification(t *testing.T) {
func TestMDMAppleProfileVerification(t *testing.T) {
ds := CreateMySQLDS(t)
ctx := context.Background()

View file

@ -2,13 +2,14 @@ package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/go-kit/kit/log/level"
"github.com/jmoiron/sqlx"
)
@ -161,7 +162,7 @@ FROM (
for k := range fleetIdentsMap {
fleetIdentifiers = append(fleetIdentifiers, k)
}
fleetNamesMap := microsoft_mdm.FleetReservedProfileNames()
fleetNamesMap := syncml.FleetReservedProfileNames()
fleetNames := make([]string, 0, len(fleetNamesMap))
for k := range fleetNamesMap {
fleetNames = append(fleetNames, k)
@ -324,3 +325,306 @@ WHERE
return nil
})
}
func (ds *Datastore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
if err := setMDMProfilesVerifiedDB(ctx, tx, host, toVerify); err != nil {
return err
}
if err := setMDMProfilesFailedDB(ctx, tx, host, toFail); err != nil {
return err
}
if err := setMDMProfilesRetryDB(ctx, tx, host, toRetry); err != nil {
return err
}
return nil
})
}
// setMDMProfilesRetryDB sets the status of the given identifiers to retry (nil) and increments the retry count
func setMDMProfilesRetryDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
status = NULL,
detail = '',
retries = retries + 1
WHERE
host_uuid = ?
AND operation_type = ?
AND %s IN(?)`
args := []interface{}{
host.UUID,
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
var stmt string
switch host.Platform {
case "darwin":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set retry host profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting retry host profiles")
}
return nil
}
// setMDMProfilesFailedDB sets the status of the given identifiers to failed if the current status
// is verifying or verified. It also sets the detail to a message indicating that the profile was
// either verifying or verified. Only profiles with the install operation type are updated.
func setMDMProfilesFailedDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
detail = if(status = ?, ?, ?),
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND %s IN(?)`
var stmt string
switch host.Platform {
case "darwin":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
args := []interface{}{
fleet.MDMDeliveryVerifying,
fleet.HostMDMProfileDetailFailedWasVerifying,
fleet.HostMDMProfileDetailFailedWasVerified,
fleet.MDMDeliveryFailed,
host.UUID,
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerified},
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set failed host profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting failed host profiles")
}
return nil
}
// setMDMProfilesVerifiedDB sets the status of the given identifiers to verified if the current
// status is verifying. Only profiles with the install operation type are updated.
func setMDMProfilesVerifiedDB(ctx context.Context, tx sqlx.ExtContext, host *fleet.Host, identifiersOrNames []string) error {
if len(identifiersOrNames) == 0 {
return nil
}
const baseStmt = `
UPDATE
%s
SET
detail = '',
status = ?
WHERE
host_uuid = ?
AND status IN(?)
AND operation_type = ?
AND %s IN(?)`
var stmt string
switch host.Platform {
case "darwin":
stmt = fmt.Sprintf(baseStmt, "host_mdm_apple_profiles", "profile_identifier")
case "windows":
stmt = fmt.Sprintf(baseStmt, "host_mdm_windows_profiles", "profile_name")
default:
return fmt.Errorf("unsupported platform %s", host.Platform)
}
args := []interface{}{
fleet.MDMDeliveryVerified,
host.UUID,
[]interface{}{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryFailed},
fleet.MDMOperationTypeInstall,
identifiersOrNames,
}
stmt, args, err := sqlx.In(stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "building sql statement to set verified host macOS profiles")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "setting verified host profiles")
}
return nil
}
func (ds *Datastore) GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
var teamID uint
if host.TeamID != nil {
teamID = *host.TeamID
}
switch host.Platform {
case "darwin":
return ds.getHostMDMAppleProfilesExpectedForVerification(ctx, teamID)
case "windows":
return ds.getHostMDMWindowsProfilesExpectedForVerification(ctx, teamID)
default:
return nil, fmt.Errorf("unsupported platform: %s", host.Platform)
}
}
func (ds *Datastore) getHostMDMWindowsProfilesExpectedForVerification(ctx context.Context, teamID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
stmt := `
SELECT name, syncml as raw_profile, updated_at as earliest_install_date
FROM mdm_windows_configuration_profiles mwcp
WHERE mwcp.team_id = ?
`
var profiles []*fleet.ExpectedMDMProfile
err := sqlx.SelectContext(ctx, ds.reader(ctx), &profiles, stmt, teamID)
if err != nil {
return nil, err
}
byName := make(map[string]*fleet.ExpectedMDMProfile, len(profiles))
for _, r := range profiles {
byName[r.Name] = r
}
return byName, nil
}
func (ds *Datastore) getHostMDMAppleProfilesExpectedForVerification(ctx context.Context, teamID uint) (map[string]*fleet.ExpectedMDMProfile, error) {
stmt := `
SELECT
identifier,
earliest_install_date
FROM
mdm_apple_configuration_profiles macp
JOIN (
SELECT
checksum,
min(updated_at) AS earliest_install_date
FROM
mdm_apple_configuration_profiles
GROUP BY
checksum) cs
ON macp.checksum = cs.checksum
WHERE
macp.team_id = ?`
var rows []*fleet.ExpectedMDMProfile
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &rows, stmt, teamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting expected profiles for host in team %d", teamID))
}
byIdentifier := make(map[string]*fleet.ExpectedMDMProfile, len(rows))
for _, r := range rows {
byIdentifier[r.Identifier] = r
}
return byIdentifier, nil
}
func (ds *Datastore) GetHostMDMProfilesRetryCounts(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error) {
const darwinStmt = `
SELECT
profile_identifier,
retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?`
const windowsStmt = `
SELECT
profile_name,
retries
FROM
host_mdm_windows_profiles hmwp
WHERE
hmwp.host_uuid = ?`
var stmt string
switch host.Platform {
case "darwin":
stmt = darwinStmt
case "windows":
stmt = windowsStmt
default:
return nil, fmt.Errorf("unsupported platform %s", host.Platform)
}
var dest []fleet.HostMDMProfileRetryCount
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &dest, stmt, host.UUID); err != nil {
return nil, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry counts for host %s", host.UUID))
}
return dest, nil
}
func (ds *Datastore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
const darwinStmt = `
SELECT
profile_identifier, retries
FROM
host_mdm_apple_profiles hmap
WHERE
hmap.host_uuid = ?
AND hmap.command_uuid = ?`
const windowsStmt = `
SELECT
profile_uuid, retries
FROM
host_mdm_windows_profiles hmwp
WHERE
hmwp.host_uuid = ?
AND hmwp.command_uuid = ?`
var stmt string
switch host.Platform {
case "darwin":
stmt = darwinStmt
case "windows":
stmt = windowsStmt
default:
return fleet.HostMDMProfileRetryCount{}, fmt.Errorf("unsupported platform %s", host.Platform)
}
var dest fleet.HostMDMProfileRetryCount
if err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, host.UUID, cmdUUID); err != nil {
if err == sql.ErrNoRows {
return dest, notFound("HostMDMCommand").WithMessage(fmt.Sprintf("command uuid %s not found for host uuid %s", cmdUUID, host.UUID))
}
return dest, ctxerr.Wrap(ctx, err, fmt.Sprintf("getting retry count for host %s command uuid %s", host.UUID, cmdUUID))
}
return dest, nil
}

View file

@ -9,7 +9,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
@ -326,7 +326,7 @@ func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
require.Equal(t, *meta, fleet.PaginationMetadata{})
// add fleet-managed Windows profiles for the team and globally
for name := range microsoft_mdm.FleetReservedProfileNames() {
for name := range syncml.FleetReservedProfileNames() {
_, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: name, TeamID: &team.ID, SyncML: winProf})
require.NoError(t, err)
_, err = ds.NewMDMWindowsConfigProfile(ctx, fleet.MDMWindowsConfigProfile{Name: name, TeamID: nil, SyncML: winProf})

View file

@ -10,6 +10,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
"github.com/go-kit/kit/log/level"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
@ -220,6 +221,8 @@ WHERE
return commands, nil
}
// TODO(roberto): much of this logic should be living in the service layer,
// would be nice to get the time to properly plan and implement.
func (ds *Datastore) MDMWindowsSaveResponse(ctx context.Context, deviceID string, fullResponse *fleet.SyncML) error {
if len(fullResponse.Raw) == 0 {
return ctxerr.New(ctx, "empty raw response")
@ -356,6 +359,8 @@ ON DUPLICATE KEY UPDATE
// updateMDMWindowsHostProfileStatusFromResponseDB takes a slice of potential
// profile payloads and updates the corresponding `status` and `detail` columns
// in `host_mdm_windows_profiles`
// TODO(roberto): much of this logic should be living in the service layer,
// would be nice to get the time to properly plan and implement.
func updateMDMWindowsHostProfileStatusFromResponseDB(
ctx context.Context,
tx sqlx.ExtContext,
@ -371,16 +376,17 @@ func updateMDMWindowsHostProfileStatusFromResponseDB(
// update their detail and status.
const updateHostProfilesStmt = `
INSERT INTO host_mdm_windows_profiles
(host_uuid, profile_uuid, detail, status, command_uuid)
(host_uuid, profile_uuid, detail, status, retries, command_uuid)
VALUES %s
ON DUPLICATE KEY UPDATE
detail = VALUES(detail),
status = VALUES(status)`
status = VALUES(status),
retries = VALUES(retries)`
// MySQL will use the `host_uuid` part of the primary key as a first
// pass, and then filter that subset by `command_uuid`.
const getMatchingHostProfilesStmt = `
SELECT host_uuid, profile_uuid, command_uuid
SELECT host_uuid, profile_uuid, command_uuid, retries
FROM host_mdm_windows_profiles
WHERE host_uuid = ? AND command_uuid IN (?)`
@ -409,13 +415,24 @@ func updateMDMWindowsHostProfileStatusFromResponseDB(
return ctxerr.Wrap(ctx, err, "running query to get matching profiles")
}
// batch-update the matching entries with the desired detail and status>
// batch-update the matching entries with the desired detail and status
var sb strings.Builder
args = args[:0]
for _, hp := range matchingHostProfiles {
payload := uuidsToPayloads[hp.CommandUUID]
args = append(args, hp.HostUUID, hp.ProfileUUID, payload.Detail, payload.Status)
sb.WriteString("(?, ?, ?, ?, command_uuid),")
if payload.Status != nil && *payload.Status == fleet.MDMDeliveryFailed {
if hp.Retries < mdm.MaxProfileRetries {
// if we haven't hit the max retries, we set
// the host profile status to nil (which causes
// an install profile command to be enqueued
// the next time the profile manager cron runs)
// and increment the retry count
payload.Status = nil
hp.Retries++
}
}
args = append(args, hp.HostUUID, hp.ProfileUUID, payload.Detail, payload.Status, hp.Retries)
sb.WriteString("(?, ?, ?, ?, ?, command_uuid),")
}
stmt = fmt.Sprintf(updateHostProfilesStmt, strings.TrimSuffix(sb.String(), ","))

View file

@ -726,15 +726,15 @@ type Datastore interface {
// should be verified, failed, and retried, respectively. For each profile in the toRetry slice,
// the retries count is incremented by 1 and the status is set to null so that an install
// profile command is enqueued the next time the profile manager cron runs.
UpdateHostMDMProfilesVerification(ctx context.Context, hostUUID string, toVerify, toFail, toRetry []string) error
UpdateHostMDMProfilesVerification(ctx context.Context, host *Host, toVerify, toFail, toRetry []string) error
// GetHostMDMProfilesExpected returns the expected MDM profiles for a given host. The map is
// keyed by the profile identifier.
GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *Host) (map[string]*ExpectedMDMProfile, error)
// GetHostMDMProfilesRetryCounts returns a list of MDM profile retry counts for a given host.
GetHostMDMProfilesRetryCounts(ctx context.Context, hostUUID string) ([]HostMDMProfileRetryCount, error)
GetHostMDMProfilesRetryCounts(ctx context.Context, host *Host) ([]HostMDMProfileRetryCount, error)
// GetHostMDMProfileRetryCountByCommandUUID returns the retry count for the specified
// host UUID and command UUID.
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, hostUUID, cmdUUID string) (HostMDMProfileRetryCount, error)
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *Host, cmdUUID string) (HostMDMProfileRetryCount, error)
// SetOrUpdateHostOrbitInfo inserts of updates the orbit info for a host
SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error
@ -1201,6 +1201,31 @@ const (
DefaultMunkiIssuesBatchSize = 100
)
// ProfileVerificationStore is the minimal interface required to get and update the verification
// status of a host's MDM profiles. The Fleet Datastore satisfies this interface.
type ProfileVerificationStore interface {
// GetHostMDMProfilesExpectedForVerification returns the expected MDM profiles for a given host. The map is
// keyed by the profile identifier.
GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *Host) (map[string]*ExpectedMDMProfile, error)
// GetHostMDMProfilesRetryCounts returns the retry counts for the specified host.
GetHostMDMProfilesRetryCounts(ctx context.Context, host *Host) ([]HostMDMProfileRetryCount, error)
// GetHostMDMProfileRetryCountByCommandUUID returns the retry count for the specified
// host UUID and command UUID.
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *Host, commandUUID string) (HostMDMProfileRetryCount, error)
// UpdateHostMDMProfilesVerification updates status of macOS profiles installed on a given
// host. The toVerify, toFail, and toRetry slices contain the identifiers of the profiles that
// should be verified, failed, and retried, respectively. For each profile in the toRetry slice,
// the retries count is incremented by 1 and the status is set to null so that an install
// profile command is enqueued the next time the profile manager cron runs.
UpdateHostMDMProfilesVerification(ctx context.Context, host *Host, toVerify, toFail, toRetry []string) error
// UpdateOrDeleteHostMDMAppleProfile updates information about a single
// profile status. It deletes the row if the profile operation is "remove"
// and the status is "verifying" (i.e. successfully removed).
UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profile *HostMDMAppleProfile) error
}
var _ ProfileVerificationStore = (Datastore)(nil)
type PolicyFailure struct {
PolicyID uint
Host PolicySetHost

View file

@ -103,7 +103,10 @@ func (e MDMAppleEULA) AuthzType() string {
// ExpectedMDMProfile represents an MDM profile that is expected to be installed on a host.
type ExpectedMDMProfile struct {
// Identifier is the unique identifier used by macOS profiles
Identifier string `db:"identifier"`
// Name is the unique name used by Windows profiles
Name string `db:"name"`
// EarliestInstallDate is the earliest updated_at of all team profiles with the same checksum.
// It is used to assess the case where a host has installed a profile with the identifier
// expected by the host's current team, but the host's install_date is earlier than the
@ -113,6 +116,8 @@ type ExpectedMDMProfile struct {
// Ideally, we would simply compare the checksums of the installed and expected profiles, but
// the checksums are not available in the osquery profiles table.
EarliestInstallDate time.Time `db:"earliest_install_date"`
// RawProfile contains the raw profile contents
RawProfile []byte `db:"raw_profile"`
}
// IsWithinGracePeriod returns true if the host is within the grace period for the profile.
@ -133,8 +138,11 @@ func (ep ExpectedMDMProfile) IsWithinGracePeriod(hostDetailUpdatedAt time.Time)
// HostMDMProfileRetryCount represents the number of times Fleet has attempted to install
// the identified profile on a host.
type HostMDMProfileRetryCount struct {
// Identifier is the unique identifier used by macOS profiles
ProfileIdentifier string `db:"profile_identifier"`
Retries uint `db:"retries"`
// ProfileName is the unique name used by Windows profiles
ProfileName string `db:"profile_name"`
Retries uint `db:"retries"`
}
// TeamIDSetter defines the method to set a TeamID value on a struct,

View file

@ -12,8 +12,7 @@ import (
"strings"
"time"
mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
)
//////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -62,11 +61,11 @@ func (req *SoapRequest) GetHeaderBinarySecurityToken() (*HeaderBinarySecurityTok
return nil, errors.New("binarySecurityToken is empty")
}
if req.Header.Security.Security.Encoding != mdm.EnrollEncode {
if req.Header.Security.Security.Encoding != syncml.EnrollEncode {
return nil, errors.New("binarySecurityToken encoding is invalid")
}
if req.Header.Security.Security.Value != mdm.BinarySecurityDeviceEnroll && req.Header.Security.Security.Value != mdm.BinarySecurityAzureEnroll {
if req.Header.Security.Security.Value != syncml.BinarySecurityDeviceEnroll && req.Header.Security.Security.Value != syncml.BinarySecurityAzureEnroll {
return nil, errors.New("binarySecurityToken type is invalid")
}
@ -149,15 +148,15 @@ func (req *SoapRequest) IsValidDiscoveryMsg() error {
}
// Ensure that only valid versions are supported
if req.Body.Discover.Request.RequestVersion != mdm.EnrollmentVersionV4 &&
req.Body.Discover.Request.RequestVersion != mdm.EnrollmentVersionV5 {
if req.Body.Discover.Request.RequestVersion != syncml.EnrollmentVersionV4 &&
req.Body.Discover.Request.RequestVersion != syncml.EnrollmentVersionV5 {
return errors.New("invalid discover message: Request.RequestVersion")
}
// Traverse the AuthPolicies slice and check for valid values
isInvalidAuth := true
for _, authPolicy := range req.Body.Discover.Request.AuthPolicies.AuthPolicy {
if authPolicy == mdm.AuthOnPremise {
if authPolicy == syncml.AuthOnPremise {
isInvalidAuth = false
break
}
@ -217,8 +216,8 @@ func (req *SoapRequest) IsValidRequestSecurityTokenMsg() error {
return errors.New("invalid requestsecuritytoken message: BinarySecurityToken.ValueType")
}
if req.Body.RequestSecurityToken.BinarySecurityToken.ValueType != mdm.EnrollReqTypePKCS10 &&
req.Body.RequestSecurityToken.BinarySecurityToken.ValueType != mdm.EnrollReqTypePKCS7 {
if req.Body.RequestSecurityToken.BinarySecurityToken.ValueType != syncml.EnrollReqTypePKCS10 &&
req.Body.RequestSecurityToken.BinarySecurityToken.ValueType != syncml.EnrollReqTypePKCS7 {
return errors.New("invalid requestsecuritytoken message: BinarySecurityToken.EncodingType not supported")
}
@ -230,29 +229,29 @@ func (req *SoapRequest) IsValidRequestSecurityTokenMsg() error {
return errors.New("invalid requestsecuritytoken message: AdditionalContext.ContextItems missing")
}
reqEnrollType, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemEnrollmentType)
if err != nil || (reqEnrollType != mdm.ReqSecTokenEnrollTypeDevice && reqEnrollType != mdm.ReqSecTokenEnrollTypeFull) {
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemEnrollmentType, reqEnrollType, err)
reqEnrollType, err := req.Body.RequestSecurityToken.GetContextItem(syncml.ReqSecTokenContextItemEnrollmentType)
if err != nil || (reqEnrollType != syncml.ReqSecTokenEnrollTypeDevice && reqEnrollType != syncml.ReqSecTokenEnrollTypeFull) {
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", syncml.ReqSecTokenContextItemEnrollmentType, reqEnrollType, err)
}
reqDeviceID, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemDeviceID)
reqDeviceID, err := req.Body.RequestSecurityToken.GetContextItem(syncml.ReqSecTokenContextItemDeviceID)
if err != nil || len(reqDeviceID) == 0 {
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemDeviceID, reqDeviceID, err)
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", syncml.ReqSecTokenContextItemDeviceID, reqDeviceID, err)
}
reqHwDeviceID, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemHWDevID)
reqHwDeviceID, err := req.Body.RequestSecurityToken.GetContextItem(syncml.ReqSecTokenContextItemHWDevID)
if err != nil || len(reqHwDeviceID) == 0 {
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemHWDevID, reqHwDeviceID, err)
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", syncml.ReqSecTokenContextItemHWDevID, reqHwDeviceID, err)
}
reqOSEdition, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemOSEdition)
reqOSEdition, err := req.Body.RequestSecurityToken.GetContextItem(syncml.ReqSecTokenContextItemOSEdition)
if err != nil || len(reqOSEdition) == 0 {
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemOSEdition, reqOSEdition, err)
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", syncml.ReqSecTokenContextItemOSEdition, reqOSEdition, err)
}
reqOSVersion, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemOSVersion)
reqOSVersion, err := req.Body.RequestSecurityToken.GetContextItem(syncml.ReqSecTokenContextItemOSVersion)
if err != nil || len(reqOSVersion) == 0 {
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemOSVersion, reqOSVersion, err)
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", syncml.ReqSecTokenContextItemOSVersion, reqOSVersion, err)
}
return nil
@ -360,7 +359,7 @@ func (token *HeaderBinarySecurityToken) IsValidToken() error {
return errors.New("binary security token is empty")
}
if token.Value != microsoft_mdm.BinarySecurityDeviceEnroll && token.Value != microsoft_mdm.BinarySecurityAzureEnroll {
if token.Value != syncml.BinarySecurityDeviceEnroll && token.Value != syncml.BinarySecurityAzureEnroll {
return errors.New("binary security token is invalid")
}
@ -373,7 +372,7 @@ func (token *HeaderBinarySecurityToken) IsAzureJWTToken() bool {
return false
}
if token.Value == microsoft_mdm.BinarySecurityAzureEnroll {
if token.Value == syncml.BinarySecurityAzureEnroll {
return true
}
@ -386,7 +385,7 @@ func (token *HeaderBinarySecurityToken) IsDeviceToken() bool {
return false
}
if token.Value == microsoft_mdm.BinarySecurityDeviceEnroll {
if token.Value == syncml.BinarySecurityDeviceEnroll {
return true
}
@ -503,8 +502,8 @@ func (msg RequestSecurityToken) GetBinarySecurityTokenData() (string, error) {
// Get Binary Security Token Type
func (msg RequestSecurityToken) GetBinarySecurityTokenType() (string, error) {
if msg.BinarySecurityToken.ValueType == mdm.EnrollReqTypePKCS10 ||
msg.BinarySecurityToken.ValueType == mdm.EnrollReqTypePKCS7 {
if msg.BinarySecurityToken.ValueType == syncml.EnrollReqTypePKCS10 ||
msg.BinarySecurityToken.ValueType == syncml.EnrollReqTypePKCS7 {
return msg.BinarySecurityToken.ValueType, nil
}
@ -921,13 +920,19 @@ type ProtoCmdOperation struct {
// Protocol Command
type SyncMLCmd struct {
XMLName xml.Name `xml:",omitempty"`
CmdID string `xml:"CmdID"`
MsgRef *string `xml:"MsgRef,omitempty"`
CmdRef *string `xml:"CmdRef,omitempty"`
Cmd *string `xml:"Cmd,omitempty"`
Data *string `xml:"Data,omitempty"`
Items []CmdItem `xml:"Item,omitempty"`
XMLName xml.Name `xml:",omitempty"`
CmdID string `xml:"CmdID"`
MsgRef *string `xml:"MsgRef,omitempty"`
CmdRef *string `xml:"CmdRef,omitempty"`
Cmd *string `xml:"Cmd,omitempty"`
Data *string `xml:"Data,omitempty"`
Items []CmdItem `xml:"Item,omitempty"`
// ReplaceCommands is a catch-all for any nested <Replace> commands,
// which can be found under <Atomic> elements.
//
// NOTE: in theory Atomics can have anything except Get verbs, but
// for the moment we're not allowing anything besides Replaces
ReplaceCommands []SyncMLCmd `xml:"Replace,omitempty"`
}
@ -1022,12 +1027,12 @@ func (msg *SyncML) IsValidHeader() error {
}
// SyncML DTD version check
if msg.SyncHdr.VerDTD != mdm.SyncMLSupportedVersion {
if msg.SyncHdr.VerDTD != syncml.SyncMLSupportedVersion {
return errors.New("unsupported DTD version")
}
// SyncML Proto version check
if msg.SyncHdr.VerProto != mdm.SyncMLVerProto {
if msg.SyncHdr.VerProto != syncml.SyncMLVerProto {
return errors.New("unsupported proto version")
}
@ -1304,11 +1309,7 @@ func (cmd *SyncMLCmd) IsValid() bool {
// IsEmpty checks if there are not items in the command
func (cmd *SyncMLCmd) IsEmpty() bool {
if len(cmd.Items) == 0 {
return true
}
return false
return len(cmd.Items) == 0
}
// GetTargetURI returns the first protocol commands target URI from the items list

View file

@ -4,7 +4,7 @@ import (
"encoding/xml"
"testing"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/require"
)
@ -91,7 +91,7 @@ func TestBuildMDMWindowsProfilePayloadFromMDMResponse(t *testing.T) {
RawCommand: []byte(`<Atomic><Replace><</Atomic>`),
},
statuses: map[string]SyncMLCmd{
"foo": {CmdID: "foo", Data: ptr.String(microsoft_mdm.CmdStatusAtomicFailed)},
"foo": {CmdID: "foo", Data: ptr.String(syncml.CmdStatusAtomicFailed)},
},
hostUUID: "host-uuid",
expectedError: "XML syntax error",
@ -130,9 +130,9 @@ func TestBuildMDMWindowsProfilePayloadFromMDMResponse(t *testing.T) {
</Atomic>`),
},
statuses: map[string]SyncMLCmd{
"foo": {CmdID: "foo", Data: ptr.String(microsoft_mdm.CmdStatusAtomicFailed)},
"bar": {CmdID: "bar", Data: ptr.String(microsoft_mdm.CmdStatusOK)},
"baz": {CmdID: "baz", Data: ptr.String(microsoft_mdm.CmdStatusBadRequest)},
"foo": {CmdID: "foo", Data: ptr.String(syncml.CmdStatusAtomicFailed)},
"bar": {CmdID: "bar", Data: ptr.String(syncml.CmdStatusOK)},
"baz": {CmdID: "baz", Data: ptr.String(syncml.CmdStatusBadRequest)},
},
hostUUID: "host-uuid",
expectedPayload: &MDMWindowsProfilePayload{

View file

@ -9,7 +9,7 @@ import (
"time"
"github.com/fleetdm/fleet/v4/server/mdm"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
)
// MDMWindowsBitLockerSummary reports the number of Windows hosts being managed by Fleet with
@ -49,7 +49,7 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided() error {
return errors.New("The file should include valid XML.")
}
if _, ok := microsoft_mdm.FleetReservedProfileNames()[m.Name]; ok {
if _, ok := syncml.FleetReservedProfileNames()[m.Name]; ok {
return fmt.Errorf("Profile name %q is not allowed.", m.Name)
}
@ -97,8 +97,8 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided() error {
}
var fleetProvidedLocURIValidationMap = map[string][2]string{
microsoft_mdm.FleetBitLockerTargetLocURI: {"BitLocker", "mdm.enable_disk_encryption"},
microsoft_mdm.FleetOSUpdateTargetLocURI: {"Windows updates", "mdm.windows_updates"},
syncml.FleetBitLockerTargetLocURI: {"BitLocker", "mdm.enable_disk_encryption"},
syncml.FleetOSUpdateTargetLocURI: {"Windows updates", "mdm.windows_updates"},
}
func validateFleetProvidedLocURI(locURI string) error {
@ -120,6 +120,7 @@ type MDMWindowsProfilePayload struct {
OperationType MDMOperationType `db:"operation_type"`
Detail string `db:"detail"`
CommandUUID string `db:"command_uuid"`
Retries int `db:"retries"`
}
type MDMWindowsBulkUpsertHostProfilePayload struct {

View file

@ -3,7 +3,7 @@ package fleet
import (
"testing"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/stretchr/testify/require"
)
@ -336,7 +336,7 @@ func TestValidateUserProvided(t *testing.T) {
{
name: "Valid XML with reserved name",
profile: MDMWindowsConfigProfile{
Name: microsoft_mdm.FleetWindowsOSUpdatesProfileName,
Name: syncml.FleetWindowsOSUpdatesProfileName,
SyncML: []byte(`<Replace><Target><LocURI>Custom/URI</LocURI></Target></Replace>`),
},
wantErr: true,

View file

@ -4,6 +4,7 @@ import (
"context"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
)
// Profile verification is a set of related processes that run on the Fleet server to ensure that
@ -36,39 +37,10 @@ import (
// if the profile should be retried (in which case, a new install profile command will be enqueued by the server)
// or marked as "failed" and updates the datastore accordingly.
// maxRetries is the maximum times an install profile command may be retried, after which marked as failed and no further
// attempts will be made to install the profile.
const maxRetries = 1
// ProfileVerificationStore is the minimal interface required to get and update the verification
// status of a host's MDM profiles. The Fleet Datastore satisfies this interface.
type ProfileVerificationStore interface {
// GetHostMDMProfilesExpectedForVerification returns the expected MDM profiles for a given host. The map is
// keyed by the profile identifier.
GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error)
// GetHostMDMProfilesRetryCounts returns the retry counts for the specified host.
GetHostMDMProfilesRetryCounts(ctx context.Context, hostUUID string) ([]fleet.HostMDMProfileRetryCount, error)
// GetHostMDMProfileRetryCountByCommandUUID returns the retry count for the specified
// host UUID and command UUID.
GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, hostUUID, commandUUID string) (fleet.HostMDMProfileRetryCount, error)
// UpdateHostMDMProfilesVerification updates status of macOS profiles installed on a given
// host. The toVerify, toFail, and toRetry slices contain the identifiers of the profiles that
// should be verified, failed, and retried, respectively. For each profile in the toRetry slice,
// the retries count is incremented by 1 and the status is set to null so that an install
// profile command is enqueued the next time the profile manager cron runs.
UpdateHostMDMProfilesVerification(ctx context.Context, hostUUID string, toVerify, toFail, toRetry []string) error
// UpdateOrDeleteHostMDMAppleProfile updates information about a single
// profile status. It deletes the row if the profile operation is "remove"
// and the status is "verifying" (i.e. successfully removed).
UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profile *fleet.HostMDMAppleProfile) error
}
var _ ProfileVerificationStore = (fleet.Datastore)(nil)
// VerifyHostMDMProfiles performs the verification of the MDM profiles installed on a host and
// updates the verification status in the datastore. It is intended to be called by Fleet osquery
// service when the Fleet server ingests host details.
func VerifyHostMDMProfiles(ctx context.Context, ds ProfileVerificationStore, host *fleet.Host, installed map[string]*fleet.HostMacOSProfile) error {
func VerifyHostMDMProfiles(ctx context.Context, ds fleet.ProfileVerificationStore, host *fleet.Host, installed map[string]*fleet.HostMacOSProfile) error {
expected, err := ds.GetHostMDMProfilesExpectedForVerification(ctx, host)
if err != nil {
return err
@ -99,7 +71,7 @@ func VerifyHostMDMProfiles(ctx context.Context, ds ProfileVerificationStore, hos
toFail := make([]string, 0, len(missing))
toRetry := make([]string, 0, len(missing))
if len(missing) > 0 {
counts, err := ds.GetHostMDMProfilesRetryCounts(ctx, host.UUID)
counts, err := ds.GetHostMDMProfilesRetryCounts(ctx, host)
if err != nil {
return err
}
@ -108,7 +80,7 @@ func VerifyHostMDMProfiles(ctx context.Context, ds ProfileVerificationStore, hos
retriesByProfileIdentifier[r.ProfileIdentifier] = r.Retries
}
for _, key := range missing {
if retriesByProfileIdentifier[key] < maxRetries {
if retriesByProfileIdentifier[key] < mdm.MaxProfileRetries {
// if we haven't hit the max retries, we set the host profile status to nil (which
// causes an install profile command to be enqueued the next time the profile
// manager cron runs) and increment the retry count
@ -120,24 +92,25 @@ func VerifyHostMDMProfiles(ctx context.Context, ds ProfileVerificationStore, hos
}
}
return ds.UpdateHostMDMProfilesVerification(ctx, host.UUID, verified, toFail, toRetry)
return ds.UpdateHostMDMProfilesVerification(ctx, host, verified, toFail, toRetry)
}
// HandleHostMDMProfileInstallResult ingests the result of an install profile command reported via
// the MDM protocol and updates the verification status in the datastore. It is intended to be
// called by the Fleet MDM checkin and command service install profile request handler.
func HandleHostMDMProfileInstallResult(ctx context.Context, ds ProfileVerificationStore, hostUUID string, cmdUUID string, status *fleet.MDMDeliveryStatus, detail string) error {
func HandleHostMDMProfileInstallResult(ctx context.Context, ds fleet.ProfileVerificationStore, hostUUID string, cmdUUID string, status *fleet.MDMDeliveryStatus, detail string) error {
host := &fleet.Host{UUID: hostUUID, Platform: "darwin"}
if status != nil && *status == fleet.MDMDeliveryFailed {
m, err := ds.GetHostMDMProfileRetryCountByCommandUUID(ctx, hostUUID, cmdUUID)
m, err := ds.GetHostMDMProfileRetryCountByCommandUUID(ctx, host, cmdUUID)
if err != nil {
return err
}
if m.Retries < maxRetries {
if m.Retries < mdm.MaxProfileRetries {
// if we haven't hit the max retries, we set the host profile status to nil (which
// causes an install profile command to be enqueued the next time the profile
// manager cron runs) and increment the retry count
return ds.UpdateHostMDMProfilesVerification(ctx, hostUUID, nil, nil, []string{m.ProfileIdentifier})
return ds.UpdateHostMDMProfilesVerification(ctx, host, nil, nil, []string{m.ProfileIdentifier})
}
}

View file

@ -9,6 +9,11 @@ import (
"go.mozilla.org/pkcs7"
)
// MaxProfileRetries is the maximum times an install profile command may be
// retried, after which marked as failed and no further attempts will be made
// to install the profile.
const MaxProfileRetries = 1
// DecryptBase64CMS decrypts a base64 encoded pkcs7-encrypted value using the
// provided certificate and private key.
func DecryptBase64CMS(p7Base64 string, cert *x509.Certificate, key crypto.PrivateKey) ([]byte, error) {

View file

@ -55,57 +55,6 @@ const (
MSManageEntryPoint = "/ManagementServer/MDM.svc"
)
// XML Namespaces and type URLs used by the Microsoft Device Enrollment v2 protocol (MS-MDE2)
const (
DiscoverNS = "http://schemas.microsoft.com/windows/management/2012/01/enrollment"
PolicyNS = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy"
EnrollWSTrust = "http://docs.oasis-open.org/ws-sx/ws-trust/200512"
EnrollSecExt = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
EnrollTType = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken"
EnrollPDoc = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc"
EnrollEncode = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary"
EnrollReq = "http://schemas.microsoft.com/windows/pki/2009/01/enrollment"
EnrollNSS = "http://www.w3.org/2003/05/soap-envelope"
EnrollNSA = "http://www.w3.org/2005/08/addressing"
EnrollXSI = "http://www.w3.org/2001/XMLSchema-instance"
EnrollXSD = "http://www.w3.org/2001/XMLSchema"
EnrollXSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
ActionNsDiag = "http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics"
ActionNsDiscovery = "http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/DiscoverResponse"
ActionNsPolicy = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPoliciesResponse"
ActionNsEnroll = EnrollReq + "/RSTRC/wstep"
EnrollReqTypePKCS10 = EnrollReq + "#PKCS10"
EnrollReqTypePKCS7 = EnrollReq + "#PKCS7"
BinarySecurityDeviceEnroll = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentUserToken"
BinarySecurityAzureEnroll = "urn:ietf:params:oauth:token-type:jwt"
)
// Soap Error constants
// Details here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/0a78f419-5fd7-4ddb-bc76-1c0f7e11da23
const (
// Message format is bad
SoapErrorMessageFormat = "s:messageformat"
// User not recognized
SoapErrorAuthentication = "s:authentication"
// User not allowed to enroll
SoapErrorAuthorization = "s:authorization"
// Failed to get certificate
SoapErrorCertificateRequest = "s:certificaterequest"
// Generic failure from management server, such as a database access error
SoapErrorEnrollmentServer = "s:enrollmentserver"
// The server hit an unexpected issue
SoapErrorInternalServiceFault = "s:internalservicefault"
// Cannot parse the security header
SoapErrorInvalidSecurity = "a:invalidsecurity"
)
// Device Enrolled States
const (
@ -120,294 +69,6 @@ const (
MDMDeviceStateManaged = "MDMDeviceEnrolledManaged"
)
// MS-MDE2 Message constants
const (
// Minimum supported version
EnrollmentVersionV4 = "4.0"
// Maximum supported version
EnrollmentVersionV5 = "5.0"
// xsi:nil indicates value is not present
DefaultStateXSI = "true"
// Supported authentication types
AuthOnPremise = "OnPremise"
// SOAP Fault codes
SoapFaultRecv = "s:receiver"
// SOAP Fault default error locale
SoapFaultLocale = "en-us"
// HTTP Content Type for SOAP responses
SoapContentType = "application/soap+xml; charset=utf-8"
// HTTP Content Type for SyncML MDM responses
SyncMLContentType = "application/vnd.syncml.dm+xml"
// HTTP Content Type for Webcontainer responses
WebContainerContentType = "text/html; charset=UTF-8"
// Minimal Key Length for SHA1WithRSA encryption
PolicyMinKeyLength = "2048"
// Certificate Validity Period in seconds (365 days)
PolicyCertValidityPeriodInSecs = "31536000"
// Certificate Renewal Period in seconds (180 days)
PolicyCertRenewalPeriodInSecs = "15552000"
// Supported Enroll types gathered from MS-MDE2 Spec Section 2.2.9.3
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/f7553554-b6e1-4a0d-abd6-6a2534503af7
// Supported Enroll Type Device
ReqSecTokenEnrollTypeDevice = "Device"
// Supported Enroll Type Full
ReqSecTokenEnrollTypeFull = "Full"
// Provisioning Doc Certificate Renewal Period (365 days)
WstepCertRenewalPeriodInDays = "365"
// Provisioning Doc Server supports ROBO auto certificate renewal
// TODO: Add renewal support
WstepROBOSupport = "true"
// Provisioning Doc Server retry interval
WstepRenewRetryInterval = "4"
// The PROVIDER-ID paramer specifies the server identifier for a management server used in the current management session
DocProvisioningAppProviderID = "Fleet"
// The NAME parameter is used in the APPLICATION characteristic to specify a user readable application identity
DocProvisioningAppName = DocProvisioningAppProviderID
// The CONNRETRYFREQ parameter is used in the APPLICATION characteristic to specify a user readable application identity
DocProvisioningAppConnRetryFreq = "6"
// The INITIALBACKOFFTIME parameter is used to specify the initial wait time in milliseconds when the DM client retries for the first time
DocProvisioningAppInitialBackoffTime = "30000"
// The MAXBACKOFFTIME parameter is used to specify the maximum number of milliseconds to sleep after package-sending failure
DocProvisioningAppMaxBackoffTime = "120000"
// The DocProvisioningVersion attributes defines the version of the provisioning document format
DocProvisioningVersion = "1.1"
// The number of times the DM client should retry to connect to the server when the client is initially configured or enrolled to communicate with the server.
DmClientCSPNumberOfFirstRetries = "8"
// The waiting time (in minutes) for the initial set of retries as specified by the number of retries in NumberOfFirstRetries
DmClientCSPIntervalForFirstSetOfRetries = "15"
// The number of times the DM client should retry a second round of connecting to the server when the client is initially configured/enrolled to communicate with the server
DmClientCSPNumberOfSecondRetries = "5"
// The waiting time (in minutes) for the second set of retries as specified by the number of retries in NumberOfSecondRetries
DmClientCSPIntervalForSecondSetOfRetries = "3"
// The number of times the DM client should retry connecting to the server when the client is initially configured/enrolled to communicate with the server
DmClientCSPNumberOfRemainingScheduledRetries = "0"
// The waiting time (in minutes) for the initial set of retries as specified by the number of retries in NumberOfRemainingScheduledRetries
DmClientCSPIntervalForRemainingScheduledRetries = "1560"
// It allows the IT admin to require the device to start a management session on any user login, regardless of if the user has preciously logged in
DmClientCSPPollOnLogin = "true"
// It specifies whether the DM client should send out a request pending alert in case the device response to a DM request is too slow.
DmClientCSPEnableOmaDmKeepAliveMessage = "true"
// CSR issuer should be verified during enrollment
EnrollVerifyIssue = true
// Int type used by the DM client configuration
DmClientIntType = "integer"
// Bool type used by the DM client configuration
DmClientBoolType = "boolean"
// Additional Context items present on the RequestSecurityToken token message
ReqSecTokenContextItemUXInitiated = "UXInitiated"
ReqSecTokenContextItemHWDevID = "HWDevID"
ReqSecTokenContextItemLocale = "Locale"
ReqSecTokenContextItemTargetedUserLoggedIn = "TargetedUserLoggedIn"
ReqSecTokenContextItemOSEdition = "OSEdition"
ReqSecTokenContextItemDeviceName = "DeviceName"
ReqSecTokenContextItemDeviceID = "DeviceID"
ReqSecTokenContextItemEnrollmentType = "EnrollmentType"
ReqSecTokenContextItemDeviceType = "DeviceType"
ReqSecTokenContextItemOSVersion = "OSVersion"
ReqSecTokenContextItemApplicationVersion = "ApplicationVersion"
ReqSecTokenContextItemNotInOobe = "NotInOobe"
ReqSecTokenContextItemRequestVersion = "RequestVersion"
// APPRU query param expected by STS Auth endpoint
STSAuthAppRu = "appru"
// Login related query param expected by STS Auth endpoint
STSLoginHint = "login_hint"
// redirect_uri query param expected by TOS endpoint
TOCRedirectURI = "redirect_uri"
// client-request-id query param expected by TOS endpoint
TOCReqID = "client-request-id"
// Alert payload user-driven unenrollment request
AlertUserUnenrollmentRequest = "com.microsoft:mdm.unenrollment.userrequest"
// FleetdWindowsInstallerGUID is the GUID used for fleetd on Windows
FleetdWindowsInstallerGUID = "./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7BA427C0AA-E2D5-40DF-ACE8-0D726A6BE096%7D/DownloadInstall"
)
// MS-MDM Message constants
const (
// SyncML Message Content Type
SyncMLMsgContentType = "application/vnd.syncml.dm+xml"
// SyncML Message Meta Namespace
SyncMLMetaNamespace = "syncml:metinf"
// SyncML Cmd Namespace
SyncCmdNamespace = "SYNCML:SYNCML1.2"
// SyncML Message Header Name
SyncMLHdrName = "SyncHdr"
// Supported SyncML version
SyncMLSupportedVersion = "1.2"
// SyncML ver protocol version
SyncMLVerProto = "DM/" + SyncMLSupportedVersion
)
// MS-MDM Status Code constants
// Details here: https://learn.microsoft.com/en-us/windows/client-management/oma-dm-protocol-support
const (
// The SyncML command completed successfully
CmdStatusOK = "200"
// Accepted for processing
// This code denotes an asynchronous operation, such as a request to run a remote execution of an application
CmdStatusAcceptedForProcessing = "202"
// Authentication accepted
// Normally you'll only see this code in response to the SyncHdr element (used for authentication in the OMA-DM standard)
// You may see this code if you look at OMA DM logs, but CSPs don't typically generate this code.
CmdStatusAuthenticationAccepted = "212"
// Operation canceled
// The SyncML command completed successfully, but no more commands will be processed within the session.
CmdStatusOperationCancelled = "214"
// Not executed
// A command wasn't executed as a result of user interaction to cancel the command.
CmdStatusNotExecuted = "215"
// Atomic roll back OK
// A command was inside an Atomic element and Atomic failed, thhis command was rolled back successfully
CmdStatusAtomicRollbackAccepted = "216"
// Bad request. The requested command couldn't be performed because of malformed syntax.
// CSPs don't usually generate this error, however you might see it if your SyncML is malformed.
CmdStatusBadRequest = "400"
// Invalid credentials
// The requested command failed because the requestor must provide proper authentication. CSPs don't usually generate this error
CmdStatusInvalidCredentials = "401"
// Forbidden
// The requested command failed, but the recipient understood the requested command
CmdStatusForbidden = "403"
// Not found
// The requested target wasn't found. This code will be generated if you query a node that doesn't exist
CmdStatusNotFound = "404"
// Command not allowed
// This respond code will be generated if you try to write to a read-only node
CmdStatusNotAllowed = "405"
// Optional feature not supported
// This response code will be generated if you try to access a property that the CSP doesn't support
CmdStatusOptionalFeature = "406"
// Unsupported type or format
// This response code can result from XML parsing or formatting errors
CmdStatusUnsupportedType = "415"
// Already exists
// This response code occurs if you attempt to add a node that already exists
CmdStatusAlreadyExists = "418"
// Permission Denied
// The requested command failed because the sender doesn't have adequate access control permissions (ACL) on the recipient.
// An "Access denied" errors usually get translated to this response code.
CmdStatusPermissionDenied = "425"
// Command failed. Generic failure.
// The recipient encountered an unexpected condition, which prevented it from fulfilling the request
// This response code will occur when the SyncML DPU can't map the originating error code
CmdStatusCommandFailed = "500"
// Atomic failed
// One of the operations in an Atomic block failed
CmdStatusAtomicFailed = "507"
// Atomic roll back failed
// An Atomic operation failed and the command wasn't rolled back successfully.
CmdStatusAtomicRollbackFailed = "516"
)
// MS-MDM Supported Alerts
// Details on MS-MDM 2.2.7.2: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mdm/72c6ea01-121c-48f9-85da-a26bb12aad51
const (
// SERVER-INITIATED MGMT
// Server-initiated device management session
CmdAlertServerInitiatedManagement = "1200"
// CLIENT-INITIATED MGMT
// Client-initiated device management session
CmdAlertClientInitiatedManagement = "1201"
// NEXT MESSAGE
// Request for the next message of a large object package
CmdAlertNextMessage = "1222"
// SESSION ABORT
// Informs recipient that the sender wishes to abort the DM session
CmdAlertSessionAbort = "1223"
// CLIENT EVENT
// Informs server that an event has occurred on the client
CmdAlertClientEvent = "1224"
// NO END OF DATA
// End of Data for chunked object not received.
CmdAlertNoEndOfData = "1225"
// GENERIC ALERT
// Generic client generated alert with or without a reference to a Management
CmdAlertGeneric = "1226"
)
const (
FleetBitLockerTargetLocURI = "/Vendor/MSFT/BitLocker"
FleetOSUpdateTargetLocURI = "/Vendor/MSFT/Policy/Config/Update"
FleetWindowsOSUpdatesProfileName = "Windows OS Updates"
)
func FleetReservedProfileNames() map[string]struct{} {
return map[string]struct{}{
FleetWindowsOSUpdatesProfileName: {},
}
}
func ResolveWindowsMDMDiscovery(serverURL string) (string, error) {
return commonmdm.ResolveURL(serverURL, MDE2DiscoveryPath, false)
}

View file

@ -0,0 +1,162 @@
package microsoft_mdm
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"hash/fnv"
"io"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
)
// LoopHostMDMLocURIs loops all the <LocURI> values on all the profiles for a
// given host. It provides to the callback function:
//
// - An `ExpectedMDMProfile` that references the profile owning the LocURI
// - A hash that's unique for each profile/uri combination
// - The LocURI string
// - The data (if any) of the first <Item> element of the current LocURI
func LoopHostMDMLocURIs(
ctx context.Context,
ds fleet.ProfileVerificationStore,
host *fleet.Host,
fn func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string),
) error {
profileMap, err := ds.GetHostMDMProfilesExpectedForVerification(ctx, host)
if err != nil {
return fmt.Errorf("getting host profiles for verification: %w", err)
}
for _, expectedProf := range profileMap {
var prof fleet.SyncMLCmd
wrappedBytes := fmt.Sprintf("<Atomic>%s</Atomic>", expectedProf.RawProfile)
if err := xml.Unmarshal([]byte(wrappedBytes), &prof); err != nil {
return fmt.Errorf("unmarshalling profile %s: %w", expectedProf.Name, err)
}
for _, rc := range prof.ReplaceCommands {
locURI := rc.GetTargetURI()
data := rc.GetTargetData()
ref := HashLocURI(expectedProf.Name, locURI)
fn(expectedProf, ref, locURI, data)
}
}
return nil
}
// HashLocURI creates a unique, consistent hash for a given profileName +
// locURI combination.
//
// FIXME: the mdm_bridge table decodes CmdID as `int`,
// so we encode the reference as an int32.
func HashLocURI(profileName, locURI string) string {
hash := fnv.New32a()
hash.Write([]byte(profileName + locURI))
return fmt.Sprint(hash.Sum32())
}
// VerifyHostMDMProfiles performs the verification of the MDM profiles installed on a host and
// updates the verification status in the datastore. It is intended to be called by Fleet osquery
// service when the Fleet server ingests host details.
func VerifyHostMDMProfiles(ctx context.Context, ds fleet.ProfileVerificationStore, host *fleet.Host, rawSyncML []byte) error {
var syncML fleet.SyncML
decoder := xml.NewDecoder(bytes.NewReader(rawSyncML))
// the DLL used by the `mdm_bridge` extension sends the response with
// <?xml version="1.0" encoding="utf-16"?>, however if you use
// `charset.NewReaderLabel` it fails to unmarshal (!?) for now, I'm
// relying on this hack.
decoder.CharsetReader = func(encoding string, input io.Reader) (io.Reader, error) {
return input, nil
}
if err := decoder.Decode(&syncML); err != nil {
return fmt.Errorf("decoding provided syncML: %w", err)
}
// TODO: what if more than one profile has the same
// target uri but a different value? (product question)
refToStatus := map[string]string{}
refToResult := map[string]string{}
for _, r := range syncML.GetOrderedCmds() {
if r.Cmd.CmdRef == nil {
continue
}
ref := *r.Cmd.CmdRef
if r.Verb == fleet.CmdStatus && r.Cmd.Data != nil {
refToStatus[ref] = *r.Cmd.Data
}
if r.Verb == fleet.CmdResults {
refToResult[ref] = r.Cmd.GetTargetData()
}
}
missing := map[string]struct{}{}
verified := map[string]struct{}{}
err := LoopHostMDMLocURIs(ctx, ds, host, func(profile *fleet.ExpectedMDMProfile, ref, locURI, wantData string) {
// if we didn't get a status for a LocURI, mark the profile as
// missing.
gotStatus, ok := refToStatus[ref]
if !ok {
missing[profile.Name] = struct{}{}
}
// it's okay if we didn't get a result
gotResults := refToResult[ref]
// non-200 status don't have results. Consider it failed
// TODO: should we be more granular instead? eg: special case
// `4xx` responses? I'm sure there are edge cases we're not
// accounting for here, but it's unclear at this moment.
if !strings.HasPrefix(gotStatus, "2") || wantData != gotResults {
withinGracePeriod := profile.IsWithinGracePeriod(host.DetailUpdatedAt)
if !withinGracePeriod {
missing[profile.Name] = struct{}{}
}
return
}
verified[profile.Name] = struct{}{}
})
if err != nil {
return fmt.Errorf("looping host mdm LocURIs: %w", err)
}
toFail := make([]string, 0, len(missing))
toRetry := make([]string, 0, len(missing))
if len(missing) > 0 {
counts, err := ds.GetHostMDMProfilesRetryCounts(ctx, host)
if err != nil {
return fmt.Errorf("getting host profiles retry counts: %w", err)
}
retriesByProfileUUID := make(map[string]uint, len(counts))
for _, r := range counts {
retriesByProfileUUID[r.ProfileName] = r.Retries
}
for key := range missing {
// if the profile is in missing, we failed to validate at
// least one LocURI, delete it from the verified map
delete(verified, key)
if retriesByProfileUUID[key] < mdm.MaxProfileRetries {
// if we haven't hit the max retries, we set
// the host profile status to nil (which causes
// an install profile command to be enqueued
// the next time the profile manager cron runs)
// and increment the retry count
toRetry = append(toRetry, key)
continue
}
// otherwise we set the host profile status to failed
toFail = append(toFail, key)
}
}
i := 0
verifiedSlice := make([]string, len(verified))
for k := range verified {
verifiedSlice[i] = k
i++
}
return ds.UpdateHostMDMProfilesVerification(ctx, host, verifiedSlice, toFail, toRetry)
}

View file

@ -0,0 +1,309 @@
package microsoft_mdm
import (
"context"
"encoding/xml"
"io"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestLoopHostMDMLocURIs(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
ds.GetHostMDMProfilesExpectedForVerificationFunc = func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
return map[string]*fleet.ExpectedMDMProfile{
"N1": {Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{"L1": "D1"})},
"N2": {Name: "N2", RawProfile: syncml.ForTestWithData(map[string]string{"L2": "D2"})},
"N3": {Name: "N3", RawProfile: syncml.ForTestWithData(map[string]string{"L3": "D3", "L3.1": "D3.1"})},
}, nil
}
type wantStruct struct {
locURI string
data string
profileUUID string
uniqueHash string
}
got := []wantStruct{}
err := LoopHostMDMLocURIs(ctx, ds, &fleet.Host{}, func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string) {
got = append(got, wantStruct{
locURI: locURI,
data: data,
profileUUID: profile.Name,
uniqueHash: hash,
})
})
require.NoError(t, err)
require.ElementsMatch(
t,
[]wantStruct{
{"L1", "D1", "N1", "1255198959"},
{"L2", "D2", "N2", "2736786183"},
{"L3", "D3", "N3", "894211447"},
{"L3.1", "D3.1", "N3", "3410477854"},
},
got,
)
}
func TestHashLocURI(t *testing.T) {
testCases := []struct {
name string
profileName string
locURI string
expectNotEmpty bool
}{
{
name: "basic functionality",
profileName: "profile1",
locURI: "uri1",
expectNotEmpty: true,
},
{
name: "empty strings",
profileName: "",
locURI: "",
expectNotEmpty: true,
},
{
name: "special characters",
profileName: "profile!@#",
locURI: "uri$%^",
expectNotEmpty: true,
},
{
name: "long string input",
profileName: string(make([]rune, 1000)),
locURI: string(make([]rune, 1000)),
expectNotEmpty: true,
},
{
name: "non-ASCII characters",
profileName: "プロファイル",
locURI: "URI",
expectNotEmpty: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hash := HashLocURI(tc.profileName, tc.locURI)
if tc.expectNotEmpty {
require.NotEmpty(t, hash, "hash should not be empty")
}
})
}
}
func TestVerifyHostMDMProfilesErrors(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
host := &fleet.Host{}
err := VerifyHostMDMProfiles(ctx, ds, host, []byte{})
require.ErrorIs(t, err, io.EOF)
}
func TestVerifyHostMDMProfilesHappyPaths(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
host := &fleet.Host{
DetailUpdatedAt: time.Now(),
}
type osqueryReport struct {
Name string
Status string
LocURI string
Data string
}
type hostProfile struct {
Name string
RawContents []byte
RetryCount uint
}
cases := []struct {
name string
hostProfiles []hostProfile
report []osqueryReport
toVerify []string
toFail []string
toRetry []string
withinGracePeriod bool
}{
{
name: "profile reported, but host doesn't have any",
hostProfiles: nil,
report: []osqueryReport{{"N1", "200", "L1", "D1"}},
toVerify: []string{},
toFail: []string{},
toRetry: []string{},
},
{
name: "single profile reported and verified",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1"}), 0},
},
report: []osqueryReport{{"N1", "200", "L1", "D1"}},
toVerify: []string{"N1"},
toFail: []string{},
toRetry: []string{},
},
{
name: "Get succeeds but has missing data",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1"}), 0},
{"N2", syncml.ForTestWithData(map[string]string{"L2": "D2"}), 1},
{"N3", syncml.ForTestWithData(map[string]string{"L3": "D3"}), 0},
{"N4", syncml.ForTestWithData(map[string]string{"L4": "D4"}), 1},
},
report: []osqueryReport{
{"N1", "200", "L1", ""},
{"N2", "200", "L2", ""},
{"N3", "200", "L3", "D3"},
{"N4", "200", "L4", "D4"},
},
toVerify: []string{"N3", "N4"},
toFail: []string{"N2"},
toRetry: []string{"N1"},
},
{
name: "Get fails",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1"}), 0},
{"N2", syncml.ForTestWithData(map[string]string{"L2": "D2"}), 1},
{"N3", syncml.ForTestWithData(map[string]string{"L3": "D3"}), 0},
{"N4", syncml.ForTestWithData(map[string]string{"L4": "D4"}), 1},
},
report: []osqueryReport{
{"N1", "400", "L1", ""},
{"N2", "500", "L2", ""},
{"N3", "200", "L3", "D3"},
{"N4", "200", "L4", "D4"},
},
toVerify: []string{"N3", "N4"},
toFail: []string{"N2"},
toRetry: []string{"N1"},
},
{
name: "missing report",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1"}), 0},
{"N2", syncml.ForTestWithData(map[string]string{"L2": "D2"}), 1},
},
report: []osqueryReport{},
toVerify: []string{},
toFail: []string{"N2"},
toRetry: []string{"N1"},
},
{
name: "profiles with multiple locURIs",
hostProfiles: []hostProfile{
{"N1", syncml.ForTestWithData(map[string]string{"L1": "D1", "L1.1": "D1.1"}), 0},
{"N2", syncml.ForTestWithData(map[string]string{"L2": "D2", "L2.1": "D2.1"}), 1},
{"N3", syncml.ForTestWithData(map[string]string{"L3": "D3", "L3.1": "D3.1"}), 0},
{"N4", syncml.ForTestWithData(map[string]string{"L4": "D4", "L4.1": "D4.1"}), 1},
},
report: []osqueryReport{
{"N1", "400", "L1", ""},
{"N1", "200", "L1.1", "D1.1"},
{"N2", "500", "L2", ""},
{"N2", "200", "L2.1", "D2.1"},
{"N3", "200", "L3", "D3"},
{"N3", "200", "L3.1", "D3.1"},
{"N4", "200", "L4", "D4"},
},
toVerify: []string{"N3"},
toFail: []string{"N2", "N4"},
toRetry: []string{"N1"},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
var msg fleet.SyncML
msg.Xmlns = syncml.SyncCmdNamespace
msg.SyncHdr = fleet.SyncHdr{
VerDTD: syncml.SyncMLSupportedVersion,
VerProto: syncml.SyncMLVerProto,
SessionID: "2",
MsgID: "2",
}
for _, p := range tt.report {
ref := HashLocURI(p.Name, p.LocURI)
msg.AppendCommand(fleet.MDMRaw, fleet.SyncMLCmd{
XMLName: xml.Name{Local: fleet.CmdStatus},
CmdID: uuid.NewString(),
CmdRef: &ref,
Data: ptr.String(p.Status),
})
// the protocol can respond with only a `Status`
// command if the status failed
if p.Status != "200" || p.Data != "" {
msg.AppendCommand(fleet.MDMRaw, fleet.SyncMLCmd{
XMLName: xml.Name{Local: fleet.CmdResults},
CmdID: uuid.NewString(),
CmdRef: &ref,
Items: []fleet.CmdItem{
{Target: ptr.String(p.LocURI), Data: ptr.String(p.Data)},
},
})
}
}
ds.GetHostMDMProfilesExpectedForVerificationFunc = func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
installDate := host.DetailUpdatedAt.Add(-2 * time.Hour)
if tt.withinGracePeriod {
installDate = host.DetailUpdatedAt
}
out := map[string]*fleet.ExpectedMDMProfile{}
for _, profile := range tt.hostProfiles {
out[profile.Name] = &fleet.ExpectedMDMProfile{
Name: profile.Name,
RawProfile: profile.RawContents,
EarliestInstallDate: installDate,
}
}
return out, nil
}
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error {
require.ElementsMatch(t, tt.toVerify, toVerify, "profiles to verify don't match")
require.ElementsMatch(t, tt.toFail, toFail, "profiles to fail don't match")
require.ElementsMatch(t, tt.toRetry, toRetry, "profiles to retry don't match")
return nil
}
ds.GetHostMDMProfilesRetryCountsFunc = func(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error) {
out := []fleet.HostMDMProfileRetryCount{}
for _, profile := range tt.hostProfiles {
out = append(out, fleet.HostMDMProfileRetryCount{
ProfileName: profile.Name,
Retries: profile.RetryCount,
})
}
return out, nil
}
out, err := xml.Marshal(msg)
require.NoError(t, err)
require.NoError(t, VerifyHostMDMProfiles(ctx, ds, host, out))
require.True(t, ds.UpdateHostMDMProfilesVerificationFuncInvoked)
require.True(t, ds.GetHostMDMProfilesExpectedForVerificationFuncInvoked)
ds.UpdateHostMDMProfilesVerificationFuncInvoked = false
ds.GetHostMDMProfilesExpectedForVerificationFuncInvoked = false
})
}
}

View file

@ -0,0 +1,361 @@
package syncml
import (
"bytes"
"fmt"
)
// XML Namespaces and type URLs used by the Microsoft Device Enrollment v2 protocol (MS-MDE2)
const (
DiscoverNS = "http://schemas.microsoft.com/windows/management/2012/01/enrollment"
PolicyNS = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy"
EnrollWSTrust = "http://docs.oasis-open.org/ws-sx/ws-trust/200512"
EnrollSecExt = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
EnrollTType = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken"
EnrollPDoc = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc"
EnrollEncode = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary"
EnrollReq = "http://schemas.microsoft.com/windows/pki/2009/01/enrollment"
EnrollNSS = "http://www.w3.org/2003/05/soap-envelope"
EnrollNSA = "http://www.w3.org/2005/08/addressing"
EnrollXSI = "http://www.w3.org/2001/XMLSchema-instance"
EnrollXSD = "http://www.w3.org/2001/XMLSchema"
EnrollXSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
ActionNsDiag = "http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics"
ActionNsDiscovery = "http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/DiscoverResponse"
ActionNsPolicy = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPoliciesResponse"
ActionNsEnroll = EnrollReq + "/RSTRC/wstep"
EnrollReqTypePKCS10 = EnrollReq + "#PKCS10"
EnrollReqTypePKCS7 = EnrollReq + "#PKCS7"
BinarySecurityDeviceEnroll = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentUserToken"
BinarySecurityAzureEnroll = "urn:ietf:params:oauth:token-type:jwt"
)
// Soap Error constants
// Details here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/0a78f419-5fd7-4ddb-bc76-1c0f7e11da23
const (
// Message format is bad
SoapErrorMessageFormat = "s:messageformat"
// User not recognized
SoapErrorAuthentication = "s:authentication"
// User not allowed to enroll
SoapErrorAuthorization = "s:authorization"
// Failed to get certificate
SoapErrorCertificateRequest = "s:certificaterequest"
// Generic failure from management server, such as a database access error
SoapErrorEnrollmentServer = "s:enrollmentserver"
// The server hit an unexpected issue
SoapErrorInternalServiceFault = "s:internalservicefault"
// Cannot parse the security header
SoapErrorInvalidSecurity = "a:invalidsecurity"
)
// MS-MDM Status Code constants
// Details here: https://learn.microsoft.com/en-us/windows/client-management/oma-dm-protocol-support
const (
// The SyncML command completed successfully
CmdStatusOK = "200"
// Accepted for processing
// This code denotes an asynchronous operation, such as a request to run a remote execution of an application
CmdStatusAcceptedForProcessing = "202"
// Authentication accepted
// Normally you'll only see this code in response to the SyncHdr element (used for authentication in the OMA-DM standard)
// You may see this code if you look at OMA DM logs, but CSPs don't typically generate this code.
CmdStatusAuthenticationAccepted = "212"
// Operation canceled
// The SyncML command completed successfully, but no more commands will be processed within the session.
CmdStatusOperationCancelled = "214"
// Not executed
// A command wasn't executed as a result of user interaction to cancel the command.
CmdStatusNotExecuted = "215"
// Atomic roll back OK
// A command was inside an Atomic element and Atomic failed, thhis command was rolled back successfully
CmdStatusAtomicRollbackAccepted = "216"
// Bad request. The requested command couldn't be performed because of malformed syntax.
// CSPs don't usually generate this error, however you might see it if your SyncML is malformed.
CmdStatusBadRequest = "400"
// Invalid credentials
// The requested command failed because the requestor must provide proper authentication. CSPs don't usually generate this error
CmdStatusInvalidCredentials = "401"
// Forbidden
// The requested command failed, but the recipient understood the requested command
CmdStatusForbidden = "403"
// Not found
// The requested target wasn't found. This code will be generated if you query a node that doesn't exist
CmdStatusNotFound = "404"
// Command not allowed
// This respond code will be generated if you try to write to a read-only node
CmdStatusNotAllowed = "405"
// Optional feature not supported
// This response code will be generated if you try to access a property that the CSP doesn't support
CmdStatusOptionalFeature = "406"
// Unsupported type or format
// This response code can result from XML parsing or formatting errors
CmdStatusUnsupportedType = "415"
// Already exists
// This response code occurs if you attempt to add a node that already exists
CmdStatusAlreadyExists = "418"
// Permission Denied
// The requested command failed because the sender doesn't have adequate access control permissions (ACL) on the recipient.
// An "Access denied" errors usually get translated to this response code.
CmdStatusPermissionDenied = "425"
// Command failed. Generic failure.
// The recipient encountered an unexpected condition, which prevented it from fulfilling the request
// This response code will occur when the SyncML DPU can't map the originating error code
CmdStatusCommandFailed = "500"
// Atomic failed
// One of the operations in an Atomic block failed
CmdStatusAtomicFailed = "507"
// Atomic roll back failed
// An Atomic operation failed and the command wasn't rolled back successfully.
CmdStatusAtomicRollbackFailed = "516"
)
// MS-MDM Supported Alerts
// Details on MS-MDM 2.2.7.2: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mdm/72c6ea01-121c-48f9-85da-a26bb12aad51
const (
// SERVER-INITIATED MGMT
// Server-initiated device management session
CmdAlertServerInitiatedManagement = "1200"
// CLIENT-INITIATED MGMT
// Client-initiated device management session
CmdAlertClientInitiatedManagement = "1201"
// NEXT MESSAGE
// Request for the next message of a large object package
CmdAlertNextMessage = "1222"
// SESSION ABORT
// Informs recipient that the sender wishes to abort the DM session
CmdAlertSessionAbort = "1223"
// CLIENT EVENT
// Informs server that an event has occurred on the client
CmdAlertClientEvent = "1224"
// NO END OF DATA
// End of Data for chunked object not received.
CmdAlertNoEndOfData = "1225"
// GENERIC ALERT
// Generic client generated alert with or without a reference to a Management
CmdAlertGeneric = "1226"
)
const (
FleetBitLockerTargetLocURI = "/Vendor/MSFT/BitLocker"
FleetOSUpdateTargetLocURI = "/Vendor/MSFT/Policy/Config/Update"
FleetWindowsOSUpdatesProfileName = "Windows OS Updates"
)
func FleetReservedProfileNames() map[string]struct{} {
return map[string]struct{}{
FleetWindowsOSUpdatesProfileName: {},
}
}
// MS-MDE2 Message constants
const (
// Minimum supported version
EnrollmentVersionV4 = "4.0"
// Maximum supported version
EnrollmentVersionV5 = "5.0"
// xsi:nil indicates value is not present
DefaultStateXSI = "true"
// Supported authentication types
AuthOnPremise = "OnPremise"
// SOAP Fault codes
SoapFaultRecv = "s:receiver"
// SOAP Fault default error locale
SoapFaultLocale = "en-us"
// HTTP Content Type for SOAP responses
SoapContentType = "application/soap+xml; charset=utf-8"
// HTTP Content Type for SyncML MDM responses
SyncMLContentType = "application/vnd.syncml.dm+xml"
// HTTP Content Type for Webcontainer responses
WebContainerContentType = "text/html; charset=UTF-8"
// Minimal Key Length for SHA1WithRSA encryption
PolicyMinKeyLength = "2048"
// Certificate Validity Period in seconds (365 days)
PolicyCertValidityPeriodInSecs = "31536000"
// Certificate Renewal Period in seconds (180 days)
PolicyCertRenewalPeriodInSecs = "15552000"
// Supported Enroll types gathered from MS-MDE2 Spec Section 2.2.9.3
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/f7553554-b6e1-4a0d-abd6-6a2534503af7
// Supported Enroll Type Device
ReqSecTokenEnrollTypeDevice = "Device"
// Supported Enroll Type Full
ReqSecTokenEnrollTypeFull = "Full"
// Provisioning Doc Certificate Renewal Period (365 days)
WstepCertRenewalPeriodInDays = "365"
// Provisioning Doc Server supports ROBO auto certificate renewal
// TODO: Add renewal support
WstepROBOSupport = "true"
// Provisioning Doc Server retry interval
WstepRenewRetryInterval = "4"
// The PROVIDER-ID paramer specifies the server identifier for a management server used in the current management session
DocProvisioningAppProviderID = "Fleet"
// The NAME parameter is used in the APPLICATION characteristic to specify a user readable application identity
DocProvisioningAppName = DocProvisioningAppProviderID
// The CONNRETRYFREQ parameter is used in the APPLICATION characteristic to specify a user readable application identity
DocProvisioningAppConnRetryFreq = "6"
// The INITIALBACKOFFTIME parameter is used to specify the initial wait time in milliseconds when the DM client retries for the first time
DocProvisioningAppInitialBackoffTime = "30000"
// The MAXBACKOFFTIME parameter is used to specify the maximum number of milliseconds to sleep after package-sending failure
DocProvisioningAppMaxBackoffTime = "120000"
// The DocProvisioningVersion attributes defines the version of the provisioning document format
DocProvisioningVersion = "1.1"
// The number of times the DM client should retry to connect to the server when the client is initially configured or enrolled to communicate with the server.
DmClientCSPNumberOfFirstRetries = "8"
// The waiting time (in minutes) for the initial set of retries as specified by the number of retries in NumberOfFirstRetries
DmClientCSPIntervalForFirstSetOfRetries = "15"
// The number of times the DM client should retry a second round of connecting to the server when the client is initially configured/enrolled to communicate with the server
DmClientCSPNumberOfSecondRetries = "5"
// The waiting time (in minutes) for the second set of retries as specified by the number of retries in NumberOfSecondRetries
DmClientCSPIntervalForSecondSetOfRetries = "3"
// The number of times the DM client should retry connecting to the server when the client is initially configured/enrolled to communicate with the server
DmClientCSPNumberOfRemainingScheduledRetries = "0"
// The waiting time (in minutes) for the initial set of retries as specified by the number of retries in NumberOfRemainingScheduledRetries
DmClientCSPIntervalForRemainingScheduledRetries = "1560"
// It allows the IT admin to require the device to start a management session on any user login, regardless of if the user has preciously logged in
DmClientCSPPollOnLogin = "true"
// It specifies whether the DM client should send out a request pending alert in case the device response to a DM request is too slow.
DmClientCSPEnableOmaDmKeepAliveMessage = "true"
// CSR issuer should be verified during enrollment
EnrollVerifyIssue = true
// Int type used by the DM client configuration
DmClientIntType = "integer"
// Bool type used by the DM client configuration
DmClientBoolType = "boolean"
// Additional Context items present on the RequestSecurityToken token message
ReqSecTokenContextItemUXInitiated = "UXInitiated"
ReqSecTokenContextItemHWDevID = "HWDevID"
ReqSecTokenContextItemLocale = "Locale"
ReqSecTokenContextItemTargetedUserLoggedIn = "TargetedUserLoggedIn"
ReqSecTokenContextItemOSEdition = "OSEdition"
ReqSecTokenContextItemDeviceName = "DeviceName"
ReqSecTokenContextItemDeviceID = "DeviceID"
ReqSecTokenContextItemEnrollmentType = "EnrollmentType"
ReqSecTokenContextItemDeviceType = "DeviceType"
ReqSecTokenContextItemOSVersion = "OSVersion"
ReqSecTokenContextItemApplicationVersion = "ApplicationVersion"
ReqSecTokenContextItemNotInOobe = "NotInOobe"
ReqSecTokenContextItemRequestVersion = "RequestVersion"
// APPRU query param expected by STS Auth endpoint
STSAuthAppRu = "appru"
// Login related query param expected by STS Auth endpoint
STSLoginHint = "login_hint"
// redirect_uri query param expected by TOS endpoint
TOCRedirectURI = "redirect_uri"
// client-request-id query param expected by TOS endpoint
TOCReqID = "client-request-id"
// Alert payload user-driven unenrollment request
AlertUserUnenrollmentRequest = "com.microsoft:mdm.unenrollment.userrequest"
// FleetdWindowsInstallerGUID is the GUID used for fleetd on Windows
FleetdWindowsInstallerGUID = "./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7BA427C0AA-E2D5-40DF-ACE8-0D726A6BE096%7D/DownloadInstall"
)
// MS-MDM Message constants
const (
// SyncML Message Content Type
SyncMLMsgContentType = "application/vnd.syncml.dm+xml"
// SyncML Message Meta Namespace
SyncMLMetaNamespace = "syncml:metinf"
// SyncML Cmd Namespace
SyncCmdNamespace = "SYNCML:SYNCML1.2"
// SyncML Message Header Name
SyncMLHdrName = "SyncHdr"
// Supported SyncML version
SyncMLSupportedVersion = "1.2"
// SyncML ver protocol version
SyncMLVerProto = "DM/" + SyncMLSupportedVersion
)
func ForTestWithData(locURIs map[string]string) []byte {
var syncMLBuf bytes.Buffer
for locURI, data := range locURIs {
syncMLBuf.WriteString(fmt.Sprintf(`
<Replace>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
<Data>%s</Data>
</Item>
</Replace>`, locURI, data))
}
return syncMLBuf.Bytes()
}

View file

@ -18,6 +18,7 @@ import (
"time"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/golang-jwt/jwt/v4"
"github.com/micromdm/nanomdm/cryptoutil"
"go.mozilla.org/pkcs7"
@ -293,7 +294,7 @@ func GetAzureAuthTokenClaims(tokenStr string) (AzureData, error) {
}
func populateClientCert(sn *big.Int, subject string, issuerCert *x509.Certificate, csr *x509.CertificateRequest) (*x509.Certificate, error) {
certRenewalPeriodInSecsInt, err := strconv.Atoi(PolicyCertRenewalPeriodInSecs)
certRenewalPeriodInSecsInt, err := strconv.Atoi(syncml.PolicyCertRenewalPeriodInSecs)
if err != nil {
return nil, fmt.Errorf("invalid renewal time: %w", err)
}
@ -302,7 +303,7 @@ func populateClientCert(sn *big.Int, subject string, issuerCert *x509.Certificat
yearDuration := 365 * 24 * time.Hour
certSubject := pkix.Name{
OrganizationalUnit: []string{DocProvisioningAppProviderID},
OrganizationalUnit: []string{syncml.DocProvisioningAppProviderID},
CommonName: subject,
}
@ -335,7 +336,7 @@ func populateClientCert(sn *big.Int, subject string, issuerCert *x509.Certificat
// GetClientCSR returns the client certificate signing request from the BinarySecurityToken
func GetClientCSR(binSecTokenData string, tokenType string) (*x509.CertificateRequest, error) {
// Checking if this is a valid enroll security token (CSR)
if (tokenType != EnrollReqTypePKCS10) && (tokenType != EnrollReqTypePKCS7) {
if (tokenType != syncml.EnrollReqTypePKCS10) && (tokenType != syncml.EnrollReqTypePKCS7) {
return nil, fmt.Errorf("token type is not valid for MDM enrollment: %s", tokenType)
}
@ -347,7 +348,7 @@ func GetClientCSR(binSecTokenData string, tokenType string) (*x509.CertificateRe
// Sanity checks on binary signature token
// Sanity checks are done on PKCS10 for the moment
if tokenType == EnrollReqTypePKCS7 {
if tokenType == syncml.EnrollReqTypePKCS7 {
// Parse the CSR in PKCS7 Syntax Standard
pk7CSR, err := pkcs7.Parse(rawCSR)
if err != nil {

View file

@ -502,13 +502,13 @@ type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet
type SetDiskEncryptionResetStatusFunc func(ctx context.Context, hostID uint, status bool) error
type UpdateHostMDMProfilesVerificationFunc func(ctx context.Context, hostUUID string, toVerify []string, toFail []string, toRetry []string) error
type UpdateHostMDMProfilesVerificationFunc func(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error
type GetHostMDMProfilesExpectedForVerificationFunc func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error)
type GetHostMDMProfilesRetryCountsFunc func(ctx context.Context, hostUUID string) ([]fleet.HostMDMProfileRetryCount, error)
type GetHostMDMProfilesRetryCountsFunc func(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error)
type GetHostMDMProfileRetryCountByCommandUUIDFunc func(ctx context.Context, hostUUID string, cmdUUID string) (fleet.HostMDMProfileRetryCount, error)
type GetHostMDMProfileRetryCountByCommandUUIDFunc func(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error)
type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string) error
@ -3566,11 +3566,11 @@ func (s *DataStore) SetDiskEncryptionResetStatus(ctx context.Context, hostID uin
return s.SetDiskEncryptionResetStatusFunc(ctx, hostID, status)
}
func (s *DataStore) UpdateHostMDMProfilesVerification(ctx context.Context, hostUUID string, toVerify []string, toFail []string, toRetry []string) error {
func (s *DataStore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error {
s.mu.Lock()
s.UpdateHostMDMProfilesVerificationFuncInvoked = true
s.mu.Unlock()
return s.UpdateHostMDMProfilesVerificationFunc(ctx, hostUUID, toVerify, toFail, toRetry)
return s.UpdateHostMDMProfilesVerificationFunc(ctx, host, toVerify, toFail, toRetry)
}
func (s *DataStore) GetHostMDMProfilesExpectedForVerification(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
@ -3580,18 +3580,18 @@ func (s *DataStore) GetHostMDMProfilesExpectedForVerification(ctx context.Contex
return s.GetHostMDMProfilesExpectedForVerificationFunc(ctx, host)
}
func (s *DataStore) GetHostMDMProfilesRetryCounts(ctx context.Context, hostUUID string) ([]fleet.HostMDMProfileRetryCount, error) {
func (s *DataStore) GetHostMDMProfilesRetryCounts(ctx context.Context, host *fleet.Host) ([]fleet.HostMDMProfileRetryCount, error) {
s.mu.Lock()
s.GetHostMDMProfilesRetryCountsFuncInvoked = true
s.mu.Unlock()
return s.GetHostMDMProfilesRetryCountsFunc(ctx, hostUUID)
return s.GetHostMDMProfilesRetryCountsFunc(ctx, host)
}
func (s *DataStore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, hostUUID string, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
func (s *DataStore) GetHostMDMProfileRetryCountByCommandUUID(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
s.mu.Lock()
s.GetHostMDMProfileRetryCountByCommandUUIDFuncInvoked = true
s.mu.Unlock()
return s.GetHostMDMProfileRetryCountByCommandUUIDFunc(ctx, hostUUID, cmdUUID)
return s.GetHostMDMProfileRetryCountByCommandUUIDFunc(ctx, host, cmdUUID)
}
func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {

View file

@ -2417,6 +2417,13 @@ func ReconcileAppleProfiles(
commander *apple_mdm.MDMAppleCommander,
logger kitlog.Logger,
) error {
appConfig, err := ds.AppConfig(ctx)
if err != nil {
return fmt.Errorf("reading app config: %w", err)
}
if !appConfig.MDM.EnabledAndConfigured {
return nil
}
if err := ensureFleetdConfig(ctx, ds, logger); err != nil {
logger.Log("err", "unable to ensure a fleetd configuration profiles are in place", "details", err)
}

View file

@ -1242,13 +1242,13 @@ func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) {
require.Equal(t, c.want, profile)
return nil
}
ds.GetHostMDMProfileRetryCountByCommandUUIDFunc = func(ctx context.Context, hstUUID, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
require.Equal(t, hostUUID, hstUUID)
ds.GetHostMDMProfileRetryCountByCommandUUIDFunc = func(ctx context.Context, host *fleet.Host, cmdUUID string) (fleet.HostMDMProfileRetryCount, error) {
require.Equal(t, hostUUID, host.UUID)
require.Equal(t, commandUUID, cmdUUID)
return fleet.HostMDMProfileRetryCount{ProfileIdentifier: profileIdentifier, Retries: c.prevRetries}, nil
}
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, hostUUID string, toVerify, toFail, toRetry []string) error {
require.Equal(t, hostUUID, hostUUID)
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
require.Equal(t, hostUUID, host.UUID)
require.Nil(t, toVerify)
require.Nil(t, toFail)
require.ElementsMatch(t, toRetry, []string{profileIdentifier})
@ -2199,6 +2199,7 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
appCfg := &fleet.AppConfig{}
appCfg.ServerSettings.ServerURL = "https://test.example.com"
appCfg.MDM.EnabledAndConfigured = true
return appCfg, nil
}

View file

@ -24,7 +24,7 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/go-kit/log"
@ -5674,7 +5674,7 @@ func checkWindowsOSUpdatesProfile(t *testing.T, ds *mysql.Datastore, teamID *uin
if teamID != nil {
globalOrTeamID = *teamID
}
err := sqlx.GetContext(ctx, tx, &prof, `SELECT profile_uuid, syncml FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`, globalOrTeamID, microsoft_mdm.FleetWindowsOSUpdatesProfileName)
err := sqlx.GetContext(ctx, tx, &prof, `SELECT profile_uuid, syncml FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`, globalOrTeamID, syncml.FleetWindowsOSUpdatesProfileName)
if errors.Is(err, sql.ErrNoRows) {
return nil
}

View file

@ -39,6 +39,7 @@ import (
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/mock"
"github.com/fleetdm/fleet/v4/server/service/schedule"
@ -647,7 +648,7 @@ func (s *integrationMDMTestSuite) TestAppleProfileManagement() {
s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamSummary, &expectedTeamSummary) // host still verifying team profiles
}
func (s *integrationMDMTestSuite) TestProfileRetries() {
func (s *integrationMDMTestSuite) TestAppleProfileRetries() {
t := s.T()
ctx := context.Background()
@ -686,7 +687,7 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
mobileconfig.FleetdConfigPayloadIdentifier: 0,
}
checkRetryCounts := func(t *testing.T) {
counts, err := s.ds.GetHostMDMProfilesRetryCounts(ctx, h.UUID)
counts, err := s.ds.GetHostMDMProfilesRetryCounts(ctx, h)
require.NoError(t, err)
require.Len(t, counts, len(expectedRetryCounts))
for _, c := range counts {
@ -933,6 +934,270 @@ func (s *integrationMDMTestSuite) TestProfileRetries() {
})
}
func (s *integrationMDMTestSuite) TestWindowsProfileRetries() {
t := s.T()
ctx := context.Background()
testProfiles := map[string][]byte{
"N1": syncml.ForTestWithData(map[string]string{"L1": "D1"}),
"N2": syncml.ForTestWithData(map[string]string{"L2": "D2", "L3": "D3"}),
}
h, mdmDevice := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
expectedProfileStatuses := map[string]fleet.MDMDeliveryStatus{
"N1": fleet.MDMDeliveryVerifying,
"N2": fleet.MDMDeliveryVerifying,
}
checkProfilesStatus := func(t *testing.T) {
storedProfs, err := s.ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
require.NoError(t, err)
require.Len(t, storedProfs, len(expectedProfileStatuses))
for _, p := range storedProfs {
want, ok := expectedProfileStatuses[p.Name]
require.True(t, ok, "unexpected profile: %s", p.Name)
require.Equal(t, want, *p.Status, "expected status %s but got %s for profile: %s", want, *p.Status, p.Name)
}
}
expectedRetryCounts := map[string]uint{
"N1": 0,
"N2": 0,
}
checkRetryCounts := func(t *testing.T) {
counts, err := s.ds.GetHostMDMProfilesRetryCounts(ctx, h)
require.NoError(t, err)
require.Len(t, counts, len(expectedRetryCounts))
for _, c := range counts {
want, ok := expectedRetryCounts[c.ProfileName]
require.True(t, ok, "unexpected profile: %s", c.ProfileName)
require.Equal(t, want, c.Retries, "expected retry count %d but got %d for profile: %s", want, c.Retries, c.ProfileName)
}
}
type profileData struct {
Status string
LocURI string
Data string
}
hostProfileReports := map[string][]profileData{
"N1": {{"200", "L1", "D1"}},
"N2": {{"200", "L2", "D2"}, {"200", "L3", "D3"}},
}
reportHostProfs := func(t *testing.T, profileNames ...string) {
var responseOps []*mdm_types.SyncMLCmd
for _, profileName := range profileNames {
report, ok := hostProfileReports[profileName]
require.True(t, ok)
for _, p := range report {
ref := microsoft_mdm.HashLocURI(profileName, p.LocURI)
responseOps = append(responseOps, &mdm_types.SyncMLCmd{
XMLName: xml.Name{Local: mdm_types.CmdStatus},
CmdID: uuid.NewString(),
CmdRef: &ref,
Data: ptr.String(p.Status),
})
// the protocol can respond with only a `Status`
// command if the status failed
if p.Status != "200" || p.Data != "" {
responseOps = append(responseOps, &mdm_types.SyncMLCmd{
XMLName: xml.Name{Local: mdm_types.CmdResults},
CmdID: uuid.NewString(),
CmdRef: &ref,
Items: []mdm_types.CmdItem{
{Target: ptr.String(p.LocURI), Data: ptr.String(p.Data)},
},
})
}
}
}
msg, err := createSyncMLMessage("2", "2", "foo", "bar", responseOps)
require.NoError(t, err)
out, err := xml.Marshal(msg)
require.NoError(t, err)
require.NoError(t, microsoft_mdm.VerifyHostMDMProfiles(ctx, s.ds, h, out))
}
verifyCommands := func(wantProfileInstalls int, status string) {
s.awaitTriggerProfileSchedule(t)
cmds, err := mdmDevice.StartManagementSession()
require.NoError(t, err)
// profile installs + 2 protocol commands acks
require.Len(t, cmds, wantProfileInstalls+2)
msgID, err := mdmDevice.GetCurrentMsgID()
require.NoError(t, err)
atomicCmds := 0
for _, c := range cmds {
if c.Verb == "Atomic" {
atomicCmds++
}
mdmDevice.AppendResponse(fleet.SyncMLCmd{
XMLName: xml.Name{Local: mdm_types.CmdStatus},
MsgRef: &msgID,
CmdRef: ptr.String(c.Cmd.CmdID),
Cmd: ptr.String(c.Verb),
Data: ptr.String(status),
Items: nil,
CmdID: uuid.NewString(),
})
}
require.Equal(t, wantProfileInstalls, atomicCmds)
cmds, err = mdmDevice.SendResponse()
require.NoError(t, err)
// the ack of the message should be the only returned command
require.Len(t, cmds, 1)
}
t.Run("retry after verifying", func(t *testing.T) {
// upload test profiles then simulate expired grace period by setting updated_at timestamp of profiles back by 48 hours
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
// profiles to install + 2 boilerplate <Status>
verifyCommands(len(testProfiles), syncml.CmdStatusOK)
checkProfilesStatus(t) // all profiles verifying
checkRetryCounts(t) // no retries yet
// report osquery results with N2 missing and confirm N2 marked
// as verifying and other profiles are marked as verified
reportHostProfs(t, "N1")
expectedProfileStatuses["N2"] = fleet.MDMDeliveryPending
expectedProfileStatuses["N1"] = fleet.MDMDeliveryVerified
checkProfilesStatus(t)
expectedRetryCounts["N2"] = 1
checkRetryCounts(t)
// report osquery results with N2 present and confirm that all profiles are verified
verifyCommands(1, syncml.CmdStatusOK)
reportHostProfs(t, "N1", "N2")
expectedProfileStatuses["N2"] = fleet.MDMDeliveryVerified
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that no profiles were sent
verifyCommands(0, syncml.CmdStatusOK)
})
t.Run("retry after verification", func(t *testing.T) {
// report osquery results with N1 missing and confirm that the N1 marked as pending (initial retry)
reportHostProfs(t, "N2")
expectedProfileStatuses["N1"] = fleet.MDMDeliveryPending
checkProfilesStatus(t)
expectedRetryCounts["N1"] = 1
checkRetryCounts(t)
// trigger a profile sync and confirm that the install profile command for N1 was resent
verifyCommands(1, syncml.CmdStatusOK)
// report osquery results with N1 missing again and confirm that the N1 marked as failed (max retries exceeded)
reportHostProfs(t, "N2")
expectedProfileStatuses["N1"] = fleet.MDMDeliveryFailed
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile command for N1 was not resent
verifyCommands(0, syncml.CmdStatusOK)
})
t.Run("retry after device error", func(t *testing.T) {
// add another profile
newProfile := syncml.ForTestWithData(map[string]string{"L3": "D3"})
testProfiles["N3"] = newProfile
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
// trigger a profile sync and confirm that the install profile command for N3 was sent and
// simulate a device error
verifyCommands(1, syncml.CmdStatusAtomicFailed)
expectedProfileStatuses["N3"] = fleet.MDMDeliveryPending
checkProfilesStatus(t)
expectedRetryCounts["N3"] = 1
checkRetryCounts(t)
// trigger a profile sync and confirm that the install profile command for N3 was sent and
// simulate a device ack
verifyCommands(1, syncml.CmdStatusOK)
expectedProfileStatuses["N3"] = fleet.MDMDeliveryVerifying
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// report osquery results with N3 missing and confirm that the N3 marked as failed (max
// retries exceeded)
reportHostProfs(t, "N2")
expectedProfileStatuses["N3"] = fleet.MDMDeliveryFailed
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile command for N3 was not resent
verifyCommands(0, syncml.CmdStatusOK)
})
t.Run("repeated device error", func(t *testing.T) {
// add another profile
testProfiles["N4"] = syncml.ForTestWithData(map[string]string{"L4": "D4"})
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
// trigger a profile sync and confirm that the install profile command for N4 was sent and
// simulate a device error
verifyCommands(1, syncml.CmdStatusAtomicFailed)
expectedProfileStatuses["N4"] = fleet.MDMDeliveryPending
checkProfilesStatus(t)
expectedRetryCounts["N4"] = 1
checkRetryCounts(t)
// trigger a profile sync and confirm that the install profile
// command for N4 was sent and simulate a second device error
verifyCommands(1, syncml.CmdStatusAtomicFailed)
expectedProfileStatuses["N4"] = fleet.MDMDeliveryFailed
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile
// command for N4 was not resent
verifyCommands(0, syncml.CmdStatusOK)
})
t.Run("retry count does not reset", func(t *testing.T) {
// add another profile
testProfiles["N5"] = syncml.ForTestWithData(map[string]string{"L5": "D5"})
//hostProfsByIdent["N5"] = &fleet.HostMacOSProfile{Identifier: "N5", DisplayName: "N5", InstallDate: time.Now()}
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: testProfiles}, http.StatusNoContent)
// trigger a profile sync and confirm that the install profile
// command for N5 was sent and simulate a device error
verifyCommands(1, syncml.CmdStatusAtomicFailed)
expectedProfileStatuses["N5"] = fleet.MDMDeliveryPending
checkProfilesStatus(t)
expectedRetryCounts["N5"] = 1
checkRetryCounts(t)
// trigger a profile sync and confirm that the install profile
// command for N5 was sent and simulate a device ack
verifyCommands(1, syncml.CmdStatusOK)
expectedProfileStatuses["N5"] = fleet.MDMDeliveryVerifying
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// report osquery results with N5 found and confirm that the N5 marked as verified
hostProfileReports["N5"] = []profileData{{"200", "L5", "D5"}}
reportHostProfs(t, "N2", "N5")
expectedProfileStatuses["N5"] = fleet.MDMDeliveryVerified
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile command for N5 was not resent
verifyCommands(0, syncml.CmdStatusOK)
// report osquery results again, this time N5 is missing and confirm that the N5 marked as
// failed (max retries exceeded)
reportHostProfs(t, "N2")
expectedProfileStatuses["N5"] = fleet.MDMDeliveryFailed
checkProfilesStatus(t)
checkRetryCounts(t) // unchanged
// trigger a profile sync and confirm that the install profile command for N5 was not resent
verifyCommands(0, syncml.CmdStatusOK)
})
}
func checkNextPayloads(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, forceDeviceErr bool) ([][]byte, []string) {
var cmd *micromdm.CommandPayload
var err error
@ -3865,6 +4130,7 @@ func (s *integrationMDMTestSuite) TestHostMDMAppleProfilesStatus() {
h, err := s.ds.LoadHostByOrbitNodeKey(ctx, orbitNodeKey)
require.NoError(t, err)
h.OrbitNodeKey = &orbitNodeKey
h.Platform = "darwin"
err = mdmDevice.Enroll()
require.NoError(t, err)
@ -6864,7 +7130,7 @@ func (s *integrationMDMTestSuite) TestValidDiscoveryRequest() {
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -6915,7 +7181,7 @@ func (s *integrationMDMTestSuite) TestInvalidDiscoveryRequest() {
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if response can be unmarshalled to an golang type
var xmlType interface{}
@ -6967,7 +7233,7 @@ func (s *integrationMDMTestSuite) TestNoEmailDiscoveryRequest() {
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -7002,7 +7268,7 @@ func (s *integrationMDMTestSuite) TestValidGetPoliciesRequestWithDeviceToken() {
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -7032,7 +7298,7 @@ func (s *integrationMDMTestSuite) TestValidGetPoliciesRequestWithAzureToken() {
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -7075,7 +7341,7 @@ func (s *integrationMDMTestSuite) TestGetPoliciesRequestWithInvalidUUID() {
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -7108,7 +7374,7 @@ func (s *integrationMDMTestSuite) TestGetPoliciesRequestWithNotElegibleHost() {
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -7142,7 +7408,7 @@ func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithDevice
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -7193,7 +7459,7 @@ func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithAzureT
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -7245,7 +7511,7 @@ func (s *integrationMDMTestSuite) TestInvalidRequestSecurityTokenRequestWithMiss
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SoapContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)
// Checking if SOAP response can be unmarshalled to an golang type
var xmlType interface{}
@ -7305,7 +7571,7 @@ func (s *integrationMDMTestSuite) TestValidGetTOC() {
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.WebContainerContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.WebContainerContentType)
resTOCcontent := string(resBytes)
require.Contains(t, resTOCcontent, "Microsoft.AAD.BrokerPlugin")
@ -7547,8 +7813,8 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
fleetdExecCmd = c
}
}
require.Equal(t, microsoft_mdm.FleetdWindowsInstallerGUID, fleetdAddCmd.Cmd.GetTargetURI())
require.Equal(t, microsoft_mdm.FleetdWindowsInstallerGUID, fleetdExecCmd.Cmd.GetTargetURI())
require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdAddCmd.Cmd.GetTargetURI())
require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdExecCmd.Cmd.GetTargetURI())
}
func (s *integrationMDMTestSuite) TestValidManagementUnenrollRequest() {
@ -7590,7 +7856,7 @@ func (s *integrationMDMTestSuite) TestValidManagementUnenrollRequest() {
// Checking that Command error code was updated
// Checking response headers
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.SyncMLContentType)
require.Contains(t, resp.Header["Content-Type"], syncml.SyncMLContentType)
// Read response data
resBytes, err := io.ReadAll(resp.Body)
@ -8107,10 +8373,10 @@ func (s *integrationMDMTestSuite) TestMDMConfigProfileCRUD() {
assertAppleProfile("foo.txt", "foo", "foo-ident", 0, http.StatusBadRequest, "Couldn't upload. The file should be a .mobileconfig or .xml file.")
// Windows-reserved LocURI
assertWindowsProfile("bitlocker.xml", microsoft_mdm.FleetBitLockerTargetLocURI, 0, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include BitLocker settings.")
assertWindowsProfile("updates.xml", microsoft_mdm.FleetOSUpdateTargetLocURI, testTeam.ID, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include Windows updates settings.")
assertWindowsProfile("bitlocker.xml", syncml.FleetBitLockerTargetLocURI, 0, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include BitLocker settings.")
assertWindowsProfile("updates.xml", syncml.FleetOSUpdateTargetLocURI, testTeam.ID, http.StatusBadRequest, "Couldn't upload. Custom configuration profiles can't include Windows updates settings.")
// Windows-reserved profile name
assertWindowsProfile(microsoft_mdm.FleetWindowsOSUpdatesProfileName+".xml", "./Test", 0, http.StatusBadRequest, `Couldn't upload. Profile name "Windows OS Updates" is not allowed.`)
assertWindowsProfile(syncml.FleetWindowsOSUpdatesProfileName+".xml", "./Test", 0, http.StatusBadRequest, `Couldn't upload. Profile name "Windows OS Updates" is not allowed.`)
// Windows invalid content
body, headers := generateNewProfileMultipartRequest(t, nil, "win.xml", []byte("\x00\x01\x02"), s.token)
@ -9190,9 +9456,9 @@ func (s *integrationMDMTestSuite) newGetPoliciesMsg(deviceToken bool, encodedBin
}
// JWT token by default
tokType := microsoft_mdm.BinarySecurityAzureEnroll
tokType := syncml.BinarySecurityAzureEnroll
if deviceToken {
tokType = microsoft_mdm.BinarySecurityDeviceEnroll
tokType = syncml.BinarySecurityDeviceEnroll
}
return []byte(`
@ -9234,9 +9500,9 @@ func (s *integrationMDMTestSuite) newSecurityTokenMsg(encodedBinToken string, de
}
// JWT token by default
tokType := microsoft_mdm.BinarySecurityAzureEnroll
tokType := syncml.BinarySecurityAzureEnroll
if deviceToken {
tokType = microsoft_mdm.BinarySecurityDeviceEnroll
tokType = syncml.BinarySecurityDeviceEnroll
}
// Preparing the RequestSecurityToken Request message
@ -9456,19 +9722,31 @@ func (s *integrationMDMTestSuite) TestWindowsProfileManagement() {
verifyHostProfileStatus := func(cmds []fleet.ProtoCmdOperation, wantStatus string) {
for _, cmd := range cmds {
var gotStatus string
var gotProfile struct {
Status string `db:"status"`
Retries int `db:"retries"`
}
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
stmt := `SELECT status FROM host_mdm_windows_profiles WHERE command_uuid = ?`
return sqlx.GetContext(context.Background(), q, &gotStatus, stmt, cmd.Cmd.CmdID)
stmt := `
SELECT COALESCE(status, 'pending') as status, retries
FROM host_mdm_windows_profiles
WHERE command_uuid = ?`
return sqlx.GetContext(context.Background(), q, &gotProfile, stmt, cmd.Cmd.CmdID)
})
require.EqualValues(t, fleet.WindowsResponseToDeliveryStatus(wantStatus), gotStatus, "command_uuid", cmd.Cmd.CmdID)
wantDeliveryStatus := fleet.WindowsResponseToDeliveryStatus(wantStatus)
if gotProfile.Retries <= servermdm.MaxProfileRetries && wantDeliveryStatus == mdm_types.MDMDeliveryFailed {
require.EqualValues(t, "pending", gotProfile.Status, "command_uuid", cmd.Cmd.CmdID)
} else {
require.EqualValues(t, wantDeliveryStatus, gotProfile.Status, "command_uuid", cmd.Cmd.CmdID)
}
}
}
verifyProfiles := func(device *mdmtest.TestWindowsMDMClient, n int, fail bool) {
mdmResponseStatus := microsoft_mdm.CmdStatusOK
mdmResponseStatus := syncml.CmdStatusOK
if fail {
mdmResponseStatus = microsoft_mdm.CmdStatusAtomicFailed
mdmResponseStatus = syncml.CmdStatusAtomicFailed
}
s.awaitTriggerProfileSchedule(t)
cmds, err := device.StartManagementSession()
@ -9481,7 +9759,7 @@ func (s *integrationMDMTestSuite) TestWindowsProfileManagement() {
require.NoError(t, err)
for _, c := range cmds {
cmdID := c.Cmd.CmdID
status := microsoft_mdm.CmdStatusOK
status := syncml.CmdStatusOK
if c.Verb == "Atomic" {
atomicCmds = append(atomicCmds, c)
status = mdmResponseStatus
@ -9595,6 +9873,10 @@ func (s *integrationMDMTestSuite) TestWindowsProfileManagement() {
// check that we deleted the old profile in the DB
checkHostsProfilesMatch(host, teamProfiles)
// a second sync gets the profile again, because of delivery retries.
// Succeed that one
verifyProfiles(mdmDevice, 1, false)
// another sync shouldn't return profiles
verifyProfiles(mdmDevice, 0, false)
}
@ -9773,18 +10055,18 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
// profiles with reserved Windows location URIs
// bitlocker
res := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: map[string][]byte{
"N1": mobileconfigForTest("N1", "I1"),
microsoft_mdm.FleetBitLockerTargetLocURI: syncMLForTest(fmt.Sprintf("%s/Foo", microsoft_mdm.FleetBitLockerTargetLocURI)),
"N3": syncMLForTest("./Foo/Bar"),
"N1": mobileconfigForTest("N1", "I1"),
syncml.FleetBitLockerTargetLocURI: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetBitLockerTargetLocURI)),
"N3": syncMLForTest("./Foo/Bar"),
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg := extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Custom configuration profiles can't include BitLocker settings. To control these settings, use the mdm.enable_disk_encryption option.")
// os updates
res = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: map[string][]byte{
"N1": mobileconfigForTest("N1", "I1"),
microsoft_mdm.FleetOSUpdateTargetLocURI: syncMLForTest(fmt.Sprintf("%s/Foo", microsoft_mdm.FleetOSUpdateTargetLocURI)),
"N3": syncMLForTest("./Foo/Bar"),
"N1": mobileconfigForTest("N1", "I1"),
syncml.FleetOSUpdateTargetLocURI: syncMLForTest(fmt.Sprintf("%s/Foo", syncml.FleetOSUpdateTargetLocURI)),
"N3": syncMLForTest("./Foo/Bar"),
}}, http.StatusUnprocessableEntity, "team_id", strconv.Itoa(int(tm.ID)))
errMsg = extractServerErrorText(res.Body)
require.Contains(t, errMsg, "Custom configuration profiles can't include Windows updates settings. To control these settings, use the mdm.windows_updates option.")

View file

@ -24,7 +24,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/go-kit/kit/log/level"
"github.com/go-sql-driver/mysql"
@ -1118,7 +1118,7 @@ func (svc *Service) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUU
}
// prevent deleting Windows OS Updates profile (controlled by the OS Updates settings)
if _, ok := microsoft_mdm.FleetReservedProfileNames()[prof.Name]; ok {
if _, ok := syncml.FleetReservedProfileNames()[prof.Name]; ok {
err := &fleet.BadRequestError{Message: "Profiles managed by Fleet can't be deleted using this endpoint."}
return ctxerr.Wrap(ctx, err, "validate profile")
}

View file

@ -23,11 +23,12 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/log/level"
mdm_types "github.com/fleetdm/fleet/v4/server/fleet"
mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/google/uuid"
)
@ -80,7 +81,7 @@ func (r SoapResponseContainer) hijackRender(ctx context.Context, w http.Response
xmlRes = append(xmlRes, '\n')
w.Header().Set("Content-Type", mdm.SoapContentType)
w.Header().Set("Content-Type", syncml.SoapContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(xmlRes)))
w.WriteHeader(http.StatusOK)
if n, err := w.Write(xmlRes); err != nil {
@ -141,7 +142,7 @@ func (r SyncMLResponseMsgContainer) hijackRender(ctx context.Context, w http.Res
xmlRes = append(xmlRes, '\n')
w.Header().Set("Content-Type", mdm.SyncMLContentType)
w.Header().Set("Content-Type", syncml.SyncMLContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(xmlRes)))
w.WriteHeader(http.StatusOK)
if n, err := w.Write(xmlRes); err != nil {
@ -178,7 +179,7 @@ func (req MDMWebContainer) error() error { return req.Err }
func (req MDMWebContainer) hijackRender(ctx context.Context, w http.ResponseWriter) {
resData := []byte(*req.Data + "\n")
w.Header().Set("Content-Type", mdm.WebContainerContentType)
w.Header().Set("Content-Type", syncml.WebContainerContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(resData)))
w.WriteHeader(http.StatusOK)
if n, err := w.Write(resData); err != nil {
@ -221,10 +222,10 @@ func NewDiscoverResponse(authPolicy string, policyUrl string, enrollmentUrl stri
}
return mdm_types.DiscoverResponse{
XMLNS: mdm.DiscoverNS,
XMLNS: syncml.DiscoverNS,
DiscoverResult: mdm_types.DiscoverResult{
AuthPolicy: authPolicy,
EnrollmentVersion: mdm.EnrollmentVersionV4,
EnrollmentVersion: syncml.EnrollmentVersionV4,
EnrollmentPolicyServiceUrl: policyUrl,
EnrollmentServiceUrl: enrollmentUrl,
},
@ -238,25 +239,25 @@ func NewGetPoliciesResponse(minimalKeyLength string, certificateValidityPeriodSe
}
return mdm_types.GetPoliciesResponse{
XMLNS: mdm.PolicyNS,
XMLNS: syncml.PolicyNS,
Response: mdm_types.Response{
PolicyFriendlyName: mdm_types.ContentAttr{
Xsi: mdm.DefaultStateXSI,
XMLNS: mdm.EnrollXSI,
Xsi: syncml.DefaultStateXSI,
XMLNS: syncml.EnrollXSI,
},
NextUpdateHours: mdm_types.ContentAttr{
Xsi: mdm.DefaultStateXSI,
XMLNS: mdm.EnrollXSI,
Xsi: syncml.DefaultStateXSI,
XMLNS: syncml.EnrollXSI,
},
PoliciesNotChanged: mdm_types.ContentAttr{
Xsi: mdm.DefaultStateXSI,
XMLNS: mdm.EnrollXSI,
Xsi: syncml.DefaultStateXSI,
XMLNS: syncml.EnrollXSI,
},
Policies: mdm_types.Policies{
Policy: mdm_types.GPPolicy{
PolicyOIDReference: "0",
CAs: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
Attributes: mdm_types.Attributes{
CommonName: "FleetDMAttributes",
@ -275,42 +276,42 @@ func NewGetPoliciesResponse(minimalKeyLength string, certificateValidityPeriodSe
AutoEnroll: "false",
},
SupersededPolicies: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
PrivateKeyFlags: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
SubjectNameFlags: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
EnrollmentFlags: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
GeneralFlags: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
RARequirements: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
KeyArchivalAttributes: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
Extensions: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
PrivateKeyAttributes: mdm_types.PrivateKeyAttributes{
MinimalKeyLength: minimalKeyLength,
KeySpec: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
KeyUsageProperty: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
Permissions: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
AlgorithmOIDReference: mdm_types.GenericAttr{
Xsi: mdm.DefaultStateXSI,
Xsi: syncml.DefaultStateXSI,
},
CryptoProviders: []mdm_types.ProviderAttr{
{Content: "Microsoft Platform Crypto Provider"},
@ -352,25 +353,25 @@ func NewRequestSecurityTokenResponseCollection(provisionedToken string) (mdm_typ
return mdm_types.RequestSecurityTokenResponseCollection{}, errors.New("invalid parameters")
}
enrollSecExtVal := mdm.EnrollSecExt
enrollSecExtVal := syncml.EnrollSecExt
return mdm_types.RequestSecurityTokenResponseCollection{
XMLNS: mdm.EnrollWSTrust,
XMLNS: syncml.EnrollWSTrust,
RequestSecurityTokenResponse: mdm_types.RequestSecurityTokenResponse{
TokenType: mdm.EnrollTType,
TokenType: syncml.EnrollTType,
DispositionMessage: mdm_types.SecAttr{
Content: "",
XMLNS: mdm.EnrollReq,
XMLNS: syncml.EnrollReq,
},
RequestID: mdm_types.SecAttr{
Content: "0",
XMLNS: mdm.EnrollReq,
XMLNS: syncml.EnrollReq,
},
RequestedSecurityToken: mdm_types.RequestedSecurityToken{
BinarySecurityToken: mdm_types.BinarySecurityToken{
Content: provisionedToken,
XMLNS: &enrollSecExtVal,
ValueType: mdm.EnrollPDoc,
EncodingType: mdm.EnrollEncode,
ValueType: syncml.EnrollPDoc,
EncodingType: syncml.EnrollEncode,
},
},
},
@ -382,7 +383,7 @@ func NewSoapFault(errorType string, origMessage int, errorMessage error) mdm_typ
return mdm_types.SoapFault{
OriginalMessageType: origMessage,
Code: mdm_types.Code{
Value: mdm.SoapFaultRecv,
Value: syncml.SoapFaultRecv,
Subcode: mdm_types.Subcode{
Value: errorType,
},
@ -390,7 +391,7 @@ func NewSoapFault(errorType string, origMessage int, errorMessage error) mdm_typ
Reason: mdm_types.Reason{
Text: mdm_types.ReasonText{
Content: errorMessage.Error(),
Lang: mdm.SoapFaultLocale,
Lang: syncml.SoapFaultLocale,
},
},
}
@ -427,16 +428,16 @@ func NewSoapResponse(payload interface{}, relatesTo string) (fleet.SoapResponse,
// Useful constants
// Some of these are string urls to be assigned to pointers - they need to have a type and cannot be const literals
var (
urlNSS = mdm.EnrollNSS
urlNSA = mdm.EnrollNSA
urlXSI = mdm.EnrollXSI
urlXSD = mdm.EnrollXSD
urlXSU = mdm.EnrollXSU
urlDiag = mdm.ActionNsDiag
urlDiscovery = mdm.ActionNsDiscovery
urlPolicy = mdm.ActionNsPolicy
urlEnroll = mdm.ActionNsEnroll
urlSecExt = mdm.EnrollSecExt
urlNSS = syncml.EnrollNSS
urlNSA = syncml.EnrollNSA
urlXSI = syncml.EnrollXSI
urlXSD = syncml.EnrollXSD
urlXSU = syncml.EnrollXSU
urlDiag = syncml.ActionNsDiag
urlDiscovery = syncml.ActionNsDiscovery
urlPolicy = syncml.ActionNsPolicy
urlEnroll = syncml.ActionNsEnroll
urlSecExt = syncml.EnrollSecExt
MUValue = "1"
timestampID = "_0"
secWindowStartTimeMin = -5
@ -600,9 +601,9 @@ func NewCertStoreProvisioningData(enrollmentType string, identityFingerprint str
}),
newCharacteristic("WSTEP", nil, []mdm_types.Characteristic{
newCharacteristic("Renew", []mdm_types.Param{
newParm("ROBOSupport", mdm.WstepROBOSupport, "boolean"),
newParm("RenewPeriod", mdm.WstepCertRenewalPeriodInDays, "integer"),
newParm("RetryInterval", mdm.WstepRenewRetryInterval, "integer"),
newParm("ROBOSupport", syncml.WstepROBOSupport, "boolean"),
newParm("RenewPeriod", syncml.WstepCertRenewalPeriodInDays, "integer"),
newParm("RetryInterval", syncml.WstepRenewRetryInterval, "integer"),
}, nil),
}),
})
@ -618,13 +619,13 @@ func NewCertStoreProvisioningData(enrollmentType string, identityFingerprint str
func NewApplicationProvisioningData(mdmEndpoint string) mdm_types.Characteristic {
provDoc := newCharacteristic("APPLICATION", []mdm_types.Param{
// The PROVIDER-ID parameter specifies the server identifier for a management server used in the current management session
newParm("PROVIDER-ID", mdm.DocProvisioningAppProviderID, ""),
newParm("PROVIDER-ID", syncml.DocProvisioningAppProviderID, ""),
// The APPID parameter is used to differentiate the types of available application services and protocols.
newParm("APPID", "w7", ""),
// The NAME parameter is used in the APPLICATION characteristic to specify a user readable application identity.
newParm("NAME", mdm.DocProvisioningAppName, ""),
newParm("NAME", syncml.DocProvisioningAppName, ""),
// The ADDR parameter is used in the APPADDR param to get or set the address of the OMA DM server.
newParm("ADDR", mdmEndpoint, ""),
@ -632,13 +633,13 @@ func NewApplicationProvisioningData(mdmEndpoint string) mdm_types.Characteristic
// The ROLE parameter is used in the APPLICATION characteristic to specify the security application chamber that the DM session should run with when communicating with the DM server.
// The BACKCOMPATRETRYFREQ parameter is used to specify how many retries the DM client performs when there are Connection Manager-level or WinInet-level errors
newParm("CONNRETRYFREQ", mdm.DocProvisioningAppConnRetryFreq, ""),
newParm("CONNRETRYFREQ", syncml.DocProvisioningAppConnRetryFreq, ""),
// The INITIALBACKOFFTIME parameter is used to specify the initial wait time in milliseconds when the DM client retries for the first time
newParm("INITIALBACKOFFTIME", mdm.DocProvisioningAppInitialBackoffTime, ""),
newParm("INITIALBACKOFFTIME", syncml.DocProvisioningAppInitialBackoffTime, ""),
// The MAXBACKOFFTIME parameter is used to specify the maximum number of milliseconds to sleep after package-sending failure
newParm("MAXBACKOFFTIME", mdm.DocProvisioningAppMaxBackoffTime, ""),
newParm("MAXBACKOFFTIME", syncml.DocProvisioningAppMaxBackoffTime, ""),
// The DEFAULTENCODING parameter is used to specify whether the DM client should use WBXML or XML for the DM package when communicating with the server.
newParm("DEFAULTENCODING", "application/vnd.syncml.dm+xml", ""),
@ -676,17 +677,17 @@ func NewApplicationProvisioningData(mdmEndpoint string) mdm_types.Characteristic
func NewDMClientProvisioningData() mdm_types.Characteristic {
dmClient := newCharacteristic("DMClient", nil, []mdm_types.Characteristic{
newCharacteristic("Provider", nil, []mdm_types.Characteristic{
newCharacteristic(mdm.DocProvisioningAppProviderID,
newCharacteristic(syncml.DocProvisioningAppProviderID,
[]mdm_types.Param{}, []mdm_types.Characteristic{
newCharacteristic("Poll", []mdm_types.Param{
newParm("NumberOfFirstRetries", mdm.DmClientCSPNumberOfFirstRetries, mdm.DmClientIntType),
newParm("IntervalForFirstSetOfRetries", mdm.DmClientCSPIntervalForFirstSetOfRetries, mdm.DmClientIntType),
newParm("NumberOfSecondRetries", mdm.DmClientCSPNumberOfSecondRetries, mdm.DmClientIntType),
newParm("IntervalForSecondSetOfRetries", mdm.DmClientCSPIntervalForSecondSetOfRetries, mdm.DmClientIntType),
newParm("NumberOfRemainingScheduledRetries", mdm.DmClientCSPNumberOfRemainingScheduledRetries, mdm.DmClientIntType),
newParm("IntervalForRemainingScheduledRetries", mdm.DmClientCSPIntervalForRemainingScheduledRetries, mdm.DmClientIntType),
newParm("PollOnLogin", mdm.DmClientCSPPollOnLogin, mdm.DmClientBoolType),
newParm("AllUsersPollOnFirstLogin", mdm.DmClientCSPPollOnLogin, mdm.DmClientBoolType),
newParm("NumberOfFirstRetries", syncml.DmClientCSPNumberOfFirstRetries, syncml.DmClientIntType),
newParm("IntervalForFirstSetOfRetries", syncml.DmClientCSPIntervalForFirstSetOfRetries, syncml.DmClientIntType),
newParm("NumberOfSecondRetries", syncml.DmClientCSPNumberOfSecondRetries, syncml.DmClientIntType),
newParm("IntervalForSecondSetOfRetries", syncml.DmClientCSPIntervalForSecondSetOfRetries, syncml.DmClientIntType),
newParm("NumberOfRemainingScheduledRetries", syncml.DmClientCSPNumberOfRemainingScheduledRetries, syncml.DmClientIntType),
newParm("IntervalForRemainingScheduledRetries", syncml.DmClientCSPIntervalForRemainingScheduledRetries, syncml.DmClientIntType),
newParm("PollOnLogin", syncml.DmClientCSPPollOnLogin, syncml.DmClientBoolType),
newParm("AllUsersPollOnFirstLogin", syncml.DmClientCSPPollOnLogin, syncml.DmClientBoolType),
}, nil),
}),
}),
@ -698,7 +699,7 @@ func NewDMClientProvisioningData() mdm_types.Characteristic {
// NewProvisioningDoc returns a new ProvisioningDoc container
func NewProvisioningDoc(certStoreData mdm_types.Characteristic, applicationData mdm_types.Characteristic, dmClientData mdm_types.Characteristic) mdm_types.WapProvisioningDoc {
return mdm_types.WapProvisioningDoc{
Version: mdm.DocProvisioningVersion,
Version: syncml.DocProvisioningVersion,
Characteristics: []mdm_types.Characteristic{
certStoreData,
applicationData,
@ -714,21 +715,21 @@ func mdmMicrosoftDiscoveryEndpoint(ctx context.Context, request interface{}, svc
// Checking first if Discovery message is valid and returning error if this is not the case
if err := req.IsValidDiscoveryMsg(); err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Getting the DiscoveryResponse message
discoveryResponseMsg, err := svc.GetMDMMicrosoftDiscoveryResponse(ctx, req.Body.Discover.Request.EmailAddress)
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Embedding the DiscoveryResponse message inside of a SoapResponse
response, err := NewSoapResponse(discoveryResponseMsg, req.GetMessageID())
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEDiscovery, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
@ -743,12 +744,12 @@ func mdmMicrosoftAuthEndpoint(ctx context.Context, request interface{}, svc flee
params := request.(*SoapRequestContainer).Params
// Sanity check on the expected query params
if !params.Has(mdm.STSAuthAppRu) || !params.Has(mdm.STSLoginHint) {
if !params.Has(syncml.STSAuthAppRu) || !params.Has(syncml.STSLoginHint) {
return getSTSAuthContent(""), errors.New("expected STS params are not present")
}
appru := params.Get(mdm.STSAuthAppRu)
loginHint := params.Get(mdm.STSLoginHint)
appru := params.Get(syncml.STSAuthAppRu)
loginHint := params.Get(syncml.STSLoginHint)
if (len(appru) == 0) || (len(loginHint) == 0) {
return getSTSAuthContent(""), errors.New("expected STS params are empty")
@ -770,28 +771,28 @@ func mdmMicrosoftPolicyEndpoint(ctx context.Context, request interface{}, svc fl
// Checking first if GetPolicies message is valid and returning error if this is not the case
if err := req.IsValidGetPolicyMsg(); err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEPolicy, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Binary security token should be extracted to ensure this is a valid call
hdrSecToken, err := req.GetHeaderBinarySecurityToken()
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEPolicy, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Getting the GetPoliciesResponse message
policyResponseMsg, err := svc.GetMDMWindowsPolicyResponse(ctx, hdrSecToken)
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEPolicy, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Embedding the DiscoveryResponse message inside of a SoapResponse
response, err := NewSoapResponse(policyResponseMsg, req.GetMessageID())
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEPolicy, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEPolicy, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
@ -808,35 +809,35 @@ func mdmMicrosoftEnrollEndpoint(ctx context.Context, request interface{}, svc fl
// Checking first if RequestSecurityToken message is valid and returning error if this is not the case
if err := req.IsValidRequestSecurityTokenMsg(); err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Getting the RequestSecurityToken message from the SOAP request
reqSecurityTokenMsg, err := req.GetRequestSecurityTokenMessage()
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Binary security token should be extracted to ensure this is a valid call
hdrBinarySecToken, err := req.GetHeaderBinarySecurityToken()
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Getting the RequestSecurityTokenResponseCollection message
enrollResponseMsg, err := svc.GetMDMWindowsEnrollResponse(ctx, reqSecurityTokenMsg, hdrBinarySecToken)
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
// Embedding the DiscoveryResponse message inside of a SoapResponse
response, err := NewSoapResponse(enrollResponseMsg, req.GetMessageID())
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
return getSoapResponseFault(req.GetMessageID(), soapFault), nil
}
@ -857,14 +858,14 @@ func mdmMicrosoftManagementEndpoint(ctx context.Context, request interface{}, sv
// Checking first if incoming SyncML message is valid and returning error if this is not the case
if err := reqSyncML.IsValidMsg(); err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MSMDM, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MSMDM, err)
return getSoapResponseFault(reqSyncML.SyncHdr.MsgID, soapFault), nil
}
// Getting the MS-MDM response message
resSyncML, err := svc.GetMDMWindowsManagementResponse(ctx, reqSyncML, reqCerts)
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MSMDM, err)
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MSMDM, err)
return getSoapResponseFault(reqSyncML.SyncHdr.MsgID, soapFault), nil
}
@ -879,19 +880,19 @@ func mdmMicrosoftTOSEndpoint(ctx context.Context, request interface{}, svc fleet
params := request.(*MDMWebContainer).Params
// Sanity check on the expected query params
if !params.Has(mdm.TOCRedirectURI) || !params.Has(mdm.TOCReqID) {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, errors.New("invalid params"))
return getSoapResponseFault(mdm.SoapErrorInternalServiceFault, soapFault), nil
if !params.Has(syncml.TOCRedirectURI) || !params.Has(syncml.TOCReqID) {
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEEnrollment, errors.New("invalid params"))
return getSoapResponseFault(syncml.SoapErrorInternalServiceFault, soapFault), nil
}
redirectURI := params.Get(mdm.TOCRedirectURI)
reqID := params.Get(mdm.TOCReqID)
redirectURI := params.Get(syncml.TOCRedirectURI)
reqID := params.Get(syncml.TOCReqID)
// Getting the TOS content message
resTOCData, err := svc.GetMDMWindowsTOSContent(ctx, redirectURI, reqID)
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
return getSoapResponseFault(mdm.SoapErrorInternalServiceFault, soapFault), nil
soapFault := svc.GetAuthorizedSoapFault(ctx, syncml.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
return getSoapResponseFault(syncml.SoapErrorInternalServiceFault, soapFault), nil
}
return MDMWebContainer{
@ -961,7 +962,7 @@ func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *flee
if authToken.IsAzureJWTToken() {
// Validate the JWT Auth token by retreving its claims
tokenData, err := mdm.GetAzureAuthTokenClaims(authToken.Content)
tokenData, err := microsoft_mdm.GetAzureAuthTokenClaims(authToken.Content)
if err != nil {
return "", "", fmt.Errorf("binary security token claim failed: %v", err)
}
@ -985,17 +986,17 @@ func (svc *Service) GetMDMMicrosoftDiscoveryResponse(ctx context.Context, upnEma
}
// Getting the DiscoveryResponse message content
urlPolicyEndpoint, err := mdm.ResolveWindowsMDMPolicy(appCfg.ServerSettings.ServerURL)
urlPolicyEndpoint, err := microsoft_mdm.ResolveWindowsMDMPolicy(appCfg.ServerSettings.ServerURL)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "resolve policy endpoint")
}
urlEnrollEndpoint, err := mdm.ResolveWindowsMDMEnroll(appCfg.ServerSettings.ServerURL)
urlEnrollEndpoint, err := microsoft_mdm.ResolveWindowsMDMEnroll(appCfg.ServerSettings.ServerURL)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "resolve enroll endpoint")
}
discoveryMsg, err := NewDiscoverResponse(mdm.AuthOnPremise, urlPolicyEndpoint, urlEnrollEndpoint)
discoveryMsg, err := NewDiscoverResponse(syncml.AuthOnPremise, urlPolicyEndpoint, urlEnrollEndpoint)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creation of DiscoverResponse message")
}
@ -1073,7 +1074,7 @@ func (svc *Service) GetMDMWindowsPolicyResponse(ctx context.Context, authToken *
svc.authz.SkipAuthorization(ctx)
// Getting the GetPoliciesResponse message content
policyMsg, err := NewGetPoliciesResponse(mdm.PolicyMinKeyLength, mdm.PolicyCertValidityPeriodInSecs, mdm.PolicyCertRenewalPeriodInSecs)
policyMsg, err := NewGetPoliciesResponse(syncml.PolicyMinKeyLength, syncml.PolicyCertValidityPeriodInSecs, syncml.PolicyCertRenewalPeriodInSecs)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creation of GetPoliciesResponse message")
}
@ -1285,7 +1286,7 @@ func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID st
<CmdID>` + addCommandUUID + `</CmdID>
<Item>
<Target>
<LocURI>` + mdm.FleetdWindowsInstallerGUID + `</LocURI>
<LocURI>` + syncml.FleetdWindowsInstallerGUID + `</LocURI>
</Target>
</Item>
</Add>`)
@ -1298,7 +1299,7 @@ func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID st
<CmdID>` + execCommandUUID + `</CmdID>
<Item>
<Target>
<LocURI>` + mdm.FleetdWindowsInstallerGUID + `</LocURI>
<LocURI>` + syncml.FleetdWindowsInstallerGUID + `</LocURI>
</Target>
<Data>
<MsiInstallJob id="{A427C0AA-E2D5-40DF-ACE8-0D726A6BE096}">
@ -1332,7 +1333,7 @@ func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID st
addFleetdCmd := &fleet.MDMWindowsCommand{
CommandUUID: addCommandUUID,
RawCommand: rawAddCmd,
TargetLocURI: mdm.FleetdWindowsInstallerGUID,
TargetLocURI: syncml.FleetdWindowsInstallerGUID,
}
if err := svc.ds.MDMWindowsInsertCommandForHosts(ctx, []string{deviceID}, addFleetdCmd); err != nil {
return ctxerr.Wrap(ctx, err, "insert add command to install fleetd")
@ -1341,7 +1342,7 @@ func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID st
execFleetCmd := &fleet.MDMWindowsCommand{
CommandUUID: execCommandUUID,
RawCommand: rawExecCmd,
TargetLocURI: mdm.FleetdWindowsInstallerGUID,
TargetLocURI: syncml.FleetdWindowsInstallerGUID,
}
if err := svc.ds.MDMWindowsInsertCommandForHosts(ctx, []string{deviceID}, execFleetCmd); err != nil {
return ctxerr.Wrap(ctx, err, "insert exec command to install fleetd")
@ -1380,7 +1381,7 @@ func (svc *Service) processGenericAlert(ctx context.Context, messageID string, d
}
// Checking if user-initiated unenrollment request is present
if *item.Meta.Type.Content == mdm.AlertUserUnenrollmentRequest {
if *item.Meta.Type.Content == syncml.AlertUserUnenrollmentRequest {
// Deleting the device from the list of enrolled device
err := svc.ds.MDMWindowsDeleteEnrolledDeviceWithDeviceID(ctx, deviceID)
@ -1405,11 +1406,11 @@ func (svc *Service) processIncomingAlertsCommands(ctx context.Context, messageID
alertID := *cmd.Cmd.Data
switch alertID {
case mdm.CmdAlertClientInitiatedManagement:
case syncml.CmdAlertClientInitiatedManagement:
return svc.processNewSessionAlert(ctx, messageID, deviceID, cmd)
case mdm.CmdAlertServerInitiatedManagement:
case syncml.CmdAlertServerInitiatedManagement:
return svc.processNewSessionAlert(ctx, messageID, deviceID, cmd)
case mdm.CmdAlertGeneric:
case syncml.CmdAlertGeneric:
return svc.processGenericAlert(ctx, messageID, deviceID, cmd)
}
@ -1430,7 +1431,7 @@ func (svc *Service) processIncomingMDMCmds(ctx context.Context, deviceID string,
// Acknowledge the message header
// msgref is always 0 for the header
if err = reqMsg.IsValidHeader(); err == nil {
ackMsg := NewSyncMLCmdStatus(reqMessageID, "0", mdm.SyncMLHdrName, mdm.CmdStatusOK)
ackMsg := NewSyncMLCmdStatus(reqMessageID, "0", syncml.SyncMLHdrName, syncml.CmdStatusOK)
responseCmds = append(responseCmds, ackMsg)
}
@ -1453,7 +1454,7 @@ func (svc *Service) processIncomingMDMCmds(ctx context.Context, deviceID string,
}
// CmdStatusOK is returned for the rest of the operations
responseCmds = append(responseCmds, NewSyncMLCmdStatus(reqMessageID, protoCMD.Cmd.CmdID, protoCMD.Verb, mdm.CmdStatusOK))
responseCmds = append(responseCmds, NewSyncMLCmdStatus(reqMessageID, protoCMD.Cmd.CmdID, protoCMD.Verb, syncml.CmdStatusOK))
}
return responseCmds, nil
@ -1506,7 +1507,7 @@ func (svc *Service) createResponseSyncML(ctx context.Context, req *fleet.SyncML,
return nil, fmt.Errorf("appconfig was not available %w", err)
}
urlManagementEndpoint, err := mdm.ResolveWindowsMDMManagement(appConfig.ServerSettings.ServerURL)
urlManagementEndpoint, err := microsoft_mdm.ResolveWindowsMDMManagement(appConfig.ServerSettings.ServerURL)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "resolve management endpoint")
}
@ -1560,7 +1561,7 @@ func (svc *Service) getManagementResponse(ctx context.Context, reqMsg *fleet.Syn
// HW DeviceID is used to check the list of enrolled devices
func (svc *Service) removeWindowsDeviceIfAlreadyMDMEnrolled(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken) error {
// Getting the HW DeviceID from the RequestSecurityToken msg
reqHWDeviceID, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemHWDevID)
reqHWDeviceID, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemHWDevID)
if err != nil {
return err
}
@ -1584,13 +1585,13 @@ func (svc *Service) removeWindowsDeviceIfAlreadyMDMEnrolled(ctx context.Context,
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/35e1aca6-1b8a-48ba-bbc0-23af5d46907a
func (svc *Service) getDeviceProvisioningInformation(ctx context.Context, secTokenMsg *fleet.RequestSecurityToken) (string, error) {
// Getting the HW DeviceID from the RequestSecurityToken msg
reqHWDeviceID, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemHWDevID)
reqHWDeviceID, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemHWDevID)
if err != nil {
return "", err
}
// Getting the EnrollmentType information from the RequestSecurityToken msg
reqEnrollType, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemEnrollmentType)
reqEnrollType, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemEnrollmentType)
if err != nil {
return "", err
}
@ -1608,7 +1609,7 @@ func (svc *Service) getDeviceProvisioningInformation(ctx context.Context, secTok
}
// Getting the client CSR request from the device
clientCSR, err := mdm.GetClientCSR(binSecurityTokenData, binSecurityTokenType)
clientCSR, err := microsoft_mdm.GetClientCSR(binSecurityTokenData, binSecurityTokenType)
if err != nil {
return "", err
}
@ -1634,7 +1635,7 @@ func (svc *Service) getDeviceProvisioningInformation(ctx context.Context, secTok
}
// Getting the MS-MDM management URL to provision the device
urlManagementEndpoint, err := mdm.ResolveWindowsMDMManagement(appCfg.ServerSettings.ServerURL)
urlManagementEndpoint, err := microsoft_mdm.ResolveWindowsMDMManagement(appCfg.ServerSettings.ServerURL)
if err != nil {
return "", err
}
@ -1662,43 +1663,43 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, userID st
)
// Getting the DeviceID context information from the RequestSecurityToken msg
reqDeviceID, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemDeviceID)
reqDeviceID, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemDeviceID)
if err != nil {
return fmt.Errorf("%s %v", error_tag, err)
}
// Getting the HWDevID context information from the RequestSecurityToken msg
reqHWDevID, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemHWDevID)
reqHWDevID, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemHWDevID)
if err != nil {
return fmt.Errorf("%s %v", error_tag, err)
}
// Getting the Enroll DeviceType context information from the RequestSecurityToken msg
reqDeviceType, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemDeviceType)
reqDeviceType, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemDeviceType)
if err != nil {
return fmt.Errorf("%s %v", error_tag, err)
}
// Getting the Enroll DeviceName context information from the RequestSecurityToken msg
reqDeviceName, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemDeviceName)
reqDeviceName, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemDeviceName)
if err != nil {
return fmt.Errorf("%s %v", error_tag, err)
}
// Getting the Enroll RequestVersion context information from the RequestSecurityToken msg
reqEnrollVersion, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemRequestVersion)
reqEnrollVersion, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemRequestVersion)
if err != nil {
reqEnrollVersion = "request_version_not_present"
}
// Getting the RequestVersion context information from the RequestSecurityToken msg
reqAppVersion, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemApplicationVersion)
reqAppVersion, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemApplicationVersion)
if err != nil {
return fmt.Errorf("%s %v", error_tag, err)
}
// Getting the EnrollmentType information from the RequestSecurityToken msg
reqEnrollType, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemEnrollmentType)
reqEnrollType, err := GetContextItem(secTokenMsg, syncml.ReqSecTokenContextItemEnrollmentType)
if err != nil {
return fmt.Errorf("%s %v", error_tag, err)
}
@ -1707,7 +1708,7 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, userID st
enrolledDevice := &fleet.MDMWindowsEnrolledDevice{
MDMDeviceID: reqDeviceID,
MDMHardwareID: reqHWDevID,
MDMDeviceState: mdm.MDMDeviceStateEnrolled,
MDMDeviceState: microsoft_mdm.MDMDeviceStateEnrolled,
MDMDeviceType: reqDeviceType,
MDMDeviceName: reqDeviceName,
MDMEnrollType: reqEnrollType,
@ -1805,10 +1806,10 @@ func createSyncMLMessage(sessionID string, msgID string, deviceID string, source
// setting up things on the SyncML message
var msg mdm_types.SyncML
msg.Xmlns = mdm.SyncCmdNamespace
msg.Xmlns = syncml.SyncCmdNamespace
msg.SyncHdr = mdm_types.SyncHdr{
VerDTD: mdm.SyncMLSupportedVersion,
VerProto: mdm.SyncMLVerProto,
VerDTD: syncml.SyncMLSupportedVersion,
VerProto: syncml.SyncMLVerProto,
SessionID: sessionID,
MsgID: msgID,
Target: &mdm_types.LocURI{LocURI: &deviceID},
@ -2069,6 +2070,14 @@ func (svc *Service) GetMDMWindowsProfilesSummary(ctx context.Context, teamID *ui
}
func ReconcileWindowsProfiles(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger) error {
appConfig, err := ds.AppConfig(ctx)
if err != nil {
return fmt.Errorf("reading app config: %w", err)
}
if !appConfig.MDM.WindowsEnabledAndConfigured {
return nil
}
// retrieve the profiles to install/remove.
toInstall, err := ds.ListMDMWindowsProfilesToInstall(ctx)
if err != nil {

View file

@ -9,8 +9,8 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
mdm_types "github.com/fleetdm/fleet/v4/server/fleet"
mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/stretchr/testify/require"
)
@ -34,7 +34,7 @@ func NewSoapRequest(request []byte) (fleet.SoapRequest, error) {
func TestValidSoapResponse(t *testing.T) {
relatesTo := "urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749"
soapFaultMsg := NewSoapFault(mdm.SoapErrorAuthentication, mdm_types.MDEDiscovery, errors.New("test"))
soapFaultMsg := NewSoapFault(syncml.SoapErrorAuthentication, mdm_types.MDEDiscovery, errors.New("test"))
sres, err := NewSoapResponse(&soapFaultMsg, relatesTo)
require.NoError(t, err)
outXML, err := xml.MarshalIndent(sres, "", " ")
@ -51,7 +51,7 @@ func TestInvalidSoapResponse(t *testing.T) {
func TestFaultMessageSoapResponse(t *testing.T) {
targetErrorString := "invalid input request"
soapFaultMsg := NewSoapFault(mdm.SoapErrorAuthentication, mdm_types.MDEDiscovery, errors.New(targetErrorString))
soapFaultMsg := NewSoapFault(syncml.SoapErrorAuthentication, mdm_types.MDEDiscovery, errors.New(targetErrorString))
sres, err := NewSoapResponse(&soapFaultMsg, "urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749")
require.NoError(t, err)
outXML, err := xml.MarshalIndent(sres, "", " ")
@ -394,11 +394,11 @@ func TestBuildCommandFromProfileBytes(t *testing.T) {
func syncMLForTest(locURI string) []byte {
return []byte(fmt.Sprintf(`
<Replace>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
</Item>
</Replace>`, locURI))
<Replace>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
</Item>
</Replace>`, locURI))
}

View file

@ -709,6 +709,9 @@ func (svc *Service) detailQueriesForHost(ctx context.Context, host *fleet.Host)
if query.RunsForPlatform(host.Platform) {
queryName := hostDetailQueryPrefix + name
queries[queryName] = query.Query
if query.QueryFunc != nil && query.Query == "" {
queries[queryName] = query.QueryFunc(ctx, svc.logger, host, svc.ds)
}
discoveryQuery := query.Discovery
if discoveryQuery == "" {
discoveryQuery = alwaysTrueQuery

View file

@ -19,6 +19,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/publicip"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async"
"github.com/go-kit/kit/log"
@ -29,6 +30,8 @@ import (
type DetailQuery struct {
// Query is the SQL query string.
Query string
// QueryFunc is optionally used to dynamically build a query.
QueryFunc func(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore) string
// Discovery is the SQL query that defines whether the query will run on the host or not.
// If not set, Fleet makes sure the query will always run.
Discovery string
@ -586,6 +589,12 @@ var mdmQueries = map[string]DetailQuery{
DirectIngestFunc: directIngestMacOSProfiles,
Discovery: discoveryTable("macos_profiles"),
},
"mdm_config_profiles_windows": {
QueryFunc: buildConfigProfilesWindowsQuery,
Platforms: []string{"windows"},
DirectIngestFunc: directIngestWindowsProfiles,
Discovery: discoveryTable("mdm_bridge"),
},
// There are two mutually-exclusive queries used to read the FileVaultPRK depending on which
// extension tables are discovered on the agent. The preferred query uses the newer custom
// `filevault_prk` extension table rather than the macadmins `file_lines` table. It is preferred
@ -1677,7 +1686,7 @@ func GetDetailQueries(
generatedMap["scheduled_query_stats"] = scheduledQueryStats
}
if appConfig != nil && appConfig.MDM.EnabledAndConfigured {
if appConfig != nil && (appConfig.MDM.EnabledAndConfigured || appConfig.MDM.WindowsEnabledAndConfigured) {
for key, query := range mdmQueries {
if slices.Equal(query.Platforms, []string{"windows"}) && !appConfig.MDM.WindowsEnabledAndConfigured {
continue
@ -1722,3 +1731,69 @@ func splitCleanSemicolonSeparated(s string) []string {
}
return cleaned
}
func buildConfigProfilesWindowsQuery(
ctx context.Context,
logger log.Logger,
host *fleet.Host,
ds fleet.Datastore,
) string {
var sb strings.Builder
sb.WriteString("<SyncBody>")
gotProfiles := false
err := microsoft_mdm.LoopHostMDMLocURIs(ctx, ds, host, func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string) {
// Per the [docs][1], to `<Get>` configurations you must
// replace `/Policy/Config` with `Policy/Result`
// [1]: https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-configuration-service-provider
locURI = strings.Replace(locURI, "/Policy/Config", "/Policy/Result", 1)
sb.WriteString(
// NOTE: intentionally building the xml as a one-liner
// to prevent any errors in the query.
fmt.Sprintf(
"<Get><CmdID>%s</CmdID><Item><Target><LocURI>%s</LocURI></Target></Item></Get>",
hash,
locURI,
))
gotProfiles = true
})
if err != nil {
logger.Log(
"component", "service",
"method", "QueryFunc - windows config profiles",
"err", err,
)
return ""
}
if !gotProfiles {
logger.Log(
"component", "service",
"method", "QueryFunc - windows config profiles",
"info", "host doesn't have profiles to check",
)
return ""
}
sb.WriteString("</SyncBody>")
return fmt.Sprintf("SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input = '%s';", sb.String())
}
func directIngestWindowsProfiles(
ctx context.Context,
logger log.Logger,
host *fleet.Host,
ds fleet.Datastore,
rows []map[string]string,
) error {
if len(rows) == 0 {
return nil
}
if len(rows) > 1 {
return ctxerr.Errorf(ctx, "directIngestWindowsProfiles invalid number of rows: %d", len(rows))
}
rawResponse := []byte(rows[0]["raw_mdm_command_output"])
if len(rawResponse) == 0 {
return ctxerr.Errorf(ctx, "directIngestWindowsProfiles host %s got an empty SyncML response", host.UUID)
}
return microsoft_mdm.VerifyHostMDMProfiles(ctx, ds, host, rawResponse)
}

View file

@ -7,8 +7,10 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"regexp"
"slices"
"sort"
"strings"
@ -19,6 +21,7 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async"
@ -1292,8 +1295,8 @@ func TestDirectIngestHostMacOSProfiles(t *testing.T) {
}
return expected, nil
}
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, hostUUID string, toVerify, toFailed, toRetry []string) error {
require.Equal(t, h.UUID, hostUUID)
ds.UpdateHostMDMProfilesVerificationFunc = func(ctx context.Context, host *fleet.Host, toVerify, toFailed, toRetry []string) error {
require.Equal(t, h.UUID, host.UUID)
require.Equal(t, len(installedProfiles), len(toVerify))
require.Len(t, toFailed, 0)
require.Len(t, toRetry, 0)
@ -1537,3 +1540,66 @@ func TestSanitizeSoftware(t *testing.T) {
})
}
}
func TestDirectIngestWindowsProfiles(t *testing.T) {
ctx := context.Background()
logger := log.NewNopLogger()
ds := new(mock.Store)
for _, tc := range []struct {
hostProfiles []*fleet.ExpectedMDMProfile
want string
}{
{nil, ""},
{
[]*fleet.ExpectedMDMProfile{
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{})},
},
"",
},
{
[]*fleet.ExpectedMDMProfile{
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{"L1": "D1"})},
},
"SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input = '<SyncBody><Get><CmdID>1255198959</CmdID><Item><Target><LocURI>L1</LocURI></Target></Item></Get></SyncBody>';",
},
{
[]*fleet.ExpectedMDMProfile{
{Name: "N1", RawProfile: syncml.ForTestWithData(map[string]string{"L1": "D1"})},
{Name: "N2", RawProfile: syncml.ForTestWithData(map[string]string{"L2": "D2"})},
{Name: "N3", RawProfile: syncml.ForTestWithData(map[string]string{"L3": "D3", "L3.1": "D3.1"})},
},
"SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input = '<SyncBody><Get><CmdID>1255198959</CmdID><Item><Target><LocURI>L1</LocURI></Target></Item></Get><Get><CmdID>2736786183</CmdID><Item><Target><LocURI>L2</LocURI></Target></Item></Get><Get><CmdID>894211447</CmdID><Item><Target><LocURI>L3</LocURI></Target></Item></Get><Get><CmdID>3410477854</CmdID><Item><Target><LocURI>L3.1</LocURI></Target></Item></Get></SyncBody>';",
},
} {
ds.GetHostMDMProfilesExpectedForVerificationFunc = func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error) {
result := map[string]*fleet.ExpectedMDMProfile{}
for _, p := range tc.hostProfiles {
result[p.Name] = p
}
return result, nil
}
gotQuery := buildConfigProfilesWindowsQuery(ctx, logger, &fleet.Host{}, ds)
if tc.want != "" {
require.Contains(t, gotQuery, "SELECT raw_mdm_command_output FROM mdm_bridge WHERE mdm_command_input =")
re := regexp.MustCompile(`'<(.*?)>'`)
gotMatches := re.FindStringSubmatch(gotQuery)
require.NotEmpty(t, gotMatches)
wantMatches := re.FindStringSubmatch(tc.want)
require.NotEmpty(t, wantMatches)
var extractedStruct, expectedStruct fleet.SyncBody
err := xml.Unmarshal([]byte(gotMatches[0]), &extractedStruct)
require.NoError(t, err)
err = xml.Unmarshal([]byte(wantMatches[0]), &expectedStruct)
require.NoError(t, err)
require.ElementsMatch(t, expectedStruct.Get, extractedStruct.Get)
} else {
require.Equal(t, gotQuery, tc.want)
}
}
}