Modify Windows replacement code to allow Custom SCEP variables (#34633)
and refactor to share with apple mdm <!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34246 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`).
This commit is contained in:
@@ -2337,3 +2337,36 @@ func (ds *Datastore) WipeHostViaWindowsMDM(ctx context.Context, host *fleet.Host
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) UpdateOrDeleteHostMDMWindowsProfile(ctx context.Context, profile *fleet.HostMDMWindowsProfile) error {
|
||||
// Delete the host profile if it's remove and verified/verifying.
|
||||
if profile.OperationType == fleet.MDMOperationTypeRemove && profile.Status != nil &&
|
||||
(*profile.Status == fleet.MDMDeliveryVerifying || *profile.Status == fleet.MDMDeliveryVerified) {
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, `
|
||||
DELETE FROM host_mdm_windows_profiles
|
||||
WHERE host_uuid = ? AND command_uuid = ?
|
||||
`, profile.HostUUID, profile.CommandUUID)
|
||||
return err
|
||||
}
|
||||
|
||||
detail := profile.Detail
|
||||
|
||||
if profile.OperationType == fleet.MDMOperationTypeRemove && profile.Status != nil && *profile.Status == fleet.MDMDeliveryFailed {
|
||||
detail = fmt.Sprintf("Failed to remove: %s", detail)
|
||||
}
|
||||
|
||||
status := profile.Status
|
||||
// We need to run with retry due to potential deadlocks with BulkSetPendingMDMHostProfiles.
|
||||
// Deadlock seen in 2024/12/12 loadtest: https://docs.google.com/document/d/1-Q6qFTd7CDm-lh7MVRgpNlNNJijk6JZ4KO49R1fp80U
|
||||
|
||||
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
UPDATE host_mdm_windows_profiles
|
||||
SET status = ?, operation_type = ?, detail = ?
|
||||
WHERE host_uuid = ? AND command_uuid = ?
|
||||
`, status, profile.OperationType, detail, profile.HostUUID, profile.CommandUUID)
|
||||
|
||||
return err
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20251022123456, Down_20251022123456)
|
||||
}
|
||||
|
||||
func Up_20251022123456(tx *sql.Tx) error {
|
||||
insStmt := `
|
||||
INSERT INTO fleet_variables (
|
||||
name, is_prefix, created_at
|
||||
) VALUES
|
||||
('FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID', 0, :created_at)
|
||||
`
|
||||
// use a constant time so that the generated schema is deterministic
|
||||
createdAt := time.Date(2025, 10, 22, 0, 0, 0, 0, time.UTC)
|
||||
stmt, args, err := sqlx.Named(insStmt, map[string]any{"created_at": createdAt})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to prepare insert for FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID: %s", err)
|
||||
}
|
||||
_, err = tx.Exec(stmt, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID into fleet_variables: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20251022123456(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20251022123456(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// look up table, and see it does not contain FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID
|
||||
var count int
|
||||
err := db.Get(&count, `SELECT COUNT(*) FROM fleet_variables WHERE name = 'FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID'`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// look up table, and see it now contains FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID
|
||||
err = db.Get(&count, `SELECT COUNT(*) FROM fleet_variables WHERE name = 'FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID'`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -2424,6 +2424,8 @@ type Datastore interface {
|
||||
|
||||
// GetCurrentTime gets the current time from the database
|
||||
GetCurrentTime(ctx context.Context) (time.Time, error)
|
||||
|
||||
UpdateOrDeleteHostMDMWindowsProfile(ctx context.Context, profile *HostMDMWindowsProfile) error
|
||||
}
|
||||
|
||||
type AndroidDatastore interface {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
mdm_types "github.com/fleetdm/fleet/v4/server/mdm"
|
||||
@@ -64,11 +65,33 @@ const (
|
||||
FleetVarCustomSCEPProxyURLPrefix FleetVarName = "CUSTOM_SCEP_PROXY_URL_"
|
||||
FleetVarSmallstepSCEPChallengePrefix FleetVarName = "SMALLSTEP_SCEP_CHALLENGE_"
|
||||
FleetVarSmallstepSCEPProxyURLPrefix FleetVarName = "SMALLSTEP_SCEP_PROXY_URL_"
|
||||
FleetVarSCEPWindowsCertificateID FleetVarName = "SCEP_WINDOWS_CERTIFICATE_ID" // nolint:gosec // G101: Potential hardcoded credentials
|
||||
|
||||
// OneTimeChallengeTTL is the time to live for one-time challenges.
|
||||
OneTimeChallengeTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
var (
|
||||
// Fleet variable regexp patterns
|
||||
FleetVarHostEndUserEmailIDPRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserEmailIDP))
|
||||
FleetVarHostHardwareSerialRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostHardwareSerial))
|
||||
FleetVarHostEndUserIDPUsernameRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserIDPUsername))
|
||||
FleetVarHostEndUserIDPDepartmentRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserIDPDepartment))
|
||||
FleetVarHostEndUserIDPUsernameLocalPartRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserIDPUsernameLocalPart))
|
||||
FleetVarHostEndUserIDPGroupsRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserIDPGroups))
|
||||
FleetVarNDESSCEPChallengeRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarNDESSCEPChallenge))
|
||||
FleetVarNDESSCEPProxyURLRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarNDESSCEPProxyURL))
|
||||
FleetVarHostEndUserIDPFullnameRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostEndUserIDPFullname))
|
||||
FleetVarSCEPRenewalIDRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarSCEPRenewalID))
|
||||
FleetVarHostUUIDRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarHostUUID))
|
||||
FleetVarSCEPWindowsCertificateIDRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, FleetVarSCEPWindowsCertificateID))
|
||||
|
||||
// Fleet variable replacement failed errors
|
||||
HostEndUserEmailIDPVariableReplacementFailedError = fmt.Sprintf("There is no IdP email for this host. "+
|
||||
"Fleet couldn't populate $FLEET_VAR_%s. "+
|
||||
"[Learn more](https://fleetdm.com/learn-more-about/idp-email)", FleetVarHostEndUserEmailIDP)
|
||||
)
|
||||
|
||||
type AppleMDM struct {
|
||||
CommonName string `json:"common_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
@@ -1100,3 +1123,17 @@ type MDMCommandResults interface {
|
||||
}
|
||||
|
||||
type MDMCommandResultsHandler func(ctx context.Context, results MDMCommandResults) error
|
||||
|
||||
// Helper function for variable replacement in MDM profiles
|
||||
func GetFirstIDPEmail(ctx context.Context, ds Datastore, hostUUID string) (email *string, err error) {
|
||||
// TODO: Should we check on another type of device mapping? Instead of mdm_idp_accounts source.
|
||||
emails, err := ds.GetHostEmails(ctx, hostUUID, DeviceMappingMDMIdpAccounts)
|
||||
if err != nil {
|
||||
// This is a server error, so we exit.
|
||||
return nil, err
|
||||
}
|
||||
if len(emails) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &emails[0], nil
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/admx"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/wlanxml"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/profiles"
|
||||
"github.com/fleetdm/fleet/v4/server/variables"
|
||||
"github.com/go-kit/log"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
)
|
||||
|
||||
@@ -30,7 +31,8 @@ import (
|
||||
// - The data (if any) of the first <Item> element of the current LocURI
|
||||
func LoopOverExpectedHostProfiles(
|
||||
ctx context.Context,
|
||||
ds fleet.ProfileVerificationStore,
|
||||
logger kitlog.Logger,
|
||||
ds fleet.Datastore,
|
||||
host *fleet.Host,
|
||||
fn func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string),
|
||||
) error {
|
||||
@@ -46,7 +48,7 @@ func LoopOverExpectedHostProfiles(
|
||||
|
||||
// Process Fleet variables if present (similar to how it's done during profile deployment)
|
||||
// This ensures we compare what was actually sent to the device
|
||||
processedContent := PreprocessWindowsProfileContents(host.UUID, expanded)
|
||||
processedContent := PreprocessWindowsProfileContentsForVerification(ctx, logger, ds, host.UUID, expectedProf.ProfileUUID, expanded)
|
||||
expectedProf.RawProfile = []byte(processedContent)
|
||||
|
||||
var prof fleet.SyncMLCmd
|
||||
@@ -85,7 +87,7 @@ func HashLocURI(profileName, locURI string) string {
|
||||
// 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, logger log.Logger, ds fleet.ProfileVerificationStore, host *fleet.Host,
|
||||
func VerifyHostMDMProfiles(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, host *fleet.Host,
|
||||
rawProfileResultsSyncML []byte,
|
||||
) error {
|
||||
profileResults, err := transformProfileResults(rawProfileResultsSyncML)
|
||||
@@ -150,7 +152,7 @@ func splitMissingProfilesIntoFailAndRetryBuckets(ctx context.Context, ds fleet.P
|
||||
return toFail, toRetry, nil
|
||||
}
|
||||
|
||||
func compareResultsToExpectedProfiles(ctx context.Context, logger log.Logger, ds fleet.ProfileVerificationStore, host *fleet.Host,
|
||||
func compareResultsToExpectedProfiles(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, host *fleet.Host,
|
||||
profileResults profileResultsTransform, existingProfiles []fleet.HostMDMWindowsProfile,
|
||||
) (verified map[string]struct{}, missing map[string]struct{}, err error) {
|
||||
missing = map[string]struct{}{}
|
||||
@@ -162,7 +164,7 @@ func compareResultsToExpectedProfiles(ctx context.Context, logger log.Logger, ds
|
||||
windowsProfilesByID[existingProfile.ProfileUUID] = existingProfile
|
||||
}
|
||||
|
||||
err = LoopOverExpectedHostProfiles(ctx, ds, host, func(profile *fleet.ExpectedMDMProfile, ref, locURI, wantData string) {
|
||||
err = LoopOverExpectedHostProfiles(ctx, logger, 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 := profileResults.cmdRefToStatus[ref]
|
||||
if !ok {
|
||||
@@ -278,7 +280,41 @@ func IsWin32OrDesktopBridgeADMXCSP(locURI string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PreprocessWindowsProfileContents processes Windows configuration profiles to replace Fleet variables
|
||||
// PreprocessWindowsProfileContentsForVerification processes Windows configuration profiles to replace Fleet variables
|
||||
// with the given host UUID for verification purposes.
|
||||
//
|
||||
// This function is similar to PreprocessWindowsProfileContentsForDeployment, but it does not require
|
||||
// a datastore or logger since it only replaces certain fleet variables to avoid datastore unnecessary work.
|
||||
func PreprocessWindowsProfileContentsForVerification(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, hostUUID string, profileUUID string, profileContents string) string {
|
||||
replacedContents, _ := preprocessWindowsProfileContents(ctx, logger, ds, nil, true, hostUUID, "", profileUUID, nil, profileContents)
|
||||
// ^ We ignore the error here, and rely on the fact that the function will return the original contents if no replacements were made.
|
||||
// So verification fails on individual profile level, instead of entire verification failing.
|
||||
return replacedContents
|
||||
}
|
||||
|
||||
// PreprocessWindowsProfileContentsForDeployment processes Windows configuration profiles to replace Fleet variables
|
||||
// with their actual values for each host during profile deployment.
|
||||
func PreprocessWindowsProfileContentsForDeployment(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, appConfig *fleet.AppConfig, hostUUID string, hostCmdUUID string, profileUUID string, groupedCAs *fleet.GroupedCertificateAuthorities, profileContents string) (string, error) {
|
||||
// TODO: Should we avoid iterating this list for every profile?
|
||||
customSCEPCAs := make(map[string]*fleet.CustomSCEPProxyCA, len(groupedCAs.CustomScepProxy))
|
||||
for _, ca := range groupedCAs.CustomScepProxy {
|
||||
customSCEPCAs[ca.Name] = &ca
|
||||
}
|
||||
|
||||
return preprocessWindowsProfileContents(ctx, logger, ds, appConfig, false, hostUUID, hostCmdUUID, profileUUID, customSCEPCAs, profileContents)
|
||||
}
|
||||
|
||||
// This error type is used to indicate errors during Microsoft profile processing, such as variable replacement failures.
|
||||
// It should not break the entire deployment flow, but rather be handled gracefully at the profile level, setting it to failed and detail = Error()
|
||||
type MicrosoftProfileProcessingError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *MicrosoftProfileProcessingError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// preprocessWindowsProfileContents processes Windows configuration profiles to replace Fleet variables
|
||||
// with their actual values for each host. This function is used both during profile deployment
|
||||
// and during profile verification to ensure consistency.
|
||||
//
|
||||
@@ -286,6 +322,10 @@ func IsWin32OrDesktopBridgeADMXCSP(locURI string) bool {
|
||||
//
|
||||
// Currently supported variables:
|
||||
// - $FLEET_VAR_HOST_UUID or ${FLEET_VAR_HOST_UUID}: Replaced with the host's UUID
|
||||
// - $FLEET_VAR_HOST_END_USER_EMAIL_IDP or ${FLEET_VAR_HOST_END_USER_EMAIL_IDP}: Replaced with the host's end user email from the IDP
|
||||
// - $FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID or ${FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID}: Replaced with the host command UUID for SCEP certificate
|
||||
// - $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_<CA_NAME> or ${FLEET_VAR_CUSTOM_SCEP_CHALLENGE_<CA_NAME>}: Replaced with the challenge for the specified custom SCEP CA
|
||||
// - $FLEET_VAR_CUSTOM_SCEP_PROXY_URL_<CA_NAME> or ${FLEET_VAR_CUSTOM_SCEP_PROXY_URL_<CA_NAME>}: Replaced with the proxy URL for the specified custom SCEP CA
|
||||
//
|
||||
// Why we don't use Go templates here:
|
||||
// 1. Error handling: Go templates don't provide fine-grained error handling for individual variable
|
||||
@@ -296,29 +336,80 @@ func IsWin32OrDesktopBridgeADMXCSP(locURI string) bool {
|
||||
// thousands of host profiles. Direct string replacement is more efficient for our use case.
|
||||
// 4. XML escaping: We need XML-specific escaping for values, which is simpler to control with direct
|
||||
// string replacement rather than template functions.
|
||||
func PreprocessWindowsProfileContents(hostUUID string, profileContents string) string {
|
||||
func preprocessWindowsProfileContents(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, appConfig *fleet.AppConfig,
|
||||
isVerifying bool, hostUUID string, hostCmdUUID string, profileUUID string,
|
||||
customSCEPCAs map[string]*fleet.CustomSCEPProxyCA, profileContents string,
|
||||
) (string, error) {
|
||||
// Check if Fleet variables are present
|
||||
fleetVars := variables.Find(profileContents)
|
||||
if len(fleetVars) == 0 {
|
||||
// No variables to replace, return original content
|
||||
return profileContents
|
||||
return profileContents, nil
|
||||
}
|
||||
|
||||
// Process each Fleet variable
|
||||
result := profileContents
|
||||
for fleetVar := range fleetVars {
|
||||
if fleetVar == string(fleet.FleetVarHostUUID) {
|
||||
// Replace HOST_UUID with the actual host UUID
|
||||
// Use XML escaping for the replacement value to be safe and prevent XML injection
|
||||
b := make([]byte, 0, len(hostUUID))
|
||||
buf := bytes.NewBuffer(b)
|
||||
_ = xml.EscapeText(buf, []byte(hostUUID))
|
||||
escapedUUID := buf.String()
|
||||
|
||||
result = variables.Replace(result, fleetVar, escapedUUID)
|
||||
result = profiles.ReplaceFleetVariableInXML(fleet.FleetVarHostUUIDRegexp, result, hostUUID)
|
||||
} else if fleetVar == string(fleet.FleetVarHostEndUserEmailIDP) {
|
||||
replacedContents, replacedVariable, err := profiles.ReplaceHostEndUserEmailIDPVariable(ctx, ds, profileContents, hostUUID)
|
||||
if err != nil {
|
||||
return profileContents, ctxerr.Wrap(ctx, err, "replacing host end user email IDP variable")
|
||||
}
|
||||
if !replacedVariable {
|
||||
return profileContents, &MicrosoftProfileProcessingError{message: fleet.HostEndUserEmailIDPVariableReplacementFailedError}
|
||||
}
|
||||
result = replacedContents
|
||||
}
|
||||
// Add other Fleet variables here as they are implemented
|
||||
|
||||
// We skip some variables during verification, to avoid unnecessary datastore calls
|
||||
// or processing that is not needed for verification.
|
||||
if isVerifying {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case fleetVar == string(fleet.FleetVarSCEPWindowsCertificateID):
|
||||
result = profiles.ReplaceFleetVariableInXML(fleet.FleetVarSCEPWindowsCertificateIDRegexp, result, hostCmdUUID)
|
||||
case strings.HasPrefix(fleetVar, string(fleet.FleetVarCustomSCEPChallengePrefix)):
|
||||
caName := strings.TrimPrefix(fleetVar, string(fleet.FleetVarCustomSCEPChallengePrefix))
|
||||
err := profiles.IsCustomSCEPConfigured(ctx, customSCEPCAs, caName, fleetVar, func(errMsg string) error {
|
||||
return &MicrosoftProfileProcessingError{message: errMsg}
|
||||
})
|
||||
if err != nil {
|
||||
return profileContents, err
|
||||
}
|
||||
replacedContents, replacedVariable, err := profiles.ReplaceCustomSCEPChallengeVariable(ctx, logger, fleetVar, customSCEPCAs, result)
|
||||
if err != nil {
|
||||
return profileContents, ctxerr.Wrap(ctx, err, "replacing custom SCEP challenge variable")
|
||||
}
|
||||
if !replacedVariable {
|
||||
return profileContents, &MicrosoftProfileProcessingError{message: fmt.Sprintf("Custom SCEP challenge variable replacement failed for variable %s", fleetVar)}
|
||||
}
|
||||
result = replacedContents
|
||||
case strings.HasPrefix(fleetVar, string(fleet.FleetVarCustomSCEPProxyURLPrefix)):
|
||||
caName := strings.TrimPrefix(fleetVar, string(fleet.FleetVarCustomSCEPProxyURLPrefix))
|
||||
err := profiles.IsCustomSCEPConfigured(ctx, customSCEPCAs, caName, fleetVar, func(errMsg string) error {
|
||||
return &MicrosoftProfileProcessingError{message: errMsg}
|
||||
})
|
||||
if err != nil {
|
||||
return profileContents, err
|
||||
}
|
||||
replacedContents, _, replacedVariable, err := profiles.ReplaceCustomSCEPProxyURLVariable(ctx, logger, ds, appConfig, fleetVar, customSCEPCAs, result, hostUUID, profileUUID)
|
||||
if err != nil {
|
||||
return profileContents, ctxerr.Wrap(ctx, err, "replacing custom SCEP challenge variable")
|
||||
}
|
||||
if !replacedVariable {
|
||||
return profileContents, &MicrosoftProfileProcessingError{message: fmt.Sprintf("Custom SCEP challenge variable replacement failed for variable %s", fleetVar)}
|
||||
}
|
||||
result = replacedContents
|
||||
|
||||
// TODO: Add managed certificate here.
|
||||
}
|
||||
|
||||
// Add other Fleet variables here as they are implemented, identify if it can be skipped for verification.
|
||||
}
|
||||
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/wlanxml"
|
||||
@@ -65,7 +66,7 @@ func TestLoopHostMDMLocURIs(t *testing.T) {
|
||||
uniqueHash string
|
||||
}
|
||||
got := []wantStruct{}
|
||||
err := LoopOverExpectedHostProfiles(ctx, ds, &fleet.Host{}, func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string) {
|
||||
err := LoopOverExpectedHostProfiles(ctx, log.NewNopLogger(), ds, &fleet.Host{}, func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string) {
|
||||
got = append(got, wantStruct{
|
||||
locURI: locURI,
|
||||
data: data,
|
||||
@@ -821,7 +822,16 @@ type hostProfile struct {
|
||||
RetryCount uint
|
||||
}
|
||||
|
||||
func TestPreprocessWindowsProfileContents(t *testing.T) {
|
||||
func TestPreprocessWindowsProfileContentsForVerification(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
|
||||
ds.GetHostEmailsFunc = func(ctx context.Context, hostUUID, source string) ([]string, error) {
|
||||
if source == fleet.DeviceMappingMDMIdpAccounts && strings.Contains(hostUUID, "end-user-email") {
|
||||
return []string{"test@idp.com"}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hostUUID string
|
||||
@@ -882,12 +892,227 @@ func TestPreprocessWindowsProfileContents(t *testing.T) {
|
||||
profileContents: `<Replace><Data>ID1: $FLEET_VAR_HOST_UUID, ID2: ${FLEET_VAR_HOST_UUID}</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>ID1: test-host-1234-uuid, ID2: test-host-1234-uuid</Data></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "fleet variable with db access",
|
||||
hostUUID: "test-host-end-user-email",
|
||||
profileContents: `<Replace><Data>ID: $FLEET_VAR_HOST_UUID, Other: $FLEET_VAR_HOST_END_USER_EMAIL_IDP</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>ID: test-host-end-user-email, Other: test@idp.com</Data></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "skips scep windows id var",
|
||||
hostUUID: "test-host-1234-uuid",
|
||||
profileContents: `<Replace><Data>ID: $FLEET_VAR_HOST_UUID, SCEP: $FLEET_VAR_HOST_SCEP_WINDOWS_ID</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>ID: test-host-1234-uuid, SCEP: $FLEET_VAR_HOST_SCEP_WINDOWS_ID</Data></Replace>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := PreprocessWindowsProfileContents(tt.hostUUID, tt.profileContents)
|
||||
result := PreprocessWindowsProfileContentsForVerification(t.Context(), log.NewNopLogger(), ds, tt.hostUUID, uuid.NewString(), tt.profileContents)
|
||||
require.Equal(t, tt.expectedContents, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
|
||||
baseSetup := func() {
|
||||
ds.GetHostEmailsFunc = func(ctx context.Context, hostUUID, source string) ([]string, error) {
|
||||
if source == fleet.DeviceMappingMDMIdpAccounts && strings.Contains(hostUUID, "end-user-email") {
|
||||
return []string{"test@idp.com"}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
|
||||
if ds.GetAllCertificateAuthoritiesFunc == nil {
|
||||
return &fleet.GroupedCertificateAuthorities{
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
cas, err := ds.GetAllCertificateAuthoritiesFunc(ctx, includeSecrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fleet.GroupCertificateAuthoritiesByType(cas)
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
ServerSettings: fleet.ServerSettings{
|
||||
ServerURL: "https://test-fleet.com",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// use the same uuid for all profile UUID actions
|
||||
profileUUID := uuid.NewString()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hostUUID string
|
||||
hostCmdUUID string
|
||||
profileContents string
|
||||
expectedContents string
|
||||
expectError bool
|
||||
processingError string // if set then we expect the error to be of type MicrosoftProfileProcessingError with this message
|
||||
setup func() // Used for setting up datastore mocks.
|
||||
freeTier bool
|
||||
}{
|
||||
{
|
||||
name: "no fleet variables",
|
||||
hostUUID: "test-uuid-123",
|
||||
profileContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>Simple Value</Data></Item></Replace>`,
|
||||
expectedContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>Simple Value</Data></Item></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "host uuid fleet variable",
|
||||
hostUUID: "test-uuid-456",
|
||||
profileContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>Device ID: $FLEET_VAR_HOST_UUID</Data></Item></Replace>`,
|
||||
expectedContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>Device ID: test-uuid-456</Data></Item></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "host end user email idp",
|
||||
hostUUID: "test-uuid-end-user-email",
|
||||
profileContents: `<Replace><Data>Email: $FLEET_VAR_HOST_END_USER_EMAIL_IDP</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>Email: test@idp.com</Data></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "no host end user email idp found",
|
||||
hostUUID: "test-uuid-no-end-user",
|
||||
profileContents: `<Replace><Data>Email: $FLEET_VAR_HOST_END_USER_EMAIL_IDP</Data></Replace>`,
|
||||
expectError: true,
|
||||
processingError: fleet.HostEndUserEmailIDPVariableReplacementFailedError,
|
||||
},
|
||||
{
|
||||
name: "scep windows certificate id",
|
||||
hostUUID: "test-host-1234-uuid",
|
||||
hostCmdUUID: "cmd-uuid-5678",
|
||||
profileContents: `<Replace><Data>SCEP: $FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>SCEP: cmd-uuid-5678</Data></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "custom scep proxy url not usable in free tier",
|
||||
hostUUID: "test-host-1234-uuid",
|
||||
hostCmdUUID: "cmd-uuid-5678",
|
||||
profileContents: `<Replace><Data>CA: $FLEET_VAR_CUSTOM_SCEP_PROXY_URL_CERTIFICATE</Data></Replace>`,
|
||||
expectError: true,
|
||||
processingError: "Custom SCEP integration requires a Fleet Premium license.",
|
||||
freeTier: true,
|
||||
},
|
||||
{
|
||||
name: "custom scep proxy url ca not found",
|
||||
hostUUID: "test-host-1234-uuid",
|
||||
hostCmdUUID: "cmd-uuid-5678",
|
||||
profileContents: `<Replace><Data>CA: $FLEET_VAR_CUSTOM_SCEP_PROXY_URL_CERTIFICATE</Data></Replace>`,
|
||||
expectError: true,
|
||||
processingError: "Fleet couldn't populate $CUSTOM_SCEP_PROXY_URL_CERTIFICATE because CERTIFICATE certificate authority doesn't exist.",
|
||||
},
|
||||
{
|
||||
name: "custom scep proxy url ca found and replaced",
|
||||
hostUUID: "test-host-1234-uuid",
|
||||
hostCmdUUID: "cmd-uuid-5678",
|
||||
profileContents: `<Replace><Data> $FLEET_VAR_CUSTOM_SCEP_PROXY_URL_CERTIFICATE</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>https://test-fleet.com/mdm/scep/proxy/test-host-1234-uuid%2C` + profileUUID + `%2CCERTIFICATE%2Csupersecret</Data></Replace>`,
|
||||
setup: func() {
|
||||
ds.GetAllCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) ([]*fleet.CertificateAuthority, error) {
|
||||
return []*fleet.CertificateAuthority{
|
||||
{
|
||||
ID: 1,
|
||||
Name: ptr.String("CERTIFICATE"),
|
||||
Type: string(fleet.CATypeCustomSCEPProxy),
|
||||
URL: ptr.String("https://scep.proxy.url/scep"),
|
||||
Challenge: ptr.String("supersecret"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
ds.NewChallengeFunc = func(ctx context.Context) (string, error) {
|
||||
return "supersecret", nil
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom scep challenge not usable in free tier",
|
||||
hostUUID: "test-host-1234-uuid",
|
||||
hostCmdUUID: "cmd-uuid-5678",
|
||||
profileContents: `<Replace><Data>CA: $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_CERTIFICATE</Data></Replace>`,
|
||||
expectError: true,
|
||||
processingError: "Custom SCEP integration requires a Fleet Premium license.",
|
||||
freeTier: true,
|
||||
},
|
||||
{
|
||||
name: "custom scep proxy challenge ca not found",
|
||||
hostUUID: "test-host-1234-uuid",
|
||||
hostCmdUUID: "cmd-uuid-5678",
|
||||
profileContents: `<Replace><Data>CA: $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_CERTIFICATE</Data></Replace>`,
|
||||
expectError: true,
|
||||
processingError: "Fleet couldn't populate $CUSTOM_SCEP_CHALLENGE_CERTIFICATE because CERTIFICATE certificate authority doesn't exist.",
|
||||
},
|
||||
{
|
||||
name: "custom scep proxy challenge ca found and replaced",
|
||||
hostUUID: "test-host-1234-uuid",
|
||||
hostCmdUUID: "cmd-uuid-5678",
|
||||
profileContents: `<Replace><Data> $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_CERTIFICATE</Data></Replace>`,
|
||||
expectedContents: `<Replace><Data>supersecret</Data></Replace>`,
|
||||
setup: func() {
|
||||
ds.GetAllCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) ([]*fleet.CertificateAuthority, error) {
|
||||
return []*fleet.CertificateAuthority{
|
||||
{
|
||||
ID: 1,
|
||||
Name: ptr.String("CERTIFICATE"),
|
||||
Type: string(fleet.CATypeCustomSCEPProxy),
|
||||
URL: ptr.String("https://scep.proxy.url/scep"),
|
||||
Challenge: ptr.String("supersecret"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
ds.NewChallengeFunc = func(ctx context.Context) (string, error) {
|
||||
return "supersecret", nil
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
baseSetup()
|
||||
if tt.setup != nil {
|
||||
tt.setup()
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
ds = new(mock.Store) // Reset the mock datastore after each test, to avoid overlapping setups.
|
||||
})
|
||||
|
||||
licenseInfo := &fleet.LicenseInfo{
|
||||
Tier: fleet.TierPremium,
|
||||
}
|
||||
if tt.freeTier {
|
||||
licenseInfo.Tier = fleet.TierFree
|
||||
}
|
||||
ctx := license.NewContext(t.Context(), licenseInfo)
|
||||
|
||||
appConfig, err := ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Populate this one, in setup by mocking ds.GetAllCertificateAuthoritiesFunc if needed.
|
||||
groupedCAs, err := ds.GetGroupedCertificateAuthorities(ctx, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := PreprocessWindowsProfileContentsForDeployment(ctx, log.NewNopLogger(), ds, appConfig, tt.hostUUID, tt.hostCmdUUID, profileUUID, groupedCAs, tt.profileContents)
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
if tt.processingError != "" {
|
||||
var processingErr *MicrosoftProfileProcessingError
|
||||
require.ErrorAs(t, err, &processingErr, "expected ProfileProcessingError")
|
||||
require.Equal(t, tt.processingError, processingErr.Error())
|
||||
}
|
||||
return // do not verify profile contents if an error is expected
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expectedContents, result)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package profiles
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
)
|
||||
|
||||
/*
|
||||
This file contains functions to replace profile variables in MDM profiles, that are supported
|
||||
on multiple platforms, so it can be shared.
|
||||
|
||||
Fleet variables supported across systems:
|
||||
- $FLEET_VAR_CUSTOM_SCEP_CHALLENGE_<CA_NAME>
|
||||
- $FLEET_VAR_CUSTOM_SCEP_PROXY_URL_<CA_NAME>
|
||||
- $FLEET_VAR_HOST_END_USER_EMAIL_IDP
|
||||
|
||||
Once more is needed it should be placed here, and the main replacement logic can be taken from the apple_mdm.go
|
||||
under server/service folder. Inside the `preprocessProfileContents` under the `fleetVarLoop` loop.
|
||||
*/
|
||||
|
||||
func ReplaceCustomSCEPChallengeVariable(ctx context.Context, logger kitlog.Logger, fleetVariable string, customSCEPCAs map[string]*fleet.CustomSCEPProxyCA, profileContents string) (contents string, replacedVariable bool, err error) {
|
||||
caName := strings.TrimPrefix(fleetVariable, string(fleet.FleetVarCustomSCEPChallengePrefix))
|
||||
ca, ok := customSCEPCAs[caName]
|
||||
if !ok {
|
||||
level.Error(logger).Log("msg", "Custom SCEP CA not found. "+
|
||||
"This error should never happen since we validated/populated CAs earlier", "ca_name", caName)
|
||||
return "", false, nil
|
||||
}
|
||||
contents, err = ReplaceExactFleetPrefixVariableInXML(string(fleet.FleetVarCustomSCEPChallengePrefix), ca.Name, profileContents, ca.Challenge)
|
||||
if err != nil {
|
||||
return "", false, ctxerr.Wrap(ctx, err, "replacing Fleet variable for SCEP challenge")
|
||||
}
|
||||
return contents, true, nil
|
||||
}
|
||||
|
||||
func ReplaceCustomSCEPProxyURLVariable(ctx context.Context, logger kitlog.Logger, ds fleet.Datastore, appConfig *fleet.AppConfig,
|
||||
fleetVar string, customSCEPCAs map[string]*fleet.CustomSCEPProxyCA, profileContents string,
|
||||
hostUUID string, profUUID string,
|
||||
) (contents string, managedCertificate *fleet.MDMManagedCertificate, replacedVariable bool, err error) {
|
||||
caName := strings.TrimPrefix(fleetVar, string(fleet.FleetVarCustomSCEPProxyURLPrefix))
|
||||
ca, ok := customSCEPCAs[caName]
|
||||
if !ok {
|
||||
level.Error(logger).Log("msg", "Custom SCEP CA not found. "+
|
||||
"This error should never happen since we validated/populated CAs earlier", "ca_name", caName)
|
||||
return "", nil, false, nil
|
||||
}
|
||||
// Generate a new SCEP challenge for the profile
|
||||
challenge, err := ds.NewChallenge(ctx)
|
||||
if err != nil {
|
||||
return "", nil, false, ctxerr.Wrap(ctx, err, "generating SCEP challenge")
|
||||
}
|
||||
// Insert the SCEP URL into the profile contents
|
||||
proxyURL := fmt.Sprintf("%s%s%s", appConfig.MDMUrl(), apple_mdm.SCEPProxyPath,
|
||||
url.PathEscape(fmt.Sprintf("%s,%s,%s,%s", hostUUID, profUUID, caName, challenge)))
|
||||
contents, err = ReplaceExactFleetPrefixVariableInXML(string(fleet.FleetVarCustomSCEPProxyURLPrefix), ca.Name, profileContents, proxyURL)
|
||||
if err != nil {
|
||||
return "", nil, false, ctxerr.Wrap(ctx, err, "replacing Fleet variable for SCEP proxy URL")
|
||||
}
|
||||
|
||||
managedCertificate = &fleet.MDMManagedCertificate{
|
||||
HostUUID: hostUUID,
|
||||
ProfileUUID: profUUID,
|
||||
Type: fleet.CAConfigCustomSCEPProxy,
|
||||
CAName: caName,
|
||||
}
|
||||
|
||||
return contents, managedCertificate, true, nil
|
||||
}
|
||||
|
||||
// ! Important if we add new replacedVariable=false cases, that we verify the caller functions still behave correctly, as some run actions based on whether a variable was replaced or not.
|
||||
func ReplaceHostEndUserEmailIDPVariable(ctx context.Context, ds fleet.Datastore, profileContents string, hostUUID string) (contents string, replacedVariable bool, err error) {
|
||||
email, err := fleet.GetFirstIDPEmail(ctx, ds, hostUUID)
|
||||
if err != nil {
|
||||
return "", false, ctxerr.Wrap(ctx, err, "getting IDP email")
|
||||
}
|
||||
if email == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
contents = ReplaceFleetVariableInXML(fleet.FleetVarHostEndUserEmailIDPRegexp, profileContents, *email)
|
||||
return contents, true, nil
|
||||
}
|
||||
|
||||
func ReplaceExactFleetPrefixVariableInXML(prefix string, suffix string, contents string, replacement string) (string, error) {
|
||||
// Escape XML characters since this replacement is intended for XML profile.
|
||||
b := make([]byte, 0, len(replacement))
|
||||
buf := bytes.NewBuffer(b)
|
||||
// error is always nil for Buffer.Write method, so we ignore it
|
||||
_ = xml.EscapeText(buf, []byte(replacement))
|
||||
|
||||
// We are replacing an exact variable, which should be present in XML like: <something>$FLEET_VAR_OUR_VAR</something>
|
||||
// We strip the leading/trailing whitespace since we don't want them to remain in XML
|
||||
// Our plist parser ignores spaces in <data> type. We don't catch this issue at profile validation, so we handle it here.
|
||||
fleetVar := "FLEET_VAR_" + prefix + suffix
|
||||
re, err := regexp.Compile(fmt.Sprintf(`>\s*((\$%s)|(\${%s}))\s*<`, fleetVar, fleetVar))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return re.ReplaceAllLiteralString(contents, fmt.Sprintf(`>%s<`, buf.String())), nil
|
||||
}
|
||||
|
||||
func ReplaceFleetVariableInXML(regExp *regexp.Regexp, contents string, replacement string) string {
|
||||
// Escape XML characters since this replacement is intended for XML profile.
|
||||
b := make([]byte, 0, len(replacement))
|
||||
buf := bytes.NewBuffer(b)
|
||||
// error is always nil for Buffer.Write method, so we ignore it
|
||||
_ = xml.EscapeText(buf, []byte(replacement))
|
||||
return regExp.ReplaceAllLiteralString(contents, buf.String())
|
||||
}
|
||||
|
||||
func IsCustomSCEPConfigured(ctx context.Context,
|
||||
customSCEPCAs map[string]*fleet.CustomSCEPProxyCA, caName string, fleetVar string,
|
||||
onError func(string) error, // A function that allows the caller to run some code on errors, if an error is returned it will be returned by IsCustomSCEPConfigured
|
||||
) error {
|
||||
if !license.IsPremium(ctx) {
|
||||
return onError("Custom SCEP integration requires a Fleet Premium license.")
|
||||
}
|
||||
if _, ok := customSCEPCAs[caName]; !ok {
|
||||
return onError(fmt.Sprintf("Fleet couldn't populate $%s because %s certificate authority doesn't exist.", fleetVar, caName))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1563,6 +1563,8 @@ type BatchApplyCertificateAuthoritiesFunc func(ctx context.Context, ops fleet.Ce
|
||||
|
||||
type GetCurrentTimeFunc func(ctx context.Context) (time.Time, error)
|
||||
|
||||
type UpdateOrDeleteHostMDMWindowsProfileFunc func(ctx context.Context, profile *fleet.HostMDMWindowsProfile) error
|
||||
|
||||
type DataStore struct {
|
||||
HealthCheckFunc HealthCheckFunc
|
||||
HealthCheckFuncInvoked bool
|
||||
@@ -3874,6 +3876,9 @@ type DataStore struct {
|
||||
GetCurrentTimeFunc GetCurrentTimeFunc
|
||||
GetCurrentTimeFuncInvoked bool
|
||||
|
||||
UpdateOrDeleteHostMDMWindowsProfileFunc UpdateOrDeleteHostMDMWindowsProfileFunc
|
||||
UpdateOrDeleteHostMDMWindowsProfileFuncInvoked bool
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -9266,3 +9271,10 @@ func (s *DataStore) GetCurrentTime(ctx context.Context) (time.Time, error) {
|
||||
s.mu.Unlock()
|
||||
return s.GetCurrentTimeFunc(ctx)
|
||||
}
|
||||
|
||||
func (s *DataStore) UpdateOrDeleteHostMDMWindowsProfile(ctx context.Context, profile *fleet.HostMDMWindowsProfile) error {
|
||||
s.mu.Lock()
|
||||
s.UpdateOrDeleteHostMDMWindowsProfileFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.UpdateOrDeleteHostMDMWindowsProfileFunc(ctx, profile)
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ type SearchHostsFunc func(ctx context.Context, matchQuery string, queryID *uint,
|
||||
|
||||
type ListHostDeviceMappingFunc func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error)
|
||||
|
||||
type SetHostDeviceMappingFunc func(ctx context.Context, id uint, email, source string) ([]*fleet.HostDeviceMapping, error)
|
||||
type SetHostDeviceMappingFunc func(ctx context.Context, id uint, email string, source string) ([]*fleet.HostDeviceMapping, error)
|
||||
|
||||
type HostLiteByIdentifierFunc func(ctx context.Context, identifier string) (*fleet.HostLite, error)
|
||||
|
||||
@@ -2822,7 +2822,7 @@ func (s *Service) ListHostDeviceMapping(ctx context.Context, id uint) ([]*fleet.
|
||||
return s.ListHostDeviceMappingFunc(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) SetHostDeviceMapping(ctx context.Context, id uint, email, source string) ([]*fleet.HostDeviceMapping, error) {
|
||||
func (s *Service) SetHostDeviceMapping(ctx context.Context, id uint, email string, source string) ([]*fleet.HostDeviceMapping, error) {
|
||||
s.mu.Lock()
|
||||
s.SetHostDeviceMappingFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
|
||||
+62
-142
@@ -9,7 +9,6 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -52,6 +51,7 @@ import (
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/cryptoutil"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
nano_service "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/profiles"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
|
||||
"github.com/fleetdm/fleet/v4/server/variables"
|
||||
@@ -4142,7 +4142,6 @@ func (svc *MDMAppleCheckinAndCommandService) handleRefetchDeviceResults(ctx cont
|
||||
|
||||
// We run this check here as we only want to run it on re-check ins for deleted hosts.
|
||||
if (platform == "ios" || platform == "ipados") && isLostModeEnabled {
|
||||
fmt.Println("===lost mode enabled on iPhone/iPad, checking for lock command record", host.UUID)
|
||||
cmd, err := svc.ds.GetLatestAppleMDMCommandOfType(ctx, host.UUID, "EnableLostMode")
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return nil, ctxerr.Wrap(ctx, err, "check for existing EnableLostMode command")
|
||||
@@ -5153,13 +5152,15 @@ func preprocessProfileContents(
|
||||
}
|
||||
if customSCEPCAs == nil {
|
||||
customSCEPCAs = make(map[string]*fleet.CustomSCEPProxyCA)
|
||||
for _, ca := range groupedCAs.CustomScepProxy {
|
||||
customSCEPCAs[ca.Name] = &ca
|
||||
}
|
||||
}
|
||||
configured, err := isCustomSCEPConfigured(ctx, groupedCAs, ds, hostProfilesToInstallMap, userEnrollmentsToHostUUIDsMap, customSCEPCAs, profUUID, target, caName,
|
||||
fleetVar)
|
||||
err := profiles.IsCustomSCEPConfigured(ctx, customSCEPCAs, caName, fleetVar, func(errMsg string) error {
|
||||
_, err := markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, userEnrollmentsToHostUUIDsMap, profUUID, errMsg, ptr.Time(time.Now().UTC()))
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "checking custom SCEP configuration")
|
||||
}
|
||||
if !configured {
|
||||
valid = false
|
||||
break initialFleetVarLoop
|
||||
}
|
||||
@@ -5281,58 +5282,39 @@ func preprocessProfileContents(
|
||||
}
|
||||
managedCertificatePayloads = append(managedCertificatePayloads, payload)
|
||||
|
||||
hostContents = replaceFleetVariableInXML(fleetVarNDESSCEPChallengeRegexp, hostContents, challenge)
|
||||
hostContents = profiles.ReplaceFleetVariableInXML(fleetVarNDESSCEPChallengeRegexp, hostContents, challenge)
|
||||
|
||||
case fleetVar == string(fleet.FleetVarNDESSCEPProxyURL):
|
||||
// Insert the SCEP URL into the profile contents
|
||||
proxyURL := fmt.Sprintf("%s%s%s", appConfig.MDMUrl(), apple_mdm.SCEPProxyPath,
|
||||
url.PathEscape(fmt.Sprintf("%s,%s,NDES", hostUUID, profUUID)))
|
||||
hostContents = replaceFleetVariableInXML(fleetVarNDESSCEPProxyURLRegexp, hostContents, proxyURL)
|
||||
hostContents = profiles.ReplaceFleetVariableInXML(fleetVarNDESSCEPProxyURLRegexp, hostContents, proxyURL)
|
||||
|
||||
case fleetVar == string(fleet.FleetVarSCEPRenewalID):
|
||||
// Insert the SCEP renewal ID into the SCEP Payload CN or OU
|
||||
fleetRenewalID := "fleet-" + profUUID
|
||||
hostContents = replaceFleetVariableInXML(fleetVarSCEPRenewalIDRegexp, hostContents, fleetRenewalID)
|
||||
hostContents = profiles.ReplaceFleetVariableInXML(fleetVarSCEPRenewalIDRegexp, hostContents, fleetRenewalID)
|
||||
|
||||
case strings.HasPrefix(fleetVar, string(fleet.FleetVarCustomSCEPChallengePrefix)):
|
||||
caName := strings.TrimPrefix(fleetVar, string(fleet.FleetVarCustomSCEPChallengePrefix))
|
||||
ca, ok := customSCEPCAs[caName]
|
||||
if !ok {
|
||||
level.Error(logger).Log("msg", "Custom SCEP CA not found. "+
|
||||
"This error should never happen since we validated/populated CAs earlier", "ca_name", caName)
|
||||
replacedContents, replacedVariable, err := profiles.ReplaceCustomSCEPChallengeVariable(ctx, logger, fleetVar, customSCEPCAs, hostContents)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "replacing custom SCEP challenge variable")
|
||||
}
|
||||
if !replacedVariable {
|
||||
continue
|
||||
}
|
||||
hostContents, err = replaceExactFleetPrefixVariableInXML(string(fleet.FleetVarCustomSCEPChallengePrefix), ca.Name, hostContents, ca.Challenge)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "replacing Fleet variable for SCEP challenge")
|
||||
}
|
||||
hostContents = replacedContents
|
||||
|
||||
case strings.HasPrefix(fleetVar, string(fleet.FleetVarCustomSCEPProxyURLPrefix)):
|
||||
caName := strings.TrimPrefix(fleetVar, string(fleet.FleetVarCustomSCEPProxyURLPrefix))
|
||||
ca, ok := customSCEPCAs[caName]
|
||||
if !ok {
|
||||
level.Error(logger).Log("msg", "Custom SCEP CA not found. "+
|
||||
"This error should never happen since we validated/populated CAs earlier", "ca_name", caName)
|
||||
replacedContents, managedCertificate, replacedVariable, err := profiles.ReplaceCustomSCEPProxyURLVariable(ctx, logger, ds, appConfig, fleetVar, customSCEPCAs, hostContents, hostUUID, profUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "replacing custom SCEP proxy URL variable")
|
||||
}
|
||||
if !replacedVariable {
|
||||
continue
|
||||
}
|
||||
// Generate a new SCEP challenge for the profile
|
||||
challenge, err := ds.NewChallenge(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "generating SCEP challenge")
|
||||
}
|
||||
// Insert the SCEP URL into the profile contents
|
||||
proxyURL := fmt.Sprintf("%s%s%s", appConfig.MDMUrl(), apple_mdm.SCEPProxyPath,
|
||||
url.PathEscape(fmt.Sprintf("%s,%s,%s,%s", hostUUID, profUUID, caName, challenge)))
|
||||
hostContents, err = replaceExactFleetPrefixVariableInXML(string(fleet.FleetVarCustomSCEPProxyURLPrefix), ca.Name, hostContents, proxyURL)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "replacing Fleet variable for SCEP proxy URL")
|
||||
}
|
||||
managedCertificatePayloads = append(managedCertificatePayloads, &fleet.MDMManagedCertificate{
|
||||
HostUUID: hostUUID,
|
||||
ProfileUUID: profUUID,
|
||||
Type: fleet.CAConfigCustomSCEPProxy,
|
||||
CAName: caName,
|
||||
})
|
||||
hostContents = replacedContents
|
||||
managedCertificatePayloads = append(managedCertificatePayloads, managedCertificate)
|
||||
|
||||
case strings.HasPrefix(fleetVar, string(fleet.FleetVarSmallstepSCEPChallengePrefix)):
|
||||
caName := strings.TrimPrefix(fleetVar, string(fleet.FleetVarSmallstepSCEPChallengePrefix))
|
||||
@@ -5369,7 +5351,7 @@ func preprocessProfileContents(
|
||||
CAName: caName,
|
||||
}
|
||||
managedCertificatePayloads = append(managedCertificatePayloads, payload)
|
||||
hostContents, err = replaceExactFleetPrefixVariableInXML(string(fleet.FleetVarSmallstepSCEPChallengePrefix), ca.Name, hostContents, challenge)
|
||||
hostContents, err = profiles.ReplaceExactFleetPrefixVariableInXML(string(fleet.FleetVarSmallstepSCEPChallengePrefix), ca.Name, hostContents, challenge)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "replacing Smallstep SCEP challenge variable")
|
||||
}
|
||||
@@ -5379,21 +5361,32 @@ func preprocessProfileContents(
|
||||
caName := strings.TrimPrefix(fleetVar, string(fleet.FleetVarSmallstepSCEPProxyURLPrefix))
|
||||
proxyURL := fmt.Sprintf("%s%s%s", appConfig.MDMUrl(), apple_mdm.SCEPProxyPath,
|
||||
url.PathEscape(fmt.Sprintf("%s,%s,%s", hostUUID, profUUID, caName)))
|
||||
hostContents, err = replaceExactFleetPrefixVariableInXML(string(fleet.FleetVarSmallstepSCEPProxyURLPrefix), caName, hostContents, proxyURL)
|
||||
hostContents, err = profiles.ReplaceExactFleetPrefixVariableInXML(string(fleet.FleetVarSmallstepSCEPProxyURLPrefix), caName, hostContents, proxyURL)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "replacing Smallstep SCEP URL variable")
|
||||
}
|
||||
|
||||
case fleetVar == string(fleet.FleetVarHostEndUserEmailIDP):
|
||||
email, ok, err := getIDPEmail(ctx, ds, target, hostUUID)
|
||||
replacedContents, replacedVariable, err := profiles.ReplaceHostEndUserEmailIDPVariable(ctx, ds, hostContents, hostUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting IDP email")
|
||||
return ctxerr.Wrap(ctx, err, "replacing host end user email IDP variable")
|
||||
}
|
||||
if !ok {
|
||||
if !replacedVariable {
|
||||
// We couldn't retrieve the end user email IDP, so mark the profile as failed with additional detail.
|
||||
err = ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
|
||||
CommandUUID: target.cmdUUID,
|
||||
HostUUID: hostUUID,
|
||||
Status: &fleet.MDMDeliveryFailed,
|
||||
Detail: fleet.HostEndUserEmailIDPVariableReplacementFailedError,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
})
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for host end user email IDP")
|
||||
}
|
||||
failed = true
|
||||
break fleetVarLoop
|
||||
}
|
||||
hostContents = replaceFleetVariableInXML(fleetVarHostEndUserEmailIDPRegexp, hostContents, email)
|
||||
hostContents = replacedContents
|
||||
|
||||
case fleetVar == string(fleet.FleetVarHostHardwareSerial):
|
||||
hardwareSerial, ok, err := getHostHardwareSerial(ctx, ds, target, hostUUID)
|
||||
@@ -5404,7 +5397,7 @@ func preprocessProfileContents(
|
||||
failed = true
|
||||
break fleetVarLoop
|
||||
}
|
||||
hostContents = replaceFleetVariableInXML(fleetVarHostHardwareSerialRegexp, hostContents, hardwareSerial)
|
||||
hostContents = profiles.ReplaceFleetVariableInXML(fleetVarHostHardwareSerialRegexp, hostContents, hardwareSerial)
|
||||
|
||||
case fleetVar == string(fleet.FleetVarHostEndUserIDPUsername) || fleetVar == string(fleet.FleetVarHostEndUserIDPUsernameLocalPart) ||
|
||||
fleetVar == string(fleet.FleetVarHostEndUserIDPGroups) || fleetVar == string(fleet.FleetVarHostEndUserIDPDepartment) ||
|
||||
@@ -5437,7 +5430,7 @@ func preprocessProfileContents(
|
||||
rx = fleetVarHostEndUserIDPFullnameRegexp
|
||||
value = strings.TrimSpace(user.IdpFullName)
|
||||
}
|
||||
hostContents = replaceFleetVariableInXML(rx, hostContents, value)
|
||||
hostContents = profiles.ReplaceFleetVariableInXML(rx, hostContents, value)
|
||||
|
||||
case strings.HasPrefix(fleetVar, string(fleet.FleetVarDigiCertPasswordPrefix)):
|
||||
// We will replace the password when we populate the certificate data
|
||||
@@ -5500,12 +5493,12 @@ func preprocessProfileContents(
|
||||
failed = true
|
||||
break fleetVarLoop
|
||||
}
|
||||
hostContents, err = replaceExactFleetPrefixVariableInXML(string(fleet.FleetVarDigiCertDataPrefix), caName, hostContents,
|
||||
hostContents, err = profiles.ReplaceExactFleetPrefixVariableInXML(string(fleet.FleetVarDigiCertDataPrefix), caName, hostContents,
|
||||
base64.StdEncoding.EncodeToString(cert.PfxData))
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "replacing Fleet variable for DigiCert data")
|
||||
}
|
||||
hostContents, err = replaceExactFleetPrefixVariableInXML(string(fleet.FleetVarDigiCertPasswordPrefix), caName, hostContents, cert.Password)
|
||||
hostContents, err = profiles.ReplaceExactFleetPrefixVariableInXML(string(fleet.FleetVarDigiCertPasswordPrefix), caName, hostContents, cert.Password)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "replacing Fleet variable for DigiCert password")
|
||||
}
|
||||
@@ -5566,16 +5559,28 @@ func replaceFleetVarInItem(ctx context.Context, ds fleet.Datastore, target *cmdT
|
||||
email, ok := caVarsCache[string(fleet.FleetVarHostEndUserEmailIDP)]
|
||||
if !ok {
|
||||
var err error
|
||||
email, ok, err = getIDPEmail(ctx, ds, target, hostUUID)
|
||||
foundEmail, err := fleet.GetFirstIDPEmail(ctx, ds, hostUUID)
|
||||
if err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "getting IDP email")
|
||||
}
|
||||
if !ok {
|
||||
if foundEmail == nil {
|
||||
// We couldn't retrieve the end user email IDP, so mark the profile as failed with additional detail.
|
||||
err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
|
||||
CommandUUID: target.cmdUUID,
|
||||
HostUUID: hostUUID,
|
||||
Status: &fleet.MDMDeliveryFailed,
|
||||
Detail: fleet.HostEndUserEmailIDPVariableReplacementFailedError,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
caVarsCache[string(fleet.FleetVarHostEndUserEmailIDP)] = email
|
||||
caVarsCache[string(fleet.FleetVarHostEndUserEmailIDP)] = *foundEmail
|
||||
email = *foundEmail
|
||||
}
|
||||
*item = replaceFleetVariableInXML(fleetVarHostEndUserEmailIDPRegexp, *item, email)
|
||||
*item = profiles.ReplaceFleetVariableInXML(fleetVarHostEndUserEmailIDPRegexp, *item, email)
|
||||
case string(fleet.FleetVarHostHardwareSerial):
|
||||
hardwareSerial, ok := caVarsCache[string(fleet.FleetVarHostHardwareSerial)]
|
||||
if !ok {
|
||||
@@ -5589,7 +5594,7 @@ func replaceFleetVarInItem(ctx context.Context, ds fleet.Datastore, target *cmdT
|
||||
}
|
||||
caVarsCache[string(fleet.FleetVarHostHardwareSerial)] = hardwareSerial
|
||||
}
|
||||
*item = replaceFleetVariableInXML(fleetVarHostHardwareSerialRegexp, *item, hardwareSerial)
|
||||
*item = profiles.ReplaceFleetVariableInXML(fleetVarHostHardwareSerialRegexp, *item, hardwareSerial)
|
||||
default:
|
||||
// We should not reach this since we validated the variables when saving app config
|
||||
}
|
||||
@@ -5715,33 +5720,6 @@ func getEmailLocalPart(email string) string {
|
||||
return local
|
||||
}
|
||||
|
||||
func getIDPEmail(ctx context.Context, ds fleet.Datastore, target *cmdTarget, hostUUID string) (string, bool, error) {
|
||||
// Insert the end user email IDP into the profile contents
|
||||
emails, err := ds.GetHostEmails(ctx, hostUUID, fleet.DeviceMappingMDMIdpAccounts)
|
||||
if err != nil {
|
||||
// This is a server error, so we exit.
|
||||
return "", false, ctxerr.Wrap(ctx, err, "getting host emails")
|
||||
}
|
||||
if len(emails) == 0 {
|
||||
// We couldn't retrieve the end user email IDP, so mark the profile as failed with additional detail.
|
||||
err := ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
|
||||
CommandUUID: target.cmdUUID,
|
||||
HostUUID: hostUUID,
|
||||
Status: &fleet.MDMDeliveryFailed,
|
||||
Detail: fmt.Sprintf("There is no IdP email for this host. "+
|
||||
"Fleet couldn't populate $FLEET_VAR_%s. "+
|
||||
"[Learn more](https://fleetdm.com/learn-more-about/idp-email)",
|
||||
fleet.FleetVarHostEndUserEmailIDP),
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
})
|
||||
if err != nil {
|
||||
return "", false, ctxerr.Wrap(ctx, err, "updating host MDM Apple profile for end user email IdP")
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
return emails[0], true, nil
|
||||
}
|
||||
|
||||
func getHostHardwareSerial(ctx context.Context, ds fleet.Datastore, target *cmdTarget, hostUUID string) (string, bool, error) {
|
||||
hosts, err := ds.ListHostsLiteByUUIDs(ctx, fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}, []string{hostUUID})
|
||||
if err != nil {
|
||||
@@ -6040,37 +6018,6 @@ func (cs *customSCEPVarsFound) SetRenewalID() (*customSCEPVarsFound, bool) {
|
||||
return cs, !alreadyPresent
|
||||
}
|
||||
|
||||
func isCustomSCEPConfigured(ctx context.Context, groupedCAs *fleet.GroupedCertificateAuthorities, ds fleet.Datastore,
|
||||
hostProfilesToInstallMap map[hostProfileUUID]*fleet.MDMAppleBulkUpsertHostProfilePayload,
|
||||
userEnrollmentsToHostUUIDsMap map[string]string,
|
||||
existingCustomSCEPCAs map[string]*fleet.CustomSCEPProxyCA, profUUID string, target *cmdTarget, caName string, fleetVar string,
|
||||
) (bool, error) {
|
||||
if !license.IsPremium(ctx) {
|
||||
return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, userEnrollmentsToHostUUIDsMap, profUUID, "Custom SCEP integration requires a Fleet Premium license.", ptr.Time(time.Now().UTC()))
|
||||
}
|
||||
if _, ok := existingCustomSCEPCAs[caName]; ok {
|
||||
return true, nil
|
||||
}
|
||||
configured := false
|
||||
var scepCA *fleet.CustomSCEPProxyCA
|
||||
if len(groupedCAs.CustomScepProxy) > 0 {
|
||||
for _, ca := range groupedCAs.CustomScepProxy {
|
||||
if ca.Name == caName {
|
||||
scepCA = &ca
|
||||
configured = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !configured || scepCA == nil {
|
||||
return markProfilesFailed(ctx, ds, target, hostProfilesToInstallMap, userEnrollmentsToHostUUIDsMap, profUUID,
|
||||
fmt.Sprintf("Fleet couldn't populate $%s because %s certificate authority doesn't exist.", fleetVar, caName), ptr.Time(time.Now().UTC()))
|
||||
}
|
||||
|
||||
existingCustomSCEPCAs[caName] = scepCA
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type smallstepVarsFound struct {
|
||||
urlCA map[string]struct{}
|
||||
challengeCA map[string]struct{}
|
||||
@@ -6254,33 +6201,6 @@ func markProfilesFailed(
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func replaceFleetVariableInXML(regExp *regexp.Regexp, contents string, replacement string) string {
|
||||
// Escape XML characters since this replacement is intended for XML profile.
|
||||
b := make([]byte, 0, len(replacement))
|
||||
buf := bytes.NewBuffer(b)
|
||||
// error is always nil for Buffer.Write method, so we ignore it
|
||||
_ = xml.EscapeText(buf, []byte(replacement))
|
||||
return regExp.ReplaceAllLiteralString(contents, buf.String())
|
||||
}
|
||||
|
||||
func replaceExactFleetPrefixVariableInXML(prefix string, suffix string, contents string, replacement string) (string, error) {
|
||||
// Escape XML characters since this replacement is intended for XML profile.
|
||||
b := make([]byte, 0, len(replacement))
|
||||
buf := bytes.NewBuffer(b)
|
||||
// error is always nil for Buffer.Write method, so we ignore it
|
||||
_ = xml.EscapeText(buf, []byte(replacement))
|
||||
|
||||
// We are replacing an exact variable, which should be present in XML like: <something>$FLEET_VAR_OUR_VAR</something>
|
||||
// We strip the leading/trailing whitespace since we don't want them to remain in XML
|
||||
// Our plist parser ignores spaces in <data> type. We don't catch this issue at profile validation, so we handle it here.
|
||||
fleetVar := "FLEET_VAR_" + prefix + suffix
|
||||
re, err := regexp.Compile(fmt.Sprintf(`>\s*((\$%s)|(\${%s}))\s*<`, fleetVar, fleetVar))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return re.ReplaceAllLiteralString(contents, fmt.Sprintf(`>%s<`, buf.String())), nil
|
||||
}
|
||||
|
||||
// scepCertRenewalThresholdDays defines the number of days before a SCEP
|
||||
// certificate must be renewed.
|
||||
const scepCertRenewalThresholdDays = 180
|
||||
|
||||
@@ -2310,6 +2310,11 @@ func ReconcileWindowsProfiles(ctx context.Context, ds fleet.Datastore, logger ki
|
||||
return ctxerr.Wrap(ctx, err, "get profile contents")
|
||||
}
|
||||
|
||||
groupedCAs, err := ds.GetGroupedCertificateAuthorities(ctx, true)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting grouped certificate authorities")
|
||||
}
|
||||
|
||||
for profUUID, target := range installTargets {
|
||||
p, ok := profileContents[profUUID]
|
||||
if !ok {
|
||||
@@ -2338,12 +2343,20 @@ func ReconcileWindowsProfiles(ctx context.Context, ds fleet.Datastore, logger ki
|
||||
continue
|
||||
}
|
||||
|
||||
// Preprocess the profile content for this specific host
|
||||
processedContent := microsoft_mdm.PreprocessWindowsProfileContents(hostUUID, string(p.SyncML))
|
||||
|
||||
// Create a unique command UUID for this host since the content is unique
|
||||
hostCmdUUID := uuid.New().String()
|
||||
|
||||
// Preprocess the profile content for this specific host
|
||||
processedContent, err := microsoft_mdm.PreprocessWindowsProfileContentsForDeployment(ctx, logger, ds, appConfig, hostUUID, hostCmdUUID, profUUID, groupedCAs, string(p.SyncML))
|
||||
var profileProcessingError *microsoft_mdm.MicrosoftProfileProcessingError
|
||||
if err != nil && !errors.As(err, &profileProcessingError) {
|
||||
return ctxerr.Wrapf(ctx, err, "preprocessing profile contents for host %s and profile %s", hostUUID, profUUID)
|
||||
} else if err != nil && errors.As(err, &profileProcessingError) {
|
||||
hp.Status = &fleet.MDMDeliveryFailed
|
||||
hp.Detail = profileProcessingError.Error()
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the command with the processed content
|
||||
command, err := buildCommandFromProfileBytes([]byte(processedContent), hostCmdUUID)
|
||||
if err != nil {
|
||||
|
||||
@@ -505,6 +505,12 @@ func TestReconcileWindowsProfilesWithFleetVariableError(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*mdm_types.GroupedCertificateAuthorities, error) {
|
||||
return &fleet.GroupedCertificateAuthorities{
|
||||
CustomScepProxy: []fleet.CustomSCEPProxyCA{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run ReconcileWindowsProfiles
|
||||
err := ReconcileWindowsProfiles(ctx, ds, logger)
|
||||
require.NoError(t, err) // The function should not return an error even if insert fails
|
||||
|
||||
@@ -3031,7 +3031,7 @@ func buildConfigProfilesWindowsQuery(
|
||||
var sb strings.Builder
|
||||
sb.WriteString("<SyncBody>")
|
||||
gotProfiles := false
|
||||
err := microsoft_mdm.LoopOverExpectedHostProfiles(ctx, ds, host, func(profile *fleet.ExpectedMDMProfile, hash, locURI, data string) {
|
||||
err := microsoft_mdm.LoopOverExpectedHostProfiles(ctx, logger, 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
|
||||
|
||||
Reference in New Issue
Block a user