mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
DCSW: Allow uploading and validating Windows SCEP profiles (#34691)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34247 Allows for uploading and validating SCEP profiles for Windows. # Checklist for submitter If some of the following don't apply, delete the relevant line. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
This commit is contained in:
parent
dbbb165494
commit
f2341497c8
12 changed files with 386 additions and 47 deletions
|
|
@ -35,6 +35,11 @@ const (
|
|||
// It provides clearer semantics when used in function signatures and data structures.
|
||||
type FleetVarName string
|
||||
|
||||
// Includes $FLEET_VAR prefix
|
||||
func (n FleetVarName) WithPrefix() string {
|
||||
return fmt.Sprintf("$FLEET_VAR_%s", n)
|
||||
}
|
||||
|
||||
const (
|
||||
// FleetVarNDESSCEPChallenge and other variables are used as $FLEET_VAR_<VARIABLE_NAME>.
|
||||
// For example: $FLEET_VAR_NDES_SCEP_CHALLENGE
|
||||
|
|
|
|||
|
|
@ -1012,6 +1012,10 @@ type SyncMLCmd struct {
|
|||
// AddCommands is a catch-all for any nested <Add> commands,
|
||||
// which can be found under <Atomic> elements.
|
||||
AddCommands []SyncMLCmd `xml:"Add,omitempty"`
|
||||
|
||||
// ExecCommands is a catch-all for any nested <Exec> commands,
|
||||
// which can be found under <Atomic> elements.
|
||||
ExecCommands []SyncMLCmd `xml:"Exec,omitempty"`
|
||||
}
|
||||
|
||||
// ParseWindowsMDMCommand parses the raw XML as a single Windows MDM command.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -79,9 +80,12 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided(enableCustomOSUpdates boo
|
|||
// we don't need to validate the required nesting
|
||||
// structure (Target>Item>LocURI) so we don't need to track all the tags.
|
||||
var inValidNode bool
|
||||
var inExec bool
|
||||
var inLocURI bool
|
||||
var inComment bool
|
||||
|
||||
windowSCEPProfileValidator := newWindowsSCEPProfileValidator()
|
||||
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
|
|
@ -105,8 +109,8 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided(enableCustomOSUpdates boo
|
|||
case xml.StartElement:
|
||||
// Top-level comments should be followed by <Replace> or <Add> elements
|
||||
if inComment {
|
||||
if !inValidNode && t.Name.Local != "Replace" && t.Name.Local != "Add" {
|
||||
return errors.New("Windows configuration profiles can only have <Replace> or <Add> top level elements after comments")
|
||||
if !inValidNode && t.Name.Local != "Replace" && t.Name.Local != "Add" && t.Name.Local != "Exec" {
|
||||
return errors.New("Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements after comments")
|
||||
}
|
||||
inValidNode = true
|
||||
inComment = false
|
||||
|
|
@ -115,15 +119,18 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided(enableCustomOSUpdates boo
|
|||
switch t.Name.Local {
|
||||
case "Replace", "Add":
|
||||
inValidNode = true
|
||||
case "Exec":
|
||||
inValidNode = true
|
||||
inExec = true
|
||||
case "LocURI":
|
||||
if !inValidNode {
|
||||
return errors.New("Windows configuration profiles can only have <Replace> or <Add> top level elements.")
|
||||
return errors.New("Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.")
|
||||
}
|
||||
inLocURI = true
|
||||
|
||||
default:
|
||||
if !inValidNode {
|
||||
return errors.New("Windows configuration profiles can only have <Replace> or <Add> top level elements.")
|
||||
return errors.New("Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,12 +138,26 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided(enableCustomOSUpdates boo
|
|||
switch t.Name.Local {
|
||||
case "Replace", "Add":
|
||||
inValidNode = false
|
||||
case "Exec":
|
||||
inValidNode = false
|
||||
inExec = false
|
||||
case "LocURI":
|
||||
inLocURI = false
|
||||
}
|
||||
|
||||
case xml.CharData:
|
||||
if inLocURI {
|
||||
if inExec {
|
||||
if err := windowSCEPProfileValidator.validateExecLocURI(string(t)); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := windowSCEPProfileValidator.validateLocURI(string(t)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateFleetProvidedLocURI(string(t), enableCustomOSUpdates); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -144,6 +165,10 @@ func (m *MDMWindowsConfigProfile) ValidateUserProvided(enableCustomOSUpdates boo
|
|||
}
|
||||
}
|
||||
|
||||
if err := windowSCEPProfileValidator.finalizeValidation(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +198,131 @@ func validateFleetProvidedLocURI(locURI string, enableCustomOSUpdates bool) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
// The following list of SCEP LocURIs is based on the documentation at
|
||||
// https://learn.microsoft.com/en-us/windows/client-management/mdm/clientcertificateinstall-csp#devicescep
|
||||
// Where going through all items, only for those with (Add or Replace) under "Supported operations" are included,
|
||||
// and then based on it being marked Optional, or Required in the description.
|
||||
|
||||
// A list containg all valid SCEP Profile LocURIs, a combination of optional and required to validate for non-SCEP LocURIs.
|
||||
var validSCEPProfileLocURIs = slices.Concat([]string{
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/AADKeyIdentifierList", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/ContainerName", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/CustomTextToShowInPrompt", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/KeyProtection", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/RetryCount", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/RetryDelay", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/SubjectAlternativeNames", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/TemplateName", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/ValidPeriod", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/ValidPeriodUnits", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
}, requiredSCEPProfileLocURIs)
|
||||
|
||||
var requiredSCEPProfileLocURIs = []string{
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/CAThumbprint", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/Challenge", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/EKUMapping", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/HashAlgorithm", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/KeyLength", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/KeyUsage", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/ServerURL", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/SubjectName", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
}
|
||||
|
||||
var validExecSCEPProfileLocURIs = []string{
|
||||
fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s/Install/Enroll", FleetVarSCEPWindowsCertificateID.WithPrefix()),
|
||||
}
|
||||
|
||||
type windowsSCEPProfileValidator struct {
|
||||
totalLocURIs int
|
||||
totalExecLocURIs int
|
||||
foundLocURIs map[string]bool
|
||||
foundExecLocURIs map[string]bool
|
||||
}
|
||||
|
||||
func newWindowsSCEPProfileValidator() *windowsSCEPProfileValidator {
|
||||
return &windowsSCEPProfileValidator{
|
||||
foundLocURIs: make(map[string]bool),
|
||||
foundExecLocURIs: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *windowsSCEPProfileValidator) isSCEPProfile() bool {
|
||||
return len(v.foundLocURIs) > 0 || (v.totalExecLocURIs > 0 && len(v.foundExecLocURIs) > 0)
|
||||
}
|
||||
|
||||
func (v *windowsSCEPProfileValidator) validateLocURI(locURI string) error {
|
||||
sanitizedLocURI := strings.TrimSpace(locURI)
|
||||
|
||||
// If we see a LocURI with SCEP prefix, but no Fleet Var we fail early.
|
||||
if v.isSCEPLocURIWithoutFleetVar(sanitizedLocURI) {
|
||||
return fmt.Errorf("You must use %q after \"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/\".", FleetVarSCEPWindowsCertificateID.WithPrefix())
|
||||
}
|
||||
|
||||
if slices.Contains(validSCEPProfileLocURIs, sanitizedLocURI) {
|
||||
v.foundLocURIs[sanitizedLocURI] = true
|
||||
}
|
||||
|
||||
v.totalLocURIs++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *windowsSCEPProfileValidator) validateExecLocURI(locURI string) error {
|
||||
sanitizedLocURI := strings.TrimSpace(locURI)
|
||||
|
||||
// If we see a LocURI with SCEP prefix, but no Fleet Var we fail early.
|
||||
if v.isSCEPLocURIWithoutFleetVar(sanitizedLocURI) {
|
||||
return fmt.Errorf("You must use %q after \"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/\".", FleetVarSCEPWindowsCertificateID.WithPrefix())
|
||||
}
|
||||
|
||||
if slices.Contains(validExecSCEPProfileLocURIs, sanitizedLocURI) {
|
||||
v.foundExecLocURIs[sanitizedLocURI] = true
|
||||
}
|
||||
|
||||
v.totalExecLocURIs++
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSCEPLocURIWithoutFleetVar checks that the provided locURI starts with the SCEP prefix
|
||||
// and that it includes the required Fleet Var for SCEP Windows Certificate ID.
|
||||
// Skips any locURI that does not start with the SCEP prefix.
|
||||
func (v windowsSCEPProfileValidator) isSCEPLocURIWithoutFleetVar(locURI string) bool {
|
||||
if strings.HasPrefix(locURI, "./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/") &&
|
||||
!strings.HasPrefix(locURI, fmt.Sprintf("./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/%s", FleetVarSCEPWindowsCertificateID.WithPrefix())) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *windowsSCEPProfileValidator) finalizeValidation() error {
|
||||
if !v.isSCEPProfile() {
|
||||
// Cheeky validation here, to only allow Exec elements in SCEP profiles.
|
||||
if v.totalExecLocURIs > 0 {
|
||||
return errors.New("Only SCEP profiles can include <Exec> elements.")
|
||||
}
|
||||
return nil // Not a SCEP profile, nothing to validate here.
|
||||
}
|
||||
|
||||
// Verify that we do not have any non-scep loc URIs present
|
||||
if v.totalLocURIs != len(v.foundLocURIs) {
|
||||
return errors.New("Only options that have <LocURI> starting with \"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/\" can be added to SCEP profile.")
|
||||
}
|
||||
|
||||
// Check that at least one Exec LocURI is present and it matches the only one we have in the array.
|
||||
if len(v.foundExecLocURIs) != 1 && !v.foundExecLocURIs[validExecSCEPProfileLocURIs[0]] {
|
||||
return errors.New("Couldn't add. \"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID/Install/Enroll\" must be included within <Exec>. Please add and try again.")
|
||||
}
|
||||
|
||||
// Check that all required LocURIs are present
|
||||
for _, requiredLocURI := range requiredSCEPProfileLocURIs {
|
||||
if !v.foundLocURIs[requiredLocURI] {
|
||||
return fmt.Errorf("%q is missing. Please add and try again", requiredLocURI)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type MDMWindowsProfilePayload struct {
|
||||
ProfileUUID string `db:"profile_uuid"`
|
||||
ProfileName string `db:"profile_name"`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fleet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
|
|
@ -41,7 +42,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</SyncML>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.",
|
||||
},
|
||||
{
|
||||
name: "Add top level element",
|
||||
|
|
@ -159,7 +160,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Alert>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.",
|
||||
},
|
||||
{
|
||||
name: "XML with Replace and Atomic",
|
||||
|
|
@ -177,7 +178,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Atomic>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.",
|
||||
},
|
||||
{
|
||||
name: "XML with Replace and Delete",
|
||||
|
|
@ -195,7 +196,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Delete>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.",
|
||||
},
|
||||
{
|
||||
name: "XML with Replace and Exec",
|
||||
|
|
@ -213,7 +214,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Exec>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Only SCEP profiles can include <Exec> elements.",
|
||||
},
|
||||
{
|
||||
name: "XML with Replace and Get",
|
||||
|
|
@ -231,7 +232,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Get>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.",
|
||||
},
|
||||
{
|
||||
name: "XML with Replace and Results",
|
||||
|
|
@ -249,7 +250,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Results>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.",
|
||||
},
|
||||
{
|
||||
name: "XML with Replace and Status",
|
||||
|
|
@ -267,7 +268,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Status>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.",
|
||||
},
|
||||
{
|
||||
name: "XML with elements not defined in the protocol",
|
||||
|
|
@ -285,7 +286,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Foo>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements.",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements.",
|
||||
},
|
||||
{
|
||||
name: "invalid XML with mismatched tags",
|
||||
|
|
@ -484,7 +485,7 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
</Replace>
|
||||
`),
|
||||
},
|
||||
wantErr: "Windows configuration profiles can only have <Replace> or <Add> top level elements after comments",
|
||||
wantErr: "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements after comments",
|
||||
},
|
||||
{
|
||||
name: "XML with nested root element in data",
|
||||
|
|
@ -596,6 +597,114 @@ func TestValidateUserProvided(t *testing.T) {
|
|||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "SCEP profile with other LocURIs",
|
||||
profile: MDMWindowsConfigProfile{
|
||||
SyncML: []byte(`
|
||||
<Replace>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID</LocURI>
|
||||
</Target>
|
||||
</Replace>
|
||||
<Replace>
|
||||
<Target>
|
||||
<LocURI>Custom/URI</LocURI>
|
||||
</Target>
|
||||
</Replace>
|
||||
`),
|
||||
},
|
||||
wantErr: "Only options that have <LocURI> starting with \"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/\" can be added to SCEP profile.",
|
||||
},
|
||||
{
|
||||
name: "SCEP profile without Exec block",
|
||||
profile: MDMWindowsConfigProfile{
|
||||
SyncML: []byte(`
|
||||
<Replace>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID</LocURI>
|
||||
</Target>
|
||||
</Replace>
|
||||
`),
|
||||
},
|
||||
wantErr: "\"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID/Install/Enroll\" must be included within <Exec>. Please add and try again.",
|
||||
},
|
||||
{
|
||||
name: "SCEP profile with Exec block, but worng LocURI ",
|
||||
profile: MDMWindowsConfigProfile{
|
||||
SyncML: []byte(`
|
||||
<Replace>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID</LocURI>
|
||||
</Target>
|
||||
</Replace>
|
||||
<Exec>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID/Random/Scep/LocURI</LocURI>
|
||||
</Target>
|
||||
</Item>
|
||||
</Exec>
|
||||
`),
|
||||
},
|
||||
wantErr: "Couldn't add. \"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID/Install/Enroll\" must be included within <Exec>. Please add and try again.",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SCEP profile with missing $FLEET_VAR_%s after SCEP LocURI", FleetVarSCEPWindowsCertificateID),
|
||||
profile: MDMWindowsConfigProfile{
|
||||
SyncML: []byte(`
|
||||
<Add>
|
||||
<CmdID>12</CmdID>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/bogus-id-that-is-not-fleet-var/Install/CAThumbprint</LocURI>
|
||||
</Target>
|
||||
<Meta>
|
||||
<Format xmlns="syncml:metinf">chr</Format>
|
||||
</Meta>
|
||||
<Data>0DE4135C02E5E3C040FE1353E204D8B6F331F47A</Data>
|
||||
</Item>
|
||||
</Add>
|
||||
`),
|
||||
},
|
||||
wantErr: fmt.Sprintf("You must use \"$FLEET_VAR_%s\" after \"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/\".", FleetVarSCEPWindowsCertificateID),
|
||||
},
|
||||
{
|
||||
name: "SCEP Profile with missing required LocURI",
|
||||
profile: MDMWindowsConfigProfile{
|
||||
SyncML: []byte(`
|
||||
<Add>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID</LocURI>
|
||||
</Target>
|
||||
</Item>
|
||||
</Add>
|
||||
<Exec>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_SCEP_WINDOWS_CERTIFICATE_ID/Install/Enroll</LocURI>
|
||||
</Target>
|
||||
</Item>
|
||||
</Exec>
|
||||
`),
|
||||
},
|
||||
wantErr: fmt.Sprintf("\"./Device/Vendor/MSFT/ClientCertificateInstall/SCEP/$FLEET_VAR_%s/Install/CAThumbprint\" is missing.", FleetVarSCEPWindowsCertificateID),
|
||||
},
|
||||
{
|
||||
name: "Only SCEP profiles can have Exec elements",
|
||||
profile: MDMWindowsConfigProfile{
|
||||
SyncML: []byte(`
|
||||
<Exec>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>./Device/Vendor/CustomExecTargetLocURI</LocURI>
|
||||
</Target>
|
||||
</Item>
|
||||
</Exec>
|
||||
`),
|
||||
},
|
||||
wantErr: "Only SCEP profiles can include <Exec> elements.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
|
|
@ -909,6 +909,23 @@ func TestPreprocessWindowsProfileContentsForVerification(t *testing.T) {
|
|||
func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
|
||||
scimUser := &fleet.ScimUser{
|
||||
UserName: "test@idp.com",
|
||||
GivenName: ptr.String("First"),
|
||||
FamilyName: ptr.String("Last"),
|
||||
Department: ptr.String("Department"),
|
||||
Groups: []fleet.ScimUserGroup{
|
||||
{
|
||||
ID: 1,
|
||||
DisplayName: "Group One",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
DisplayName: "Group Two",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
baseSetup := func() {
|
||||
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
|
||||
if ds.GetAllCertificateAuthoritiesFunc == nil {
|
||||
|
|
@ -934,24 +951,10 @@ func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
|
|||
ds.HostIDsByIdentifierFunc = func(ctx context.Context, filter fleet.TeamFilter, hostnames []string) ([]uint, error) {
|
||||
return []uint{42}, nil
|
||||
}
|
||||
|
||||
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
||||
if hostID == 42 {
|
||||
return &fleet.ScimUser{
|
||||
UserName: "test@idp.com",
|
||||
GivenName: ptr.String("First"),
|
||||
FamilyName: ptr.String("Last"),
|
||||
Department: ptr.String("Department"),
|
||||
Groups: []fleet.ScimUserGroup{
|
||||
{
|
||||
ID: 1,
|
||||
DisplayName: "Group One",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
DisplayName: "Group Two",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
return scimUser, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no scim user for host id %d", hostID)
|
||||
|
|
@ -1081,6 +1084,32 @@ func TestPreprocessWindowsProfileContentsForDeployment(t *testing.T) {
|
|||
profileContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>User: $FLEET_VAR_HOST_END_USER_IDP_USERNAME - $FLEET_VAR_HOST_END_USER_IDP_USERNAME_LOCAL_PART - $FLEET_VAR_HOST_END_USER_IDP_GROUPS - $FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT - $FLEET_VAR_HOST_END_USER_IDP_FULL_NAME</Data></Item></Replace>`,
|
||||
expectedContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>User: test@idp.com - test - Group One,Group Two - Department - First Last</Data></Item></Replace>`,
|
||||
},
|
||||
{
|
||||
name: "missing groups on idp user",
|
||||
hostUUID: "no-groups-idp",
|
||||
profileContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>User: $FLEET_VAR_HOST_END_USER_IDP_GROUPS</Data></Item></Replace>`,
|
||||
expectError: true,
|
||||
processingError: "There are no IdP groups for this host. Fleet couldn't populate $FLEET_VAR_HOST_END_USER_IDP_GROUPS.",
|
||||
setup: func() {
|
||||
scimUser.Groups = []fleet.ScimUserGroup{}
|
||||
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
||||
return scimUser, nil
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing department on idp user",
|
||||
hostUUID: "no-department-idp",
|
||||
profileContents: `<Replace><Item><Target><LocURI>./Device/Test</LocURI></Target><Data>User: $FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT</Data></Item></Replace>`,
|
||||
expectError: true,
|
||||
processingError: "There is no IdP department for this host. Fleet couldn't populate $FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT.",
|
||||
setup: func() {
|
||||
scimUser.Department = nil
|
||||
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
||||
return scimUser, nil
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
params := PreprocessingParameters{
|
||||
|
|
|
|||
|
|
@ -143,9 +143,9 @@ func getHostEndUserIDPUser(ctx context.Context, ds fleet.Datastore,
|
|||
return nil, false, ctxerr.Wrap(ctx, err, "get end users for host")
|
||||
}
|
||||
|
||||
noGroupsErr := fmt.Sprintf("There is no IdP groups for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPGroups)
|
||||
noDepartmentErr := fmt.Sprintf("There is no IdP department for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPDepartment)
|
||||
noFullnameErr := fmt.Sprintf("There is no IdP full name for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPFullname)
|
||||
noGroupsErr := fmt.Sprintf("There are no IdP groups for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPGroups)
|
||||
noDepartmentErr := fmt.Sprintf("There is no IdP department for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPDepartment)
|
||||
noFullnameErr := fmt.Sprintf("There is no IdP full name for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPFullname)
|
||||
if len(users) > 0 && users[0].IdpUserName != "" {
|
||||
idpUser := users[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -5216,7 +5216,7 @@ func TestPreprocessProfileContentsEndUserIDP(t *testing.T) {
|
|||
},
|
||||
assert: func(output string) {
|
||||
assert.Len(t, targets, 0) // target is not present
|
||||
assert.Contains(t, updatedProfile.Detail, "There is no IdP groups for this host. Fleet couldn’t populate $FLEET_VAR_HOST_END_USER_IDP_GROUPS.")
|
||||
assert.Contains(t, updatedProfile.Detail, "There are no IdP groups for this host. Fleet couldn't populate $FLEET_VAR_HOST_END_USER_IDP_GROUPS.")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -5233,7 +5233,7 @@ func TestPreprocessProfileContentsEndUserIDP(t *testing.T) {
|
|||
},
|
||||
assert: func(output string) {
|
||||
assert.Len(t, targets, 0) // target is not present
|
||||
assert.Contains(t, updatedProfile.Detail, "There is no IdP department for this host. Fleet couldn’t populate $FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT.")
|
||||
assert.Contains(t, updatedProfile.Detail, "There is no IdP department for this host. Fleet couldn't populate $FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT.")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -5250,7 +5250,7 @@ func TestPreprocessProfileContentsEndUserIDP(t *testing.T) {
|
|||
},
|
||||
assert: func(output string) {
|
||||
assert.Len(t, targets, 0) // target is not present
|
||||
assert.Contains(t, updatedProfile.Detail, "There is no IdP groups for this host. Fleet couldn’t populate $FLEET_VAR_HOST_END_USER_IDP_GROUPS.")
|
||||
assert.Contains(t, updatedProfile.Detail, "There are no IdP groups for this host. Fleet couldn't populate $FLEET_VAR_HOST_END_USER_IDP_GROUPS.")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -5295,7 +5295,7 @@ func TestPreprocessProfileContentsEndUserIDP(t *testing.T) {
|
|||
},
|
||||
assert: func(output string) {
|
||||
assert.Len(t, targets, 0) // target is not present
|
||||
assert.Contains(t, updatedProfile.Detail, "There is no IdP department for this host. Fleet couldn’t populate $FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT.")
|
||||
assert.Contains(t, updatedProfile.Detail, "There is no IdP department for this host. Fleet couldn't populate $FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT.")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -5381,7 +5381,7 @@ func TestPreprocessProfileContentsEndUserIDP(t *testing.T) {
|
|||
}
|
||||
},
|
||||
assert: func(output string) {
|
||||
assert.Contains(t, updatedProfile.Detail, fmt.Sprintf("There is no IdP full name for this host. Fleet couldn’t populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPFullname))
|
||||
assert.Contains(t, updatedProfile.Detail, fmt.Sprintf("There is no IdP full name for this host. Fleet couldn't populate $FLEET_VAR_%s.", fleet.FleetVarHostEndUserIDPFullname))
|
||||
assert.Len(t, targets, 0)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7547,13 +7547,13 @@ func (s *integrationMDMTestSuite) TestWindowsProfilesWithFleetVariables() {
|
|||
Name: "TestMixed",
|
||||
Contents: syncml.ForTestWithData([]syncml.TestCommand{
|
||||
{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/HostID", Data: "$FLEET_VAR_HOST_UUID"},
|
||||
{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/Email", Data: "$FLEET_VAR_HOST_END_USER_EMAIL_IDP"},
|
||||
{Verb: "Replace", LocURI: "./Device/Vendor/MSFT/DMClient/Provider/ProviderID/UserSCEP_/SCEP/Email", Data: "$FLEET_VAR_BOGUS"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
teamID: &tm.ID,
|
||||
wantStatus: http.StatusUnprocessableEntity,
|
||||
wantErrContains: "Fleet variable $FLEET_VAR_HOST_END_USER_EMAIL_IDP is not supported in Windows profiles",
|
||||
wantErrContains: "Fleet variable $FLEET_VAR_BOGUS is not supported in Windows profiles",
|
||||
},
|
||||
{
|
||||
name: "HOST_UUID variable accepted globally",
|
||||
|
|
|
|||
|
|
@ -1796,8 +1796,15 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
|
|||
|
||||
// fleetVarsSupportedInWindowsProfiles lists the Fleet variables that are
|
||||
// supported in Windows configuration profiles.
|
||||
// except prefix variables
|
||||
var fleetVarsSupportedInWindowsProfiles = []fleet.FleetVarName{
|
||||
fleet.FleetVarHostUUID,
|
||||
fleet.FleetVarSCEPWindowsCertificateID,
|
||||
fleet.FleetVarHostEndUserIDPUsername,
|
||||
fleet.FleetVarHostEndUserIDPUsernameLocalPart,
|
||||
fleet.FleetVarHostEndUserIDPFullname,
|
||||
fleet.FleetVarHostEndUserIDPDepartment,
|
||||
fleet.FleetVarHostEndUserIDPGroups,
|
||||
}
|
||||
|
||||
func validateWindowsProfileFleetVariables(contents string, lic *fleet.LicenseInfo) ([]string, error) {
|
||||
|
|
@ -1813,7 +1820,9 @@ func validateWindowsProfileFleetVariables(contents string, lic *fleet.LicenseInf
|
|||
|
||||
// Check if all found variables are supported
|
||||
for _, varName := range foundVars {
|
||||
if !slices.Contains(fleetVarsSupportedInWindowsProfiles, fleet.FleetVarName(varName)) {
|
||||
if !slices.Contains(fleetVarsSupportedInWindowsProfiles, fleet.FleetVarName(varName)) &&
|
||||
!strings.HasPrefix(varName, string(fleet.FleetVarCustomSCEPChallengePrefix)) &&
|
||||
!strings.HasPrefix(varName, string(fleet.FleetVarCustomSCEPProxyURLPrefix)) {
|
||||
return nil, fleet.NewInvalidArgumentError("profile", fmt.Sprintf("Fleet variable $FLEET_VAR_%s is not supported in Windows profiles.", varName))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1289,7 +1289,7 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
|
|||
{"mdm not enabled", 0, `<Replace></Replace>`, false, "Windows MDM isn't turned on."},
|
||||
{"duplicate profile name", 0, `<Replace>duplicate</Replace>`, true, "configuration profile with this name already exists"},
|
||||
{"multiple Replace", 0, `<Replace>a</Replace><Replace>b</Replace>`, true, ""},
|
||||
{"Replace and non-Replace", 0, `<Replace>a</Replace><Get>b</Get>`, true, "Windows configuration profiles can only have <Replace> or <Add> top level elements."},
|
||||
{"Replace and non-Replace", 0, `<Replace>a</Replace><Get>b</Get>`, true, "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements."},
|
||||
{
|
||||
"BitLocker profile", 0,
|
||||
`<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/BitLocker/AllowStandardUserEncryption</LocURI></Target></Item></Replace>`, true,
|
||||
|
|
@ -1305,7 +1305,7 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
|
|||
{"team mdm not enabled", 1, `<Replace></Replace>`, false, "Windows MDM isn't turned on."},
|
||||
{"team duplicate profile name", 1, `<Replace>duplicate</Replace>`, true, "configuration profile with this name already exists"},
|
||||
{"team multiple Replace", 1, `<Replace>a</Replace><Replace>b</Replace>`, true, ""},
|
||||
{"team Replace and non-Replace", 1, `<Replace>a</Replace><Get>b</Get>`, true, "Windows configuration profiles can only have <Replace> or <Add> top level elements."},
|
||||
{"team Replace and non-Replace", 1, `<Replace>a</Replace><Get>b</Get>`, true, "Windows configuration profiles can only have <Replace>, <Add> or <Exec> top level elements."},
|
||||
{
|
||||
"team BitLocker profile", 1,
|
||||
`<Replace><Item><Target><LocURI>./Device/Vendor/MSFT/BitLocker/AllowStandardUserEncryption</LocURI></Target></Item></Replace>`, true,
|
||||
|
|
@ -2500,11 +2500,11 @@ func TestValidateWindowsProfileFleetVariables(t *testing.T) {
|
|||
<Target>
|
||||
<LocURI>./Device/Vendor/MSFT/Policy/Config/System/AllowLocation</LocURI>
|
||||
</Target>
|
||||
<Data>$FLEET_VAR_HOST_UUID-$FLEET_VAR_HOST_END_USER_EMAIL_IDP</Data>
|
||||
<Data>$FLEET_VAR_HOST_UUID-$FLEET_VAR_BOGUS_VAR</Data>
|
||||
</Item>
|
||||
</Replace>`,
|
||||
wantErr: true,
|
||||
errContains: "Fleet variable $FLEET_VAR_HOST_END_USER_EMAIL_IDP is not supported in Windows profiles",
|
||||
errContains: "Fleet variable $FLEET_VAR_BOGUS_VAR is not supported in Windows profiles",
|
||||
},
|
||||
{
|
||||
name: "unknown Fleet variable",
|
||||
|
|
|
|||
|
|
@ -2438,6 +2438,14 @@ func buildCommandFromProfileBytes(profileBytes []byte, commandUUID string) (*fle
|
|||
}
|
||||
}
|
||||
|
||||
// generate a CmdID for any nested <Exec>
|
||||
for i := range cmd.ExecCommands {
|
||||
cmd.ExecCommands[i].CmdID = mdm_types.CmdID{
|
||||
Value: uuid.NewString(),
|
||||
IncludeFleetComment: true,
|
||||
}
|
||||
}
|
||||
|
||||
rawCommand, err := xml.Marshal(cmd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshalling command: %w", err)
|
||||
|
|
|
|||
|
|
@ -388,7 +388,7 @@ func TestBuildCommandFromProfileBytes(t *testing.T) {
|
|||
)
|
||||
|
||||
// build and generate a second command with the same syncml
|
||||
cmd, err = buildCommandFromProfileBytes(rawSyncML, "uuid-2")
|
||||
cmd, err = buildCommandFromProfileBytes(syncMLForTestWithExec("foo/bar"), "uuid-2")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "uuid-2", cmd.CommandUUID)
|
||||
require.Empty(t, cmd.TargetLocURI)
|
||||
|
|
@ -400,7 +400,7 @@ func TestBuildCommandFromProfileBytes(t *testing.T) {
|
|||
// generated xml contains additional comments about CmdID
|
||||
require.Equal(
|
||||
t,
|
||||
fmt.Sprintf(`<Atomic><!-- CmdID generated by Fleet --><CmdID>uuid-2</CmdID><Replace><!-- CmdID generated by Fleet --><CmdID>%s</CmdID><Item><Target><LocURI>foo/bar</LocURI></Target></Item></Replace><Add><!-- CmdID generated by Fleet --><CmdID>%s</CmdID><Item><Target><LocURI>foo/bar</LocURI></Target></Item></Add></Atomic>`, syncTwo.ReplaceCommands[0].CmdID.Value, syncTwo.AddCommands[0].CmdID.Value),
|
||||
fmt.Sprintf(`<Atomic><!-- CmdID generated by Fleet --><CmdID>uuid-2</CmdID><Replace><!-- CmdID generated by Fleet --><CmdID>%s</CmdID><Item><Target><LocURI>foo/bar</LocURI></Target></Item></Replace><Add><!-- CmdID generated by Fleet --><CmdID>%s</CmdID><Item><Target><LocURI>foo/bar</LocURI></Target></Item></Add><Exec><!-- CmdID generated by Fleet --><CmdID>%s</CmdID><Item><Target><LocURI>foo/bar</LocURI></Target></Item></Exec></Atomic>`, syncTwo.ReplaceCommands[0].CmdID.Value, syncTwo.AddCommands[0].CmdID.Value, syncTwo.ExecCommands[0].CmdID.Value),
|
||||
string(cmd.RawCommand),
|
||||
)
|
||||
|
||||
|
|
@ -426,6 +426,31 @@ func syncMLForTest(locURI string) []byte {
|
|||
</Replace>`, locURI, locURI))
|
||||
}
|
||||
|
||||
func syncMLForTestWithExec(locURI string) []byte {
|
||||
return []byte(fmt.Sprintf(`
|
||||
<Add>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>%s</LocURI>
|
||||
</Target>
|
||||
</Item>
|
||||
</Add>
|
||||
<Replace>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>%s</LocURI>
|
||||
</Target>
|
||||
</Item>
|
||||
</Replace>
|
||||
<Exec>
|
||||
<Item>
|
||||
<Target>
|
||||
<LocURI>%s</LocURI>
|
||||
</Target>
|
||||
</Item>
|
||||
</Exec>`, locURI, locURI, locURI))
|
||||
}
|
||||
|
||||
func TestReconcileWindowsProfilesWithFleetVariableError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ds := new(mock.Store)
|
||||
|
|
|
|||
Loading…
Reference in a new issue