mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
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:
parent
120293e59c
commit
b07fbdc1d7
32 changed files with 1911 additions and 842 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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(), ","))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
162
server/mdm/microsoft/profile_verifier.go
Normal file
162
server/mdm/microsoft/profile_verifier.go
Normal 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)
|
||||
}
|
||||
309
server/mdm/microsoft/profile_verifier_test.go
Normal file
309
server/mdm/microsoft/profile_verifier_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
361
server/mdm/microsoft/syncml/syncml.go
Normal file
361
server/mdm/microsoft/syncml/syncml.go
Normal 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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue