2023-06-22 20:31:17 +00:00
package service
import (
2023-07-19 16:30:24 +00:00
"bytes"
2023-06-22 20:31:17 +00:00
"context"
2023-06-29 22:31:53 +00:00
"crypto/x509"
2024-06-28 15:09:22 +00:00
"database/sql"
2023-06-27 15:59:33 +00:00
"encoding/base64"
"encoding/json"
2023-06-22 20:31:17 +00:00
"encoding/xml"
"errors"
"fmt"
2023-07-20 14:54:04 +00:00
"html"
2023-06-22 20:31:17 +00:00
"io"
"net/http"
2023-07-19 16:30:24 +00:00
"net/url"
2023-10-06 22:04:33 +00:00
"regexp"
2023-06-22 20:31:17 +00:00
"strconv"
2023-07-20 14:54:04 +00:00
"strings"
2023-07-21 17:36:26 +00:00
"text/template"
2023-06-22 20:31:17 +00:00
"time"
2024-06-28 19:35:07 +00:00
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
2023-08-08 14:57:55 +00:00
"github.com/fleetdm/fleet/v4/server"
2023-06-22 20:31:17 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
2024-04-29 19:43:15 +00:00
mdmlifecycle "github.com/fleetdm/fleet/v4/server/mdm/lifecycle"
2023-11-30 12:17:07 +00:00
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
2024-12-04 22:19:18 +00:00
"github.com/fleetdm/fleet/v4/server/ptr"
2025-08-11 12:47:55 +00:00
"github.com/fleetdm/fleet/v4/server/variables"
2024-06-17 13:27:31 +00:00
kitlog "github.com/go-kit/log"
2023-11-01 14:13:12 +00:00
"github.com/go-kit/log/level"
2023-06-22 20:31:17 +00:00
mdm_types "github.com/fleetdm/fleet/v4/server/fleet"
"github.com/google/uuid"
)
2025-04-18 16:13:30 +00:00
const maxRequestLogSize = 10240
2023-06-27 15:59:33 +00:00
type SoapRequestContainer struct {
2023-07-19 16:30:24 +00:00
Data * fleet . SoapRequest
Params url . Values
Err error
2023-06-22 20:31:17 +00:00
}
2023-06-27 15:59:33 +00:00
// MDM SOAP request decoder
2023-11-01 14:13:12 +00:00
func ( req * SoapRequestContainer ) DecodeBody ( ctx context . Context , r io . Reader , u url . Values , c [ ] * x509 . Certificate ) error {
2023-06-27 15:59:33 +00:00
// Reading the request bytes
reqBytes , err := io . ReadAll ( r )
if err != nil {
return ctxerr . Wrap ( ctx , err , "reading soap mdm request" )
}
2023-07-19 16:30:24 +00:00
// Set the request parameters
req . Params = u
// Handle empty body scenario
2023-11-01 14:13:12 +00:00
req . Data = & fleet . SoapRequest { Raw : reqBytes }
2023-07-19 16:30:24 +00:00
if len ( reqBytes ) != 0 {
// Unmarshal the XML data from the request into the SoapRequest struct
err = xml . Unmarshal ( reqBytes , & req . Data )
if err != nil {
2025-04-18 16:13:30 +00:00
// We log the request body for debug by using an error implementing ErrWithInternal interface.
2025-07-22 21:24:19 +00:00
return ctxerr . Wrap ( ctx , & fleet . BadRequestError {
Message : "unmarshalling soap mdm request: " + err . Error ( ) ,
InternalErr : fmt . Errorf ( "request: %s" , truncateString ( string ( reqBytes ) , maxRequestLogSize ) ) ,
} )
2023-07-19 16:30:24 +00:00
}
2023-06-27 15:59:33 +00:00
}
2023-06-22 20:31:17 +00:00
return nil
}
2023-06-27 15:59:33 +00:00
type SoapResponseContainer struct {
Data * fleet . SoapResponse
Err error
}
2025-02-03 17:23:26 +00:00
func ( r SoapResponseContainer ) Error ( ) error { return r . Err }
2023-06-27 15:59:33 +00:00
2025-02-18 17:09:43 +00:00
// HijackRender writes the response header and the RAW HTML output
func ( r SoapResponseContainer ) HijackRender ( ctx context . Context , w http . ResponseWriter ) {
2023-06-27 15:59:33 +00:00
xmlRes , err := xml . MarshalIndent ( r . Data , "" , "\t" )
2023-06-22 20:31:17 +00:00
if err != nil {
2023-07-20 14:54:04 +00:00
logging . WithExtras ( ctx , "error with SoapResponseContainer" , err )
2023-06-22 20:31:17 +00:00
w . WriteHeader ( http . StatusBadRequest )
return
}
xmlRes = append ( xmlRes , '\n' )
2023-11-30 12:17:07 +00:00
w . Header ( ) . Set ( "Content-Type" , syncml . SoapContentType )
2023-06-22 20:31:17 +00:00
w . Header ( ) . Set ( "Content-Length" , strconv . Itoa ( len ( xmlRes ) ) )
w . WriteHeader ( http . StatusOK )
if n , err := w . Write ( xmlRes ) ; err != nil {
logging . WithExtras ( ctx , "err" , err , "written" , n )
}
}
2023-07-20 14:54:04 +00:00
type SyncMLReqMsgContainer struct {
2023-11-01 14:13:12 +00:00
Data * fleet . SyncML
2023-07-20 14:54:04 +00:00
Params url . Values
2023-11-01 14:13:12 +00:00
Certs [ ] * x509 . Certificate
2023-07-20 14:54:04 +00:00
Err error
}
// MDM SOAP request decoder
2023-11-01 14:13:12 +00:00
func ( req * SyncMLReqMsgContainer ) DecodeBody ( ctx context . Context , r io . Reader , u url . Values , c [ ] * x509 . Certificate ) error {
2023-07-20 14:54:04 +00:00
// Reading the request bytes
reqBytes , err := io . ReadAll ( r )
if err != nil {
return ctxerr . Wrap ( ctx , err , "reading SyncML message request" )
}
// Set the request parameters
req . Params = u
2023-11-01 14:13:12 +00:00
// Set the request certs
req . Certs = c
2023-07-20 14:54:04 +00:00
// Handle empty body scenario
2023-11-01 14:13:12 +00:00
req . Data = & fleet . SyncML { Raw : reqBytes }
2023-07-20 14:54:04 +00:00
if len ( reqBytes ) != 0 {
// Unmarshal the XML data from the request into the SoapRequest struct
err = xml . Unmarshal ( reqBytes , & req . Data )
if err != nil {
return ctxerr . Wrap ( ctx , err , "unmarshalling SyncML message request" )
}
}
return nil
}
type SyncMLResponseMsgContainer struct {
2023-11-01 14:13:12 +00:00
Data * fleet . SyncML
2023-07-20 14:54:04 +00:00
Err error
}
2025-02-03 17:23:26 +00:00
func ( r SyncMLResponseMsgContainer ) Error ( ) error { return r . Err }
2023-07-20 14:54:04 +00:00
2025-02-18 17:09:43 +00:00
// HijackRender writes the response header and the RAW HTML output
func ( r SyncMLResponseMsgContainer ) HijackRender ( ctx context . Context , w http . ResponseWriter ) {
2023-11-01 14:13:12 +00:00
xmlRes , err := xml . MarshalIndent ( r . Data , "" , "\t" )
if err != nil {
logging . WithExtras ( ctx , "error with SyncMLResponseMsgContainer" , err )
w . WriteHeader ( http . StatusInternalServerError )
return
}
xmlRes = append ( xmlRes , '\n' )
2023-07-20 14:54:04 +00:00
2023-11-30 12:17:07 +00:00
w . Header ( ) . Set ( "Content-Type" , syncml . SyncMLContentType )
2023-11-01 14:13:12 +00:00
w . Header ( ) . Set ( "Content-Length" , strconv . Itoa ( len ( xmlRes ) ) )
2023-07-20 14:54:04 +00:00
w . WriteHeader ( http . StatusOK )
2023-11-01 14:13:12 +00:00
if n , err := w . Write ( xmlRes ) ; err != nil {
2023-07-20 14:54:04 +00:00
logging . WithExtras ( ctx , "err" , err , "written" , n )
}
}
2023-07-21 17:36:26 +00:00
type MDMWebContainer struct {
Data * string
Params url . Values
Err error
}
// MDM SOAP request decoder
2023-11-01 14:13:12 +00:00
func ( req * MDMWebContainer ) DecodeBody ( ctx context . Context , r io . Reader , u url . Values , c [ ] * x509 . Certificate ) error {
2023-07-21 17:36:26 +00:00
reqBytes , err := io . ReadAll ( r )
if err != nil {
return ctxerr . Wrap ( ctx , err , "reading Webcontainer HTML message request" )
}
// Set the request parameters
req . Params = u
// Get req data
content := string ( reqBytes )
req . Data = & content
return nil
}
2025-02-03 17:23:26 +00:00
func ( req MDMWebContainer ) Error ( ) error { return req . Err }
2023-07-21 17:36:26 +00:00
2025-02-18 17:09:43 +00:00
// HijackRender writes the response header and the RAW HTML output
func ( req MDMWebContainer ) HijackRender ( ctx context . Context , w http . ResponseWriter ) {
2023-07-21 17:36:26 +00:00
resData := [ ] byte ( * req . Data + "\n" )
2023-11-30 12:17:07 +00:00
w . Header ( ) . Set ( "Content-Type" , syncml . WebContainerContentType )
2023-07-21 17:36:26 +00:00
w . Header ( ) . Set ( "Content-Length" , strconv . Itoa ( len ( resData ) ) )
w . WriteHeader ( http . StatusOK )
if n , err := w . Write ( resData ) ; err != nil {
logging . WithExtras ( ctx , "err" , err , "written" , n )
}
}
2023-07-19 16:30:24 +00:00
type MDMAuthContainer struct {
Data * string
Err error
}
2025-02-03 17:23:26 +00:00
func ( r MDMAuthContainer ) Error ( ) error { return r . Err }
2023-07-19 16:30:24 +00:00
2025-02-18 17:09:43 +00:00
// HijackRender writes the response header and the RAW XML output
func ( r MDMAuthContainer ) HijackRender ( ctx context . Context , w http . ResponseWriter ) {
2023-07-19 16:30:24 +00:00
w . Header ( ) . Set ( "Content-Type" , "text/html; charset=UTF-8" )
w . Header ( ) . Set ( "Content-Length" , strconv . Itoa ( len ( * r . Data ) ) )
w . WriteHeader ( http . StatusOK )
if n , err := w . Write ( [ ] byte ( * r . Data ) ) ; err != nil {
logging . WithExtras ( ctx , "err" , err , "written" , n )
}
}
2023-06-22 20:31:17 +00:00
// getUtcTime returns the current timestamp plus the specified number of minutes,
// formatted as "2006-01-02T15:04:05.000Z".
func getUtcTime ( minutes int ) string {
// Get the current time and then add the specified number of minutes
now := time . Now ( )
future := now . Add ( time . Duration ( minutes ) * time . Minute )
// Format and return the future time as a string
return future . UTC ( ) . Format ( "2006-01-02T15:04:05.000Z" )
}
// NewDiscoverResponse creates a new DiscoverResponse struct based on the auth policy, policy url, and enrollment url
2023-07-21 17:36:26 +00:00
func NewDiscoverResponse ( authPolicy string , policyUrl string , enrollmentUrl string ) ( mdm_types . DiscoverResponse , error ) {
2023-06-22 20:31:17 +00:00
if ( len ( authPolicy ) == 0 ) || ( len ( policyUrl ) == 0 ) || ( len ( enrollmentUrl ) == 0 ) {
return mdm_types . DiscoverResponse { } , errors . New ( "invalid parameters" )
}
return mdm_types . DiscoverResponse {
2023-11-30 12:17:07 +00:00
XMLNS : syncml . DiscoverNS ,
2023-06-22 20:31:17 +00:00
DiscoverResult : mdm_types . DiscoverResult {
AuthPolicy : authPolicy ,
2023-11-30 12:17:07 +00:00
EnrollmentVersion : syncml . EnrollmentVersionV4 ,
2023-06-22 20:31:17 +00:00
EnrollmentPolicyServiceUrl : policyUrl ,
EnrollmentServiceUrl : enrollmentUrl ,
} ,
} , nil
}
// NewGetPoliciesResponse creates a new GetPoliciesResponse struct based on the minimal key length, certificate validity period, and renewal period
func NewGetPoliciesResponse ( minimalKeyLength string , certificateValidityPeriodSeconds string , renewalPeriodSeconds string ) ( mdm_types . GetPoliciesResponse , error ) {
if ( len ( minimalKeyLength ) == 0 ) || ( len ( certificateValidityPeriodSeconds ) == 0 ) || ( len ( renewalPeriodSeconds ) == 0 ) {
return mdm_types . GetPoliciesResponse { } , errors . New ( "invalid parameters" )
}
return mdm_types . GetPoliciesResponse {
2023-11-30 12:17:07 +00:00
XMLNS : syncml . PolicyNS ,
2023-06-22 20:31:17 +00:00
Response : mdm_types . Response {
PolicyFriendlyName : mdm_types . ContentAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
XMLNS : syncml . EnrollXSI ,
2023-06-22 20:31:17 +00:00
} ,
NextUpdateHours : mdm_types . ContentAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
XMLNS : syncml . EnrollXSI ,
2023-06-22 20:31:17 +00:00
} ,
PoliciesNotChanged : mdm_types . ContentAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
XMLNS : syncml . EnrollXSI ,
2023-06-22 20:31:17 +00:00
} ,
Policies : mdm_types . Policies {
Policy : mdm_types . GPPolicy {
PolicyOIDReference : "0" ,
CAs : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
Attributes : mdm_types . Attributes {
CommonName : "FleetDMAttributes" ,
PolicySchema : "3" ,
HashAlgorithmOIDReference : "0" ,
Revision : mdm_types . Revision {
MajorRevision : "101" ,
MinorRevision : "0" ,
} ,
CertificateValidity : mdm_types . CertificateValidity {
ValidityPeriodSeconds : certificateValidityPeriodSeconds ,
RenewalPeriodSeconds : renewalPeriodSeconds ,
} ,
Permission : mdm_types . Permission {
Enroll : "true" ,
AutoEnroll : "false" ,
} ,
SupersededPolicies : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
PrivateKeyFlags : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
SubjectNameFlags : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
EnrollmentFlags : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
GeneralFlags : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
RARequirements : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
KeyArchivalAttributes : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
Extensions : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
PrivateKeyAttributes : mdm_types . PrivateKeyAttributes {
MinimalKeyLength : minimalKeyLength ,
KeySpec : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
KeyUsageProperty : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
Permissions : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
AlgorithmOIDReference : mdm_types . GenericAttr {
2023-11-30 12:17:07 +00:00
Xsi : syncml . DefaultStateXSI ,
2023-06-22 20:31:17 +00:00
} ,
2023-07-05 13:06:37 +00:00
CryptoProviders : [ ] mdm_types . ProviderAttr {
{ Content : "Microsoft Platform Crypto Provider" } ,
{ Content : "Microsoft Software Key Storage Provider" } ,
2023-06-22 20:31:17 +00:00
} ,
} ,
} ,
} ,
} ,
} ,
2023-07-05 13:06:37 +00:00
// These are MS-XCEP OIDs defined in section 3.1.4.1.3.16
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xcep/161aab9f-d159-4df3-85c9-f732ed2a8445
2023-06-22 20:31:17 +00:00
OIDs : mdm_types . OIDs {
2023-07-05 13:06:37 +00:00
OID : [ ] mdm_types . OID {
{
// SHA256WithRSA OID
// https://oidref.com/2.16.840.1.101.3.4.2.1
Value : "2.16.840.1.101.3.4.2.1" ,
Group : "4" ,
OIDReferenceID : "0" ,
DefaultName : "szOID_NIST_sha256" ,
} ,
{
// RSA OID
// https://oidref.com/1.2.840.113549.1.1.1
Value : "1.2.840.113549.1.1.1" ,
Group : "3" ,
OIDReferenceID : "1" ,
DefaultName : "szOID_RSA_RSA" ,
} ,
2023-06-22 20:31:17 +00:00
} ,
} ,
} , nil
}
// NewRequestSecurityTokenResponseCollection creates a new RequestSecurityTokenResponseCollection struct based on the provisioned token
func NewRequestSecurityTokenResponseCollection ( provisionedToken string ) ( mdm_types . RequestSecurityTokenResponseCollection , error ) {
if len ( provisionedToken ) == 0 {
return mdm_types . RequestSecurityTokenResponseCollection { } , errors . New ( "invalid parameters" )
}
2023-11-30 12:17:07 +00:00
enrollSecExtVal := syncml . EnrollSecExt
2023-06-22 20:31:17 +00:00
return mdm_types . RequestSecurityTokenResponseCollection {
2023-11-30 12:17:07 +00:00
XMLNS : syncml . EnrollWSTrust ,
2023-06-22 20:31:17 +00:00
RequestSecurityTokenResponse : mdm_types . RequestSecurityTokenResponse {
2023-11-30 12:17:07 +00:00
TokenType : syncml . EnrollTType ,
2023-06-22 20:31:17 +00:00
DispositionMessage : mdm_types . SecAttr {
Content : "" ,
2023-11-30 12:17:07 +00:00
XMLNS : syncml . EnrollReq ,
2023-06-22 20:31:17 +00:00
} ,
RequestID : mdm_types . SecAttr {
Content : "0" ,
2023-11-30 12:17:07 +00:00
XMLNS : syncml . EnrollReq ,
2023-06-22 20:31:17 +00:00
} ,
RequestedSecurityToken : mdm_types . RequestedSecurityToken {
BinarySecurityToken : mdm_types . BinarySecurityToken {
Content : provisionedToken ,
2023-06-27 15:59:33 +00:00
XMLNS : & enrollSecExtVal ,
2023-11-30 12:17:07 +00:00
ValueType : syncml . EnrollPDoc ,
EncodingType : syncml . EnrollEncode ,
2023-06-22 20:31:17 +00:00
} ,
} ,
} ,
} , nil
}
// NewSoapFault creates a new SoapFault struct based on the error type, original message type, and error message
func NewSoapFault ( errorType string , origMessage int , errorMessage error ) mdm_types . SoapFault {
return mdm_types . SoapFault {
OriginalMessageType : origMessage ,
Code : mdm_types . Code {
2023-11-30 12:17:07 +00:00
Value : syncml . SoapFaultRecv ,
2023-06-22 20:31:17 +00:00
Subcode : mdm_types . Subcode {
Value : errorType ,
} ,
} ,
Reason : mdm_types . Reason {
Text : mdm_types . ReasonText {
Content : errorMessage . Error ( ) ,
2023-11-30 12:17:07 +00:00
Lang : syncml . SoapFaultLocale ,
2023-06-22 20:31:17 +00:00
} ,
} ,
}
}
2023-07-19 16:30:24 +00:00
// getSTSAuthContent Retuns STS auth content
2025-02-14 22:19:34 +00:00
func getSTSAuthContent ( data string ) mdm_types . Errorer {
2023-07-19 16:30:24 +00:00
return MDMAuthContainer {
Data : & data ,
Err : nil ,
}
}
2023-06-22 20:31:17 +00:00
// getSoapResponseFault Returns a SoapResponse with a SoapFault on its body
2025-02-14 22:19:34 +00:00
func getSoapResponseFault ( relatesTo string , soapFault * mdm_types . SoapFault ) mdm_types . Errorer {
2023-06-22 20:31:17 +00:00
if len ( relatesTo ) == 0 {
relatesTo = "invalid_message_id"
}
2023-06-27 15:59:33 +00:00
response , _ := NewSoapResponse ( soapFault , relatesTo )
return SoapResponseContainer {
Data : & response ,
Err : nil ,
}
2023-06-22 20:31:17 +00:00
}
// NewSoapResponse creates a new SoapRequest struct based on the message type and the message content
2023-06-27 15:59:33 +00:00
func NewSoapResponse ( payload interface { } , relatesTo string ) ( fleet . SoapResponse , error ) {
2023-06-22 20:31:17 +00:00
// Sanity check
if len ( relatesTo ) == 0 {
2023-06-27 15:59:33 +00:00
return fleet . SoapResponse { } , errors . New ( "relatesTo is invalid" )
2023-06-22 20:31:17 +00:00
}
// Useful constants
// Some of these are string urls to be assigned to pointers - they need to have a type and cannot be const literals
var (
2023-11-30 12:17:07 +00:00
urlNSS = syncml . EnrollNSS
urlNSA = syncml . EnrollNSA
urlXSI = syncml . EnrollXSI
urlXSD = syncml . EnrollXSD
urlXSU = syncml . EnrollXSU
urlDiag = syncml . ActionNsDiag
urlDiscovery = syncml . ActionNsDiscovery
urlPolicy = syncml . ActionNsPolicy
urlEnroll = syncml . ActionNsEnroll
urlSecExt = syncml . EnrollSecExt
2023-06-22 20:31:17 +00:00
MUValue = "1"
timestampID = "_0"
secWindowStartTimeMin = - 5
secWindowEndTimeMin = 5
)
// string pointers - they need to be pointers to not be marshalled into the XML when nil
var (
headerXsu * string
action string
activityID * mdm_types . ActivityId
security * mdm_types . WsSecurity
)
// Build the response body
var body mdm_types . BodyResponse
// Set the message specific fields based on the message type
switch msg := payload . ( type ) {
case * mdm_types . DiscoverResponse :
action = urlDiscovery
uuid := uuid . New ( ) . String ( )
activityID = & mdm_types . ActivityId {
Content : uuid ,
CorrelationId : uuid ,
2023-06-27 15:59:33 +00:00
XMLNS : urlDiag ,
2023-06-22 20:31:17 +00:00
}
body . DiscoverResponse = msg
case * mdm_types . GetPoliciesResponse :
action = urlPolicy
headerXsu = & urlXSU
body . Xsi = & urlXSI
body . Xsd = & urlXSD
body . GetPoliciesResponse = msg
case * mdm_types . RequestSecurityTokenResponseCollection :
action = urlEnroll
headerXsu = & urlXSU
security = & mdm_types . WsSecurity {
MustUnderstand : MUValue ,
2023-06-27 15:59:33 +00:00
XMLNS : urlSecExt ,
2023-06-22 20:31:17 +00:00
Timestamp : mdm_types . Timestamp {
ID : timestampID ,
Created : getUtcTime ( secWindowStartTimeMin ) , // minutes ago
Expires : getUtcTime ( secWindowEndTimeMin ) , // minutes from now
} ,
}
body . RequestSecurityTokenResponseCollection = msg
// Setting the target action
case * mdm_types . SoapFault :
2024-10-18 17:38:26 +00:00
if msg . OriginalMessageType == mdm_types . MDEDiscovery { //nolint:gocritic // ignore ifElseChain
2023-06-22 20:31:17 +00:00
action = urlDiscovery
} else if msg . OriginalMessageType == mdm_types . MDEPolicy {
action = urlPolicy
} else if msg . OriginalMessageType == mdm_types . MDEEnrollment {
action = urlEnroll
} else {
action = urlDiag
}
uuid := uuid . New ( ) . String ( )
activityID = & mdm_types . ActivityId {
Content : uuid ,
CorrelationId : uuid ,
2023-06-27 15:59:33 +00:00
XMLNS : urlDiag ,
2023-06-22 20:31:17 +00:00
}
body . SoapFault = msg
default :
2023-06-27 15:59:33 +00:00
return fleet . SoapResponse { } , errors . New ( "mdm response message not supported" )
2023-06-22 20:31:17 +00:00
}
// Return the SoapRequest type with the appropriate fields set
2023-06-27 15:59:33 +00:00
return fleet . SoapResponse {
XMLNSS : urlNSS ,
XMLNSA : urlNSA ,
XMLNSU : headerXsu ,
2023-06-22 20:31:17 +00:00
Header : mdm_types . ResponseHeader {
Action : mdm_types . Action {
Content : action ,
MustUnderstand : MUValue ,
} ,
RelatesTo : relatesTo ,
ActivityId : activityID ,
Security : security ,
} ,
Body : body ,
} , nil
}
2023-06-27 15:59:33 +00:00
// NewBinarySecurityTokenPayload returns the BinarySecurityTokenPayload type
func NewBinarySecurityTokenPayload ( encodedToken string ) ( fleet . WindowsMDMAccessTokenPayload , error ) {
if len ( encodedToken ) == 0 {
return fleet . WindowsMDMAccessTokenPayload { } , errors . New ( "binary security token: token is empty" )
}
rawBytes , err := base64 . StdEncoding . DecodeString ( encodedToken )
2023-06-22 20:31:17 +00:00
if err != nil {
2023-06-27 15:59:33 +00:00
return fleet . WindowsMDMAccessTokenPayload { } , fmt . Errorf ( "binary security token: %v" , err )
2023-06-22 20:31:17 +00:00
}
2023-06-27 15:59:33 +00:00
var tokenPayload fleet . WindowsMDMAccessTokenPayload
err = json . Unmarshal ( rawBytes , & tokenPayload )
2023-06-22 20:31:17 +00:00
if err != nil {
2023-06-27 15:59:33 +00:00
return fleet . WindowsMDMAccessTokenPayload { } , fmt . Errorf ( "binary security token: %v" , err )
2023-06-22 20:31:17 +00:00
}
2023-06-27 15:59:33 +00:00
return tokenPayload , nil
}
2023-07-05 13:06:37 +00:00
// newParm returns a new ProvisioningDoc Parameter
func newParm ( name , value , datatype string ) mdm_types . Param {
return mdm_types . Param {
Name : name ,
Value : value ,
Datatype : datatype ,
}
}
// newCharacteristic returns a new ProvisioningDoc Characteristic
func newCharacteristic ( typ string , parms [ ] mdm_types . Param , characteristics [ ] mdm_types . Characteristic ) mdm_types . Characteristic {
return mdm_types . Characteristic {
Type : typ ,
Params : parms ,
Characteristics : characteristics ,
}
}
// NewProvisioningDoc returns a new ProvisioningDoc container
// NewCertStoreProvisioningData returns a new CertStoreProvisioningData Characteristic
// The enrollment client installs the client certificate, as well as the trusted root certificate and intermediate certificates.
// The provisioning information in NewCertStoreProvisioningData includes various properties that the device management client uses to communicate with the MDM Server.
// identityFingerprint is the fingerprint of the identity certificate
// identityCert is the identity certificate bytes
// signedClientFingerprint is the fingerprint of the signed client certificate
// signedClientCert is the signed client certificate bytes
func NewCertStoreProvisioningData ( enrollmentType string , identityFingerprint string , identityCert [ ] byte , signedClientFingerprint string , signedClientCert [ ] byte ) mdm_types . Characteristic {
// Target Cert Store selection based on Enrollment type
targetCertStore := "User"
if enrollmentType == "Device" {
targetCertStore = "System"
}
root := newCharacteristic ( "Root" , nil , [ ] mdm_types . Characteristic {
newCharacteristic ( "System" , nil , [ ] mdm_types . Characteristic {
newCharacteristic ( identityFingerprint , [ ] mdm_types . Param {
newParm ( "EncodedCertificate" , base64 . StdEncoding . EncodeToString ( identityCert ) , "" ) ,
} , nil ) ,
} ) ,
} )
my := newCharacteristic ( "My" , nil , [ ] mdm_types . Characteristic {
newCharacteristic ( targetCertStore , nil , [ ] mdm_types . Characteristic {
newCharacteristic ( signedClientFingerprint , [ ] mdm_types . Param {
newParm ( "EncodedCertificate" , base64 . StdEncoding . EncodeToString ( signedClientCert ) , "" ) ,
} , nil ) ,
newCharacteristic ( "PrivateKeyContainer" , nil , nil ) ,
} ) ,
newCharacteristic ( "WSTEP" , nil , [ ] mdm_types . Characteristic {
newCharacteristic ( "Renew" , [ ] mdm_types . Param {
2023-11-30 12:17:07 +00:00
newParm ( "ROBOSupport" , syncml . WstepROBOSupport , "boolean" ) ,
newParm ( "RenewPeriod" , syncml . WstepCertRenewalPeriodInDays , "integer" ) ,
newParm ( "RetryInterval" , syncml . WstepRenewRetryInterval , "integer" ) ,
2023-07-05 13:06:37 +00:00
} , nil ) ,
} ) ,
} )
certStore := newCharacteristic ( "CertificateStore" , nil , [ ] mdm_types . Characteristic { root , my } )
return certStore
}
2024-12-02 14:14:10 +00:00
// isEligibleForWindowsMDMEnrollment returns true if the host can be enrolled
2024-06-28 15:09:22 +00:00
// in Fleet's Windows MDM (if it was enabled).
2024-12-02 14:14:10 +00:00
func isEligibleForWindowsMDMEnrollment ( host * fleet . Host , mdmInfo * fleet . HostMDM ) bool {
2024-06-28 15:09:22 +00:00
return host . FleetPlatform ( ) == "windows" &&
host . IsOsqueryEnrolled ( ) &&
( mdmInfo == nil || ( ! mdmInfo . IsServer && ! mdmInfo . Enrolled ) )
}
2024-12-02 14:14:10 +00:00
// isEligibleForWindowsMDMMigration returns true if the host can be migrated to
// Fleet's Windows MDM (if it was enabled).
func isEligibleForWindowsMDMMigration ( host * fleet . Host , mdmInfo * fleet . HostMDM ) bool {
return host . FleetPlatform ( ) == "windows" &&
host . IsOsqueryEnrolled ( ) &&
( mdmInfo != nil && ! mdmInfo . IsServer && mdmInfo . Enrolled && mdmInfo . Name != fleet . WellKnownMDMFleet )
}
2023-07-05 13:06:37 +00:00
// NewApplicationProvisioningData returns a new ApplicationProvisioningData Characteristic
// The Application Provisioning configuration is used for bootstrapping a device with an OMA DM account
// The paramenters here maps to the W7 application CSP
// https://learn.microsoft.com/en-us/windows/client-management/mdm/w7-application-csp
func NewApplicationProvisioningData ( mdmEndpoint string ) mdm_types . Characteristic {
provDoc := newCharacteristic ( "APPLICATION" , [ ] mdm_types . Param {
// The PROVIDER-ID parameter specifies the server identifier for a management server used in the current management session
2023-11-30 12:17:07 +00:00
newParm ( "PROVIDER-ID" , syncml . DocProvisioningAppProviderID , "" ) ,
2023-07-05 13:06:37 +00:00
// The APPID parameter is used to differentiate the types of available application services and protocols.
newParm ( "APPID" , "w7" , "" ) ,
// The NAME parameter is used in the APPLICATION characteristic to specify a user readable application identity.
2023-11-30 12:17:07 +00:00
newParm ( "NAME" , syncml . DocProvisioningAppName , "" ) ,
2023-07-05 13:06:37 +00:00
// The ADDR parameter is used in the APPADDR param to get or set the address of the OMA DM server.
newParm ( "ADDR" , mdmEndpoint , "" ) ,
// The ROLE parameter is used in the APPLICATION characteristic to specify the security application chamber that the DM session should run with when communicating with the DM server.
// The BACKCOMPATRETRYFREQ parameter is used to specify how many retries the DM client performs when there are Connection Manager-level or WinInet-level errors
2023-11-30 12:17:07 +00:00
newParm ( "CONNRETRYFREQ" , syncml . DocProvisioningAppConnRetryFreq , "" ) ,
2023-07-05 13:06:37 +00:00
// The INITIALBACKOFFTIME parameter is used to specify the initial wait time in milliseconds when the DM client retries for the first time
2023-11-30 12:17:07 +00:00
newParm ( "INITIALBACKOFFTIME" , syncml . DocProvisioningAppInitialBackoffTime , "" ) ,
2023-07-05 13:06:37 +00:00
// The MAXBACKOFFTIME parameter is used to specify the maximum number of milliseconds to sleep after package-sending failure
2023-11-30 12:17:07 +00:00
newParm ( "MAXBACKOFFTIME" , syncml . DocProvisioningAppMaxBackoffTime , "" ) ,
2023-07-05 13:06:37 +00:00
// The DEFAULTENCODING parameter is used to specify whether the DM client should use WBXML or XML for the DM package when communicating with the server.
newParm ( "DEFAULTENCODING" , "application/vnd.syncml.dm+xml" , "" ) ,
// The BACKCOMPATRETRYDISABLED parameter is used to specify whether to retry resending a package with an older protocol version
newParm ( "BACKCOMPATRETRYDISABLED" , "" , "" ) ,
} , [ ] mdm_types . Characteristic {
// CLIENT specifies that the server authenticates itself to the OMA DM Client at the DM protocol level.
newCharacteristic ( "APPAUTH" , [ ] mdm_types . Param {
newParm ( "AAUTHLEVEL" , "CLIENT" , "" ) ,
// DIGEST - Specifies that the SyncML DM 'syncml:auth-md5' authentication type.
newParm ( "AAUTHTYPE" , "DIGEST" , "" ) ,
newParm ( "AAUTHSECRET" , "dummy" , "" ) ,
newParm ( "AAUTHDATA" , "nonce" , "" ) ,
} , nil ) ,
// APPSRV specifies that the client authenticates itself to the OMA DM Server at the DM protocol level.
newCharacteristic ( "APPAUTH" , [ ] mdm_types . Param {
newParm ( "AAUTHLEVEL" , "APPSRV" , "" ) ,
// DIGEST - Specifies that the SyncML DM 'syncml:auth-md5' authentication type.
newParm ( "AAUTHTYPE" , "DIGEST" , "" ) ,
newParm ( "AAUTHNAME" , "dummy" , "" ) ,
newParm ( "AAUTHSECRET" , "dummy" , "" ) ,
newParm ( "AAUTHDATA" , "nonce" , "" ) ,
} , nil ) ,
} )
return provDoc
}
// NewDMClientProvisioningData returns a new DMClient Characteristic
// These settings can be used to define different aspects of the DM client behavior
// The provisioning information in NewCertStoreProvisioningData includes various properties that the device management client uses to communicate with the MDM Server.
// c2DeviceName is the device name used by the IT admin console
// listOfMSIAppToInstall contains a list of LocURIs that expected to be provision via EnterpriseDesktopAppManagement CSP
func NewDMClientProvisioningData ( ) mdm_types . Characteristic {
dmClient := newCharacteristic ( "DMClient" , nil , [ ] mdm_types . Characteristic {
newCharacteristic ( "Provider" , nil , [ ] mdm_types . Characteristic {
2023-11-30 12:17:07 +00:00
newCharacteristic ( syncml . DocProvisioningAppProviderID ,
2023-07-05 13:06:37 +00:00
[ ] mdm_types . Param { } , [ ] mdm_types . Characteristic {
newCharacteristic ( "Poll" , [ ] mdm_types . Param {
2024-01-16 14:05:19 +00:00
// AllUsersPollOnFirstLogin - enabled
// https://learn.microsoft.com/en-us/windows/client-management/mdm/dmclient-csp#deviceproviderprovideridpollalluserspollonfirstlogin
newParm ( "AllUsersPollOnFirstLogin" , "true" , syncml . DmClientBoolType ) ,
// PollOnLogin - enabled
// https://learn.microsoft.com/en-us/windows/client-management/mdm/dmclient-csp#deviceproviderprovideridpollpollonlogin
newParm ( "PollOnLogin" , "true" , syncml . DmClientBoolType ) ,
// NumberOfFirstRetries - 0 (meaning repeat infinitely, Second and Remaining retries will not be used)
// https://learn.microsoft.com/en-us/windows/client-management/mdm/dmclient-csp#deviceproviderprovideridpollnumberoffirstretries
//
// Note that the docs do mention:
//
// The total time for first set of retries shouldn't be more than
// a few hours. The server shouldn't set NumberOfFirstRetries to
// be 0. RemainingScheduledRetries is used for the long run
// device polling schedule.
//
// but we really want to keep polling regularly at short intervals
// and it seems like the way to do it (and they do support infinite
// retries, so...).
newParm ( "NumberOfFirstRetries" , "0" , syncml . DmClientIntType ) ,
// IntervalForFirstSetOfRetries - 1 minute (we can't go lower than that)
// https://learn.microsoft.com/en-us/windows/client-management/mdm/dmclient-csp#deviceproviderprovideridpollintervalforfirstsetofretries
newParm ( "IntervalForFirstSetOfRetries" , "1" , syncml . DmClientIntType ) ,
// Second and Remaining retries are disabled (0).
newParm ( "NumberOfSecondRetries" , "0" , syncml . DmClientIntType ) ,
newParm ( "IntervalForSecondSetOfRetries" , "0" , syncml . DmClientIntType ) ,
newParm ( "NumberOfRemainingScheduledRetries" , "0" , syncml . DmClientIntType ) ,
newParm ( "IntervalForRemainingScheduledRetries" , "0" , syncml . DmClientIntType ) ,
2023-07-05 13:06:37 +00:00
} , nil ) ,
} ) ,
} ) ,
} )
return dmClient
}
// NewProvisioningDoc returns a new ProvisioningDoc container
func NewProvisioningDoc ( certStoreData mdm_types . Characteristic , applicationData mdm_types . Characteristic , dmClientData mdm_types . Characteristic ) mdm_types . WapProvisioningDoc {
return mdm_types . WapProvisioningDoc {
2023-11-30 12:17:07 +00:00
Version : syncml . DocProvisioningVersion ,
2023-07-05 13:06:37 +00:00
Characteristics : [ ] mdm_types . Characteristic {
certStoreData ,
applicationData ,
dmClientData ,
} ,
}
}
2023-06-27 15:59:33 +00:00
// mdmMicrosoftDiscoveryEndpoint handles the Discovery message and returns a valid DiscoveryResponse message
2023-07-05 13:06:37 +00:00
// DiscoverResponse message contains the Uniform Resource Locators (URLs) of service endpoints required for the following enrollment steps
2025-02-14 22:19:34 +00:00
func mdmMicrosoftDiscoveryEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( mdm_types . Errorer , error ) {
2023-06-27 15:59:33 +00:00
req := request . ( * SoapRequestContainer ) . Data
2023-06-22 20:31:17 +00:00
2025-08-07 15:05:15 +00:00
// Process the discovery request using the Service method which handles validation, logging, and response generation
response , err := svc . ProcessMDMMicrosoftDiscovery ( ctx , req )
2023-06-22 20:31:17 +00:00
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEDiscovery , err )
2023-06-27 15:59:33 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
2023-06-22 20:31:17 +00:00
}
2023-06-27 15:59:33 +00:00
return SoapResponseContainer {
2025-08-07 15:05:15 +00:00
Data : response ,
2023-06-27 15:59:33 +00:00
Err : nil ,
} , nil
}
2023-07-19 16:30:24 +00:00
// mdmMicrosoftAuthEndpoint handles the Security Token Service (STS) implementation
2025-02-14 22:19:34 +00:00
func mdmMicrosoftAuthEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( mdm_types . Errorer , error ) {
2023-07-19 16:30:24 +00:00
params := request . ( * SoapRequestContainer ) . Params
// Sanity check on the expected query params
2023-11-30 12:17:07 +00:00
if ! params . Has ( syncml . STSAuthAppRu ) || ! params . Has ( syncml . STSLoginHint ) {
2023-07-19 16:30:24 +00:00
return getSTSAuthContent ( "" ) , errors . New ( "expected STS params are not present" )
}
2023-11-30 12:17:07 +00:00
appru := params . Get ( syncml . STSAuthAppRu )
loginHint := params . Get ( syncml . STSLoginHint )
2023-07-19 16:30:24 +00:00
if ( len ( appru ) == 0 ) || ( len ( loginHint ) == 0 ) {
return getSTSAuthContent ( "" ) , errors . New ( "expected STS params are empty" )
}
// Getting the STS endpoint HTML content
stsAuthContent , err := svc . GetMDMMicrosoftSTSAuthResponse ( ctx , appru , loginHint )
if err != nil {
return getSTSAuthContent ( "" ) , errors . New ( "error generating STS content" )
}
return getSTSAuthContent ( stsAuthContent ) , nil
}
2023-06-27 15:59:33 +00:00
// mdmMicrosoftPolicyEndpoint handles the GetPolicies message and returns a valid GetPoliciesResponse message
2023-07-05 13:06:37 +00:00
// GetPoliciesResponse message contains the certificate policies required for the next enrollment step. For more information about these messages, see [MS-XCEP] sections 3.1.4.1.1.1 and 3.1.4.1.1.2.
2025-02-14 22:19:34 +00:00
func mdmMicrosoftPolicyEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( mdm_types . Errorer , error ) {
2023-06-27 15:59:33 +00:00
req := request . ( * SoapRequestContainer ) . Data
// Checking first if GetPolicies message is valid and returning error if this is not the case
if err := req . IsValidGetPolicyMsg ( ) ; err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEPolicy , err )
2023-06-27 15:59:33 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
// Binary security token should be extracted to ensure this is a valid call
2023-07-19 16:30:24 +00:00
hdrSecToken , err := req . GetHeaderBinarySecurityToken ( )
2023-06-27 15:59:33 +00:00
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEPolicy , err )
2023-06-27 15:59:33 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
// Getting the GetPoliciesResponse message
2023-07-19 16:30:24 +00:00
policyResponseMsg , err := svc . GetMDMWindowsPolicyResponse ( ctx , hdrSecToken )
2023-06-27 15:59:33 +00:00
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEPolicy , err )
2023-06-27 15:59:33 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
// Embedding the DiscoveryResponse message inside of a SoapResponse
2023-07-05 13:06:37 +00:00
response , err := NewSoapResponse ( policyResponseMsg , req . GetMessageID ( ) )
2023-06-27 15:59:33 +00:00
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEPolicy , err )
2023-07-05 13:06:37 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
return SoapResponseContainer {
Data : & response ,
Err : nil ,
} , nil
}
// mdmMicrosoftEnrollEndpoint handles the RequestSecurityToken message and returns a valid RequestSecurityTokenResponseCollection message
// RequestSecurityTokenResponseCollection message contains the identity and provisioning information for the device management client.
2025-02-14 22:19:34 +00:00
func mdmMicrosoftEnrollEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( mdm_types . Errorer , error ) {
2023-07-05 13:06:37 +00:00
req := request . ( * SoapRequestContainer ) . Data
// Checking first if RequestSecurityToken message is valid and returning error if this is not the case
if err := req . IsValidRequestSecurityTokenMsg ( ) ; err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEEnrollment , err )
2023-07-05 13:06:37 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
// Getting the RequestSecurityToken message from the SOAP request
reqSecurityTokenMsg , err := req . GetRequestSecurityTokenMessage ( )
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEEnrollment , err )
2023-07-05 13:06:37 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
// Binary security token should be extracted to ensure this is a valid call
2023-07-19 16:30:24 +00:00
hdrBinarySecToken , err := req . GetHeaderBinarySecurityToken ( )
2023-07-05 13:06:37 +00:00
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEEnrollment , err )
2023-07-05 13:06:37 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
// Getting the RequestSecurityTokenResponseCollection message
2023-07-19 16:30:24 +00:00
enrollResponseMsg , err := svc . GetMDMWindowsEnrollResponse ( ctx , reqSecurityTokenMsg , hdrBinarySecToken )
2023-07-05 13:06:37 +00:00
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEEnrollment , err )
2023-07-05 13:06:37 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
// Embedding the DiscoveryResponse message inside of a SoapResponse
response , err := NewSoapResponse ( enrollResponseMsg , req . GetMessageID ( ) )
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEEnrollment , err )
2023-06-27 15:59:33 +00:00
return getSoapResponseFault ( req . GetMessageID ( ) , soapFault ) , nil
}
return SoapResponseContainer {
Data : & response ,
Err : nil ,
} , nil
}
2023-07-20 14:54:04 +00:00
// mdmMicrosoftManagementEndpoint handles the OMA DM management sessions
// It receives a SyncML message with protocol commands, it process the commands and responds with a
// SyncML message with protocol commands results and more protocol commands for the calling host
// Note: This logic needs to be improved with better SyncML message parsing, better message tracking
// and better security authentication (done through TLS and in-message hash)
2025-02-14 22:19:34 +00:00
func mdmMicrosoftManagementEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( mdm_types . Errorer , error ) {
2023-07-20 14:54:04 +00:00
reqSyncML := request . ( * SyncMLReqMsgContainer ) . Data
// Checking first if incoming SyncML message is valid and returning error if this is not the case
2023-11-01 14:13:12 +00:00
if err := reqSyncML . IsValidMsg ( ) ; err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MSMDM , err )
2023-11-01 14:13:12 +00:00
return getSoapResponseFault ( reqSyncML . SyncHdr . MsgID , soapFault ) , nil
2023-07-20 14:54:04 +00:00
}
2023-11-01 14:13:12 +00:00
// Getting the MS-MDM response message
2025-01-20 15:12:33 +00:00
resSyncML , err := svc . GetMDMWindowsManagementResponse ( ctx , reqSyncML , request . ( * SyncMLReqMsgContainer ) . Certs )
2023-07-20 14:54:04 +00:00
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MSMDM , err )
2023-11-01 14:13:12 +00:00
return getSoapResponseFault ( reqSyncML . SyncHdr . MsgID , soapFault ) , nil
2023-07-20 14:54:04 +00:00
}
return SyncMLResponseMsgContainer {
Data : resSyncML ,
Err : nil ,
} , nil
}
2023-07-21 17:36:26 +00:00
// mdmMicrosoftTOSEndpoint handles the TOS content for the incoming MDM enrollment request
2025-02-14 22:19:34 +00:00
func mdmMicrosoftTOSEndpoint ( ctx context . Context , request interface { } , svc fleet . Service ) ( mdm_types . Errorer , error ) {
2023-07-21 17:36:26 +00:00
params := request . ( * MDMWebContainer ) . Params
// Sanity check on the expected query params
2023-11-30 12:17:07 +00:00
if ! params . Has ( syncml . TOCRedirectURI ) || ! params . Has ( syncml . TOCReqID ) {
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEEnrollment , errors . New ( "invalid params" ) )
return getSoapResponseFault ( syncml . SoapErrorInternalServiceFault , soapFault ) , nil
2023-07-21 17:36:26 +00:00
}
2023-11-30 12:17:07 +00:00
redirectURI := params . Get ( syncml . TOCRedirectURI )
reqID := params . Get ( syncml . TOCReqID )
2023-07-21 17:36:26 +00:00
// Getting the TOS content message
resTOCData , err := svc . GetMDMWindowsTOSContent ( ctx , redirectURI , reqID )
if err != nil {
2023-11-30 12:17:07 +00:00
soapFault := svc . GetAuthorizedSoapFault ( ctx , syncml . SoapErrorMessageFormat , mdm_types . MDEEnrollment , err )
return getSoapResponseFault ( syncml . SoapErrorInternalServiceFault , soapFault ) , nil
2023-07-21 17:36:26 +00:00
}
return MDMWebContainer {
Data : & resTOCData ,
Err : nil ,
} , nil
}
2023-11-01 14:13:12 +00:00
// authBinarySecurityToken checks if the provided token is valid. For programmatic enrollment, it
// returns the orbit node key and host uuid. For automatic enrollment, it returns only the UPN (the
// host uuid will be an empty string).
func ( svc * Service ) authBinarySecurityToken ( ctx context . Context , authToken * fleet . HeaderBinarySecurityToken ) ( claim string , hostUUID string , err error ) {
2023-07-19 16:30:24 +00:00
if authToken == nil {
2023-11-01 14:13:12 +00:00
return "" , "" , errors . New ( "authToken is empty" )
2023-06-27 15:59:33 +00:00
}
2023-11-01 14:13:12 +00:00
err = authToken . IsValidToken ( )
2023-06-27 15:59:33 +00:00
if err != nil {
2023-11-01 14:13:12 +00:00
return "" , "" , errors . New ( "authToken is not valid" )
2023-06-27 15:59:33 +00:00
}
2023-07-19 16:30:24 +00:00
// Tokens that were generated by enrollment client
if authToken . IsDeviceToken ( ) {
2023-06-27 15:59:33 +00:00
2023-07-19 16:30:24 +00:00
// Getting the Binary Security Token Payload
binSecToken , err := NewBinarySecurityTokenPayload ( authToken . Content )
2023-06-27 15:59:33 +00:00
if err != nil {
2023-11-01 14:13:12 +00:00
return "" , "" , fmt . Errorf ( "token creation error %v" , err )
2023-06-27 15:59:33 +00:00
}
2023-07-19 16:30:24 +00:00
// Validating the Binary Security Token Payload
err = binSecToken . IsValidToken ( )
if err != nil {
2023-11-01 14:13:12 +00:00
return "" , "" , fmt . Errorf ( "invalid token data %v" , err )
2023-07-19 16:30:24 +00:00
}
// Validating the Binary Security Token Type used on Programmatic Enrollments
if binSecToken . Type == mdm_types . WindowsMDMProgrammaticEnrollmentType {
2023-08-29 13:50:13 +00:00
host , err := svc . ds . LoadHostByOrbitNodeKey ( ctx , binSecToken . Payload . OrbitNodeKey )
2023-07-19 16:30:24 +00:00
if err != nil {
2023-11-01 14:13:12 +00:00
return "" , "" , fmt . Errorf ( "host data cannot be found %v" , err )
2023-07-19 16:30:24 +00:00
}
2024-06-28 15:09:22 +00:00
mdmInfo , err := svc . ds . GetHostMDM ( ctx , host . ID )
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return "" , "" , errors . New ( "unable to retrieve host mdm info" )
}
2023-07-19 16:30:24 +00:00
// This ensures that only hosts that are eligible for Windows enrollment can be enrolled
2024-12-02 14:14:10 +00:00
if ! isEligibleForWindowsMDMEnrollment ( host , mdmInfo ) {
2023-11-01 14:13:12 +00:00
return "" , "" , errors . New ( "host is not elegible for Windows MDM enrollment" )
2023-07-19 16:30:24 +00:00
}
// No errors, token is authorized
2023-11-01 14:13:12 +00:00
return binSecToken . Payload . OrbitNodeKey , host . UUID , nil
2023-07-19 16:30:24 +00:00
}
// Validating the Binary Security Token Type used on Automatic Enrollments (returned by STS Auth Endpoint)
if binSecToken . Type == mdm_types . WindowsMDMAutomaticEnrollmentType {
upnToken , err := svc . wstepCertManager . GetSTSAuthTokenUPNClaim ( binSecToken . Payload . AuthToken )
if err != nil {
2023-11-01 14:13:12 +00:00
return "" , "" , ctxerr . Wrap ( ctx , err , "issue retrieving UPN from Auth token" )
2023-07-19 16:30:24 +00:00
}
// No errors, token is authorized
2023-11-01 14:13:12 +00:00
return upnToken , "" , nil
2023-07-19 16:30:24 +00:00
}
}
// Validating the Binary Security Token Type used on Automatic Enrollments
if authToken . IsAzureJWTToken ( ) {
// Validate the JWT Auth token by retreving its claims
2023-11-30 12:17:07 +00:00
tokenData , err := microsoft_mdm . GetAzureAuthTokenClaims ( authToken . Content )
2023-07-19 16:30:24 +00:00
if err != nil {
2023-11-01 14:13:12 +00:00
return "" , "" , fmt . Errorf ( "binary security token claim failed: %v" , err )
2023-06-27 15:59:33 +00:00
}
// No errors, token is authorized
2023-11-01 14:13:12 +00:00
return tokenData . UPN , "" , nil
2023-06-27 15:59:33 +00:00
}
2023-11-01 14:13:12 +00:00
return "" , "" , errors . New ( "token is not authorized" )
2023-06-22 20:31:17 +00:00
}
2025-08-07 15:05:15 +00:00
// ProcessMDMMicrosoftDiscovery handles the Discovery message validation and response
func ( svc * Service ) ProcessMDMMicrosoftDiscovery ( ctx context . Context , req * fleet . SoapRequest ) ( * fleet . SoapResponse , error ) {
// Checking first if Discovery message is valid and returning error if this is not the case
if err := req . IsValidDiscoveryMsg ( ) ; err != nil {
// Log the raw XML request for debugging invalid messages
level . Debug ( svc . logger ) . Log (
"msg" , "invalid discover message" ,
"err" , err . Error ( ) ,
"request_xml" , string ( req . Raw ) ,
)
return nil , err
}
// Getting the DiscoveryResponse message
discoveryResponseMsg , err := svc . GetMDMMicrosoftDiscoveryResponse ( ctx , req . Body . Discover . Request . EmailAddress )
if err != nil {
return nil , err
}
// Embedding the DiscoveryResponse message inside of a SoapResponse
response , err := NewSoapResponse ( discoveryResponseMsg , req . GetMessageID ( ) )
if err != nil {
return nil , err
}
return & response , nil
}
2023-06-22 20:31:17 +00:00
// GetMDMMicrosoftDiscoveryResponse returns a valid DiscoveryResponse message
2023-07-19 16:30:24 +00:00
func ( svc * Service ) GetMDMMicrosoftDiscoveryResponse ( ctx context . Context , upnEmail string ) ( * fleet . DiscoverResponse , error ) {
2023-06-22 20:31:17 +00:00
// skipauth: This endpoint does not use authentication
svc . authz . SkipAuthorization ( ctx )
// Getting the app config
appCfg , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
2023-07-05 13:06:37 +00:00
// Getting the DiscoveryResponse message content
2023-11-30 12:17:07 +00:00
urlPolicyEndpoint , err := microsoft_mdm . ResolveWindowsMDMPolicy ( appCfg . ServerSettings . ServerURL )
2023-06-22 20:31:17 +00:00
if err != nil {
2023-06-27 15:59:33 +00:00
return nil , ctxerr . Wrap ( ctx , err , "resolve policy endpoint" )
2023-06-22 20:31:17 +00:00
}
2023-11-30 12:17:07 +00:00
urlEnrollEndpoint , err := microsoft_mdm . ResolveWindowsMDMEnroll ( appCfg . ServerSettings . ServerURL )
2023-06-22 20:31:17 +00:00
if err != nil {
2023-06-27 15:59:33 +00:00
return nil , ctxerr . Wrap ( ctx , err , "resolve enroll endpoint" )
2023-06-22 20:31:17 +00:00
}
2023-11-30 12:17:07 +00:00
discoveryMsg , err := NewDiscoverResponse ( syncml . AuthOnPremise , urlPolicyEndpoint , urlEnrollEndpoint )
2023-06-22 20:31:17 +00:00
if err != nil {
2023-06-27 15:59:33 +00:00
return nil , ctxerr . Wrap ( ctx , err , "creation of DiscoverResponse message" )
2023-06-22 20:31:17 +00:00
}
return & discoveryMsg , nil
}
2023-07-19 16:30:24 +00:00
// GetMDMMicrosoftSTSAuthResponse returns a valid Security Token Service (STS) page content
func ( svc * Service ) GetMDMMicrosoftSTSAuthResponse ( ctx context . Context , appru string , loginHint string ) ( string , error ) {
// skipauth: This endpoint does not use authentication
svc . authz . SkipAuthorization ( ctx )
// Dummy data will be returned as part of the token as user-driven enrollment is not supported yet
// In the future, the following calls would have to be made to support user-driven enrollment
// encodedBST will carry the token to return
// authToken, err := svc.wstepCertManager.NewSTSAuthToken(loginHint)
// encodedBST, err := GetEncodedBinarySecurityToken(fleet.WindowsMDMAutomaticEnrollmentType, authToken)
encodedBST := "user_driven_enrollment_not_implemented"
// STS Auth Endpoint returns HTML content that gets render in a webview container
// The webview container expect a POST request to the appru URL with the wresult parameter set to the auth token
// The security token in wresult is later passed back in <wsse:BinarySecurityToken>
// This string is opaque to the enrollment client; the client does not interpret the string.
// The returned HTML content contains a JS script that will perform a POST request to the appru URL automatically
// This will set the wresult parameter to the value of auth token
tmpl , err := template . New ( "" ) . Parse ( `
< script >
function performPost ( ) {
// Dinamically create a form element to submit the request
var form = document . createElement ( ' form ' ) ;
form . method = ' POST ' ;
2023-07-21 17:36:26 +00:00
form . action = "{{.ActionURL}}"
2023-07-19 16:30:24 +00:00
var inputToken = document . createElement ( ' input ' ) ;
inputToken . type = ' hidden ' ;
inputToken . name = ' wresult ' ;
2023-07-21 17:36:26 +00:00
inputToken . value = ' { { . Token } } ' ;
2023-07-19 16:30:24 +00:00
form . appendChild ( inputToken ) ;
// Submit the form
document . body . appendChild ( form ) ;
form . submit ( ) ;
}
// Call performPost() when the script is executed
performPost ( ) ;
< / script >
` )
if err != nil {
return "" , ctxerr . Wrap ( ctx , err , "STS content template" )
}
var htmlBuf bytes . Buffer
2023-07-21 17:36:26 +00:00
err = tmpl . Execute ( & htmlBuf , map [ string ] string { "ActionURL" : appru , "Token" : encodedBST } )
2023-07-19 16:30:24 +00:00
if err != nil {
return "" , ctxerr . Wrap ( ctx , err , "creation of STS content" )
}
return htmlBuf . String ( ) , nil
}
2023-06-27 15:59:33 +00:00
// GetMDMWindowsPolicyResponse returns a valid GetPoliciesResponse message
2023-07-19 16:30:24 +00:00
func ( svc * Service ) GetMDMWindowsPolicyResponse ( ctx context . Context , authToken * fleet . HeaderBinarySecurityToken ) ( * fleet . GetPoliciesResponse , error ) {
if authToken == nil {
return nil , fleet . NewInvalidArgumentError ( "policy response" , "authToken is invalid" )
2023-06-27 15:59:33 +00:00
}
// Validate the binary security token
2023-11-01 14:13:12 +00:00
_ , _ , err := svc . authBinarySecurityToken ( ctx , authToken )
2023-06-27 15:59:33 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "validate binary security token" )
}
// Token is authorized
svc . authz . SkipAuthorization ( ctx )
2023-07-05 13:06:37 +00:00
// Getting the GetPoliciesResponse message content
2023-11-30 12:17:07 +00:00
policyMsg , err := NewGetPoliciesResponse ( syncml . PolicyMinKeyLength , syncml . PolicyCertValidityPeriodInSecs , syncml . PolicyCertRenewalPeriodInSecs )
2023-06-27 15:59:33 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "creation of GetPoliciesResponse message" )
}
return & policyMsg , nil
}
2023-07-05 13:06:37 +00:00
// GetMDMWindowsEnrollResponse returns a valid RequestSecurityTokenResponseCollection message
// secTokenMsg is the RequestSecurityToken message
// authToken is the base64 encoded binary security token
2023-07-19 16:30:24 +00:00
func ( svc * Service ) GetMDMWindowsEnrollResponse ( ctx context . Context , secTokenMsg * fleet . RequestSecurityToken , authToken * fleet . HeaderBinarySecurityToken ) ( * fleet . RequestSecurityTokenResponseCollection , error ) {
if authToken == nil {
return nil , fleet . NewInvalidArgumentError ( "enroll response" , "authToken is not present" )
2023-07-05 13:06:37 +00:00
}
2023-07-19 16:30:24 +00:00
// Auth the binary security token
2023-11-01 14:13:12 +00:00
userID , hostUUID , err := svc . authBinarySecurityToken ( ctx , authToken )
2023-07-05 13:06:37 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "validate binary security token" )
}
// Removing the device if already MDM enrolled
err = svc . removeWindowsDeviceIfAlreadyMDMEnrolled ( ctx , secTokenMsg )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "device enroll check" )
}
2023-07-20 14:54:04 +00:00
// Getting the device provisioning information in the form of a WapProvisioningDoc
2023-07-05 13:06:37 +00:00
deviceProvisioning , err := svc . getDeviceProvisioningInformation ( ctx , secTokenMsg )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "device provisioning information" )
}
// Token is authorized
svc . authz . SkipAuthorization ( ctx )
// Getting the RequestSecurityTokenResponseCollection message content
secTokenResponseCollectionMsg , err := NewRequestSecurityTokenResponseCollection ( deviceProvisioning )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "creation of RequestSecurityTokenResponseCollection message" )
}
2023-07-06 18:33:40 +00:00
// RequestSecurityTokenResponseCollection message is ready. The identity
// and provisioning information will be sent to the Windows MDM
// Enrollment Client
// But before doing that, let's save the device information to the list
// of MDM enrolled MDM devices
//
// This method also creates the relevant enrollment activity as it has
// access to the device information.
2023-11-01 14:13:12 +00:00
err = svc . storeWindowsMDMEnrolledDevice ( ctx , userID , hostUUID , secTokenMsg )
2023-07-05 13:06:37 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "enrolled device information cannot be stored" )
}
return & secTokenResponseCollectionMsg , nil
}
2023-07-20 14:54:04 +00:00
// GetMDMWindowsManagementResponse returns a valid SyncML response message
2023-11-01 14:13:12 +00:00
func ( svc * Service ) GetMDMWindowsManagementResponse ( ctx context . Context , reqSyncML * fleet . SyncML , reqCerts [ ] * x509 . Certificate ) ( * fleet . SyncML , error ) {
2023-07-20 14:54:04 +00:00
if reqSyncML == nil {
return nil , fleet . NewInvalidArgumentError ( "syncml req message" , "message is not present" )
}
2023-11-01 14:13:12 +00:00
// Checking if the incoming request is trusted
err := svc . isTrustedRequest ( ctx , reqSyncML , reqCerts )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "management request is not trusted" )
}
2023-07-20 14:54:04 +00:00
// Getting the management response message
resSyncMLmsg , err := svc . getManagementResponse ( ctx , reqSyncML )
if err != nil {
2023-11-01 14:13:12 +00:00
return nil , ctxerr . Wrap ( ctx , err , "management response message" )
2023-07-20 14:54:04 +00:00
}
// Token is authorized
svc . authz . SkipAuthorization ( ctx )
return resSyncMLmsg , nil
}
2023-07-21 17:36:26 +00:00
// GetMDMWindowsTOSContent returns valid TOC content
func ( svc * Service ) GetMDMWindowsTOSContent ( ctx context . Context , redirectUri string , reqID string ) ( string , error ) {
2023-08-08 14:57:55 +00:00
tmpl , err := server . GetTemplate ( "frontend/templates/windowsTOS.html" , "windows-tos" )
2023-07-21 17:36:26 +00:00
if err != nil {
return "" , ctxerr . Wrap ( ctx , err , "issue generating TOS content" )
}
var htmlBuf bytes . Buffer
err = tmpl . Execute ( & htmlBuf , map [ string ] string { "RedirectURL" : redirectUri , "ClientData" : reqID } )
if err != nil {
return "" , ctxerr . Wrap ( ctx , err , "executing TOS template content" )
}
// skipauth: This endpoint does not use authentication
svc . authz . SkipAuthorization ( ctx )
return htmlBuf . String ( ) , nil
}
2023-10-06 22:04:33 +00:00
// isValidUPN checks if the provided user ID is a valid UPN
func isValidUPN ( userID string ) bool {
2023-11-01 14:13:12 +00:00
return upnRegex . MatchString ( userID )
2023-10-06 22:04:33 +00:00
}
2023-11-01 14:13:12 +00:00
// isTrustedRequest checks if the incoming request was sent from MDM enrolled device
func ( svc * Service ) isTrustedRequest ( ctx context . Context , reqSyncML * fleet . SyncML , reqCerts [ ] * x509 . Certificate ) error {
if reqSyncML == nil {
return fleet . NewInvalidArgumentError ( "syncml req message" , "message is not present" )
}
2023-10-06 22:04:33 +00:00
2023-11-01 14:13:12 +00:00
// Checking if calling request is coming from an already MDM enrolled device
deviceID , err := reqSyncML . GetSource ( )
if err != nil || deviceID == "" {
return fmt . Errorf ( "invalid SyncML message %w" , err )
}
enrolledDevice , err := svc . ds . MDMWindowsGetEnrolledDeviceWithDeviceID ( ctx , deviceID )
2023-10-06 22:04:33 +00:00
if err != nil || enrolledDevice == nil {
2023-11-01 14:13:12 +00:00
return errors . New ( "device was not MDM enrolled" )
}
// Check if TLS certs contains device ID on its common name
if len ( reqCerts ) > 0 {
for _ , reqCert := range reqCerts {
if strings . Contains ( reqCert . Subject . CommonName , deviceID ) {
return nil
}
}
}
// TODO: Latest version of the MDM client stack don't populate TLS.PeerCertificates array
// This is a temporary workaround to allow the management request to proceed
// Transport-level security should be replaced for Application-level security
// Transport-level security is defined in the MS-MDM spec in section 1.3.1
// On the other hand, Application-level security is defined here
// https://www.openmobilealliance.org/release/DM/V1_2_1-20080617-A/OMA-TS-DM_Security-V1_2_1-20080617-A.pdf
// The initial values for Application-level security configuration are defined in the
// WAP Profile blob that is sent to the device during the enrollment process. Example below
// <characteristic type="APPAUTH">
// <parm name="AAUTHLEVEL" value="CLIENT"/>
// <parm name="AAUTHTYPE" value="DIGEST"/>
// <parm name="AAUTHSECRET" value="2jsidqgffx"/>
// <parm name="AAUTHDATA" value="aGVsbG8gd29ybGQ="/>
// </characteristic>
// <characteristic type="APPAUTH">
// <parm name="AAUTHLEVEL" value="APPSRV"/>
// <parm name="AAUTHTYPE" value="DIGEST"/>
// <parm name="AAUTHNAME" value="43f8bf591b8557346021"/>
// <parm name="AAUTHSECRET" value="crbr3w2cab"/>
// <parm name="AAUTHDATA" value="aGVsbG8gd29ybGQ="/>
// </characteristic>
if len ( reqCerts ) == 0 {
return nil
}
return errors . New ( "calling device is not trusted" )
}
// regex to validate UPN
var upnRegex = regexp . MustCompile ( ` ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z] { 2,}$ ` )
// isFleetdPresentOnDevice checks if the device requires Fleetd to be deployed
func ( svc * Service ) isFleetdPresentOnDevice ( ctx context . Context , deviceID string ) ( bool , error ) {
// checking first if the device was enrolled through programmatic flow
enrolledDevice , err := svc . ds . MDMWindowsGetEnrolledDeviceWithDeviceID ( ctx , deviceID )
if err != nil {
return false , ctxerr . Wrap ( ctx , err , "get windows enrolled device" )
2023-10-06 22:04:33 +00:00
}
// If user identity is a MS-MDM UPN it means that the device was enrolled through user-driven flow
2023-11-01 14:13:12 +00:00
// This means that fleetd might not be installed
2023-10-06 22:04:33 +00:00
if isValidUPN ( enrolledDevice . MDMEnrollUserID ) {
2024-04-29 12:48:54 +00:00
var isPresent bool
if enrolledDevice . HostUUID != "" {
host , err := svc . ds . HostLiteByIdentifier ( ctx , enrolledDevice . HostUUID )
if err != nil && ! fleet . IsNotFound ( err ) {
return false , ctxerr . Wrap ( ctx , err , "get host lite by identifier" )
}
if host != nil {
orbitInfo , err := svc . ds . GetHostOrbitInfo ( ctx , host . ID )
if err != nil && ! fleet . IsNotFound ( err ) {
return false , ctxerr . Wrap ( ctx , err , "get host orbit info" )
}
if orbitInfo != nil {
isPresent = orbitInfo . Version != ""
}
}
}
return isPresent , nil
2023-10-06 22:04:33 +00:00
}
2023-11-01 14:13:12 +00:00
// TODO: Add check here to determine if MDM DeviceID is connected with Smbios UUID present on
// host table. This new check should look into command results table and extract the value of
// ./DevDetail/Ext/Microsoft/SMBIOSSerialNumber for the given DeviceID and use that for hosts
// table lookup
2023-10-06 22:04:33 +00:00
return true , nil
}
2023-11-01 14:13:12 +00:00
func ( svc * Service ) enqueueInstallFleetdCommand ( ctx context . Context , deviceID string ) error {
secrets , err := svc . ds . GetEnrollSecrets ( ctx , nil )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting enroll secrets" )
2023-07-20 14:54:04 +00:00
}
2023-11-01 14:13:12 +00:00
if len ( secrets ) == 0 {
level . Warn ( svc . logger ) . Log ( "msg" , "unable to find a global enroll secret to install fleetd" )
return nil
}
2023-07-20 14:54:04 +00:00
2024-06-28 19:35:07 +00:00
// it's okay to skip the installation if we're not able to retrieve the
// metadata, we don't want to completely error the SyncML transaction
// and we'll try again the next time the host checks in
fleetdMetadata , err := fleetdbase . GetMetadata ( )
if err != nil {
level . Warn ( svc . logger ) . Log ( "msg" , "unable to get fleetd-base metadata" )
return nil
}
2023-11-01 14:13:12 +00:00
appCfg , err := svc . ds . AppConfig ( ctx )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting app config" )
}
fleetURL := appCfg . ServerSettings . ServerURL
globalEnrollSecret := secrets [ 0 ] . Secret
addCommandUUID := uuid . NewString ( )
execCommandUUID := uuid . NewString ( )
rawAddCmd := [ ] byte ( `
< Add >
< CmdID > ` + addCommandUUID + ` < / CmdID >
< Item >
< Target >
2023-11-30 12:17:07 +00:00
< LocURI > ` + syncml.FleetdWindowsInstallerGUID + ` < / LocURI >
2023-11-01 14:13:12 +00:00
< / Target >
< / Item >
< / Add > ` )
// keeping the same GUID will prevent the MSI to be installed multiple times - it will be
// installed only the first time the message is issued.
// FleetURL and FleetSecret properties are passed to the Fleet MSI
rawExecCmd := [ ] byte ( `
< Exec >
< CmdID > ` + execCommandUUID + ` < / CmdID >
< Item >
< Target >
2023-11-30 12:17:07 +00:00
< LocURI > ` + syncml.FleetdWindowsInstallerGUID + ` < / LocURI >
2023-11-01 14:13:12 +00:00
< / Target >
< Data >
< MsiInstallJob id = "{A427C0AA-E2D5-40DF-ACE8-0D726A6BE096}" >
< Product Version = "1.0.0.0" >
< Download >
< ContentURLList >
2024-06-28 19:35:07 +00:00
< ContentURL > ` + fleetdMetadata.MSIURL + ` < / ContentURL >
2023-11-01 14:13:12 +00:00
< / ContentURLList >
< / Download >
< Validation >
2024-06-28 19:35:07 +00:00
< FileHash > ` + fleetdMetadata.MSISha256 + ` < / FileHash >
2023-11-01 14:13:12 +00:00
< / Validation >
< Enforcement >
2024-06-17 20:37:15 +00:00
< CommandLine > / quiet FLEET_URL = "` + fleetURL + `" FLEET_SECRET = "` + globalEnrollSecret + `" ENABLE_SCRIPTS = "True" < / CommandLine >
2023-11-01 14:13:12 +00:00
< TimeOut > 10 < / TimeOut >
< RetryCount > 1 < / RetryCount >
< RetryInterval > 5 < / RetryInterval >
< / Enforcement >
< / Product >
< / MsiInstallJob >
< / Data >
< Meta >
< Type xmlns = "syncml:metinf" > text / plain < / Type >
< Format xmlns = "syncml:metinf" > xml < / Format >
< / Meta >
< / Item >
< / Exec >
` )
// TODO: add ability to batch-enqueue multiple commands at the same time
addFleetdCmd := & fleet . MDMWindowsCommand {
CommandUUID : addCommandUUID ,
RawCommand : rawAddCmd ,
2023-11-30 12:17:07 +00:00
TargetLocURI : syncml . FleetdWindowsInstallerGUID ,
2023-11-01 14:13:12 +00:00
}
if err := svc . ds . MDMWindowsInsertCommandForHosts ( ctx , [ ] string { deviceID } , addFleetdCmd ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "insert add command to install fleetd" )
}
execFleetCmd := & fleet . MDMWindowsCommand {
CommandUUID : execCommandUUID ,
RawCommand : rawExecCmd ,
2023-11-30 12:17:07 +00:00
TargetLocURI : syncml . FleetdWindowsInstallerGUID ,
2023-11-01 14:13:12 +00:00
}
if err := svc . ds . MDMWindowsInsertCommandForHosts ( ctx , [ ] string { deviceID } , execFleetCmd ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "insert exec command to install fleetd" )
}
2023-07-20 14:54:04 +00:00
2023-11-01 14:13:12 +00:00
return nil
}
2023-07-20 14:54:04 +00:00
2023-11-01 14:13:12 +00:00
// Alerts Handlers
2023-07-20 14:54:04 +00:00
2023-11-01 14:13:12 +00:00
// New session Alert Handler
// This handler will return an protocol command to install an MSI on a new session from unenrolled device
func ( svc * Service ) processNewSessionAlert ( ctx context . Context , messageID string , deviceID string , cmd mdm_types . ProtoCmdOperation ) error {
// Checking if fleetd is present on the device
fleetdPresent , err := svc . isFleetdPresentOnDevice ( ctx , deviceID )
2023-07-20 14:54:04 +00:00
if err != nil {
2023-11-01 14:13:12 +00:00
return err
2023-07-20 14:54:04 +00:00
}
2023-11-01 14:13:12 +00:00
if ! fleetdPresent {
return svc . enqueueInstallFleetdCommand ( ctx , deviceID )
}
return nil
}
// Generic Alert Handlers
// This handler will check for generic alerts. Device unenrollment is handled here
func ( svc * Service ) processGenericAlert ( ctx context . Context , messageID string , deviceID string , cmd mdm_types . ProtoCmdOperation ) error {
// Checking user-initiated unenrollment request
if len ( cmd . Cmd . Items ) > 0 {
for _ , item := range cmd . Cmd . Items {
if item . Meta == nil || item . Meta . Type == nil || item . Meta . Type . Content == nil {
continue
}
// Checking if user-initiated unenrollment request is present
2023-11-30 12:17:07 +00:00
if * item . Meta . Type . Content == syncml . AlertUserUnenrollmentRequest {
2023-11-01 14:13:12 +00:00
// Deleting the device from the list of enrolled device
err := svc . ds . MDMWindowsDeleteEnrolledDeviceWithDeviceID ( ctx , deviceID )
if err != nil {
return fmt . Errorf ( "unenrolling windows device: %w" , err )
}
}
}
}
return nil
}
// processIncomingAlertsCommands will process the incoming Alerts commands.
// These commands don't require an status response.
func ( svc * Service ) processIncomingAlertsCommands ( ctx context . Context , messageID string , deviceID string , cmd mdm_types . ProtoCmdOperation ) error {
if cmd . Cmd . Data == nil {
return errors . New ( "invalid alert command" )
}
// gathering the incoming Alert ID
alertID := * cmd . Cmd . Data
switch alertID {
2023-11-30 12:17:07 +00:00
case syncml . CmdAlertClientInitiatedManagement :
2023-11-01 14:13:12 +00:00
return svc . processNewSessionAlert ( ctx , messageID , deviceID , cmd )
2023-11-30 12:17:07 +00:00
case syncml . CmdAlertServerInitiatedManagement :
2023-11-01 14:13:12 +00:00
return svc . processNewSessionAlert ( ctx , messageID , deviceID , cmd )
2023-11-30 12:17:07 +00:00
case syncml . CmdAlertGeneric :
2023-11-01 14:13:12 +00:00
return svc . processGenericAlert ( ctx , messageID , deviceID , cmd )
}
return nil
}
// processIncomingMDMCmds process the incoming message from the device
// It will return the list of operations that need to be sent to the device
func ( svc * Service ) processIncomingMDMCmds ( ctx context . Context , deviceID string , reqMsg * fleet . SyncML ) ( [ ] * fleet . SyncMLCmd , error ) {
var responseCmds [ ] * fleet . SyncMLCmd
// Get the incoming MessageID
reqMessageID , err := reqMsg . GetMessageID ( )
if err != nil {
return nil , fmt . Errorf ( "get incoming msg: %w" , err )
}
// Acknowledge the message header
// msgref is always 0 for the header
if err = reqMsg . IsValidHeader ( ) ; err == nil {
2023-11-30 12:17:07 +00:00
ackMsg := NewSyncMLCmdStatus ( reqMessageID , "0" , syncml . SyncMLHdrName , syncml . CmdStatusOK )
2023-11-01 14:13:12 +00:00
responseCmds = append ( responseCmds , ackMsg )
}
2025-01-21 15:59:32 +00:00
enrichedSyncML := fleet . NewEnrichedSyncML ( reqMsg )
if enrichedSyncML . HasCommands ( ) {
if err := svc . ds . MDMWindowsSaveResponse ( ctx , deviceID , enrichedSyncML ) ; err != nil {
return nil , fmt . Errorf ( "store incoming msgs: %w" , err )
}
2023-11-01 14:13:12 +00:00
}
// Iterate over the operations and process them
for _ , protoCMD := range reqMsg . GetOrderedCmds ( ) {
// Alerts, Results and Status don't require a status response
switch protoCMD . Verb {
case mdm_types . CmdAlert :
err := svc . processIncomingAlertsCommands ( ctx , reqMessageID , deviceID , protoCMD )
if err != nil {
return nil , fmt . Errorf ( "process incoming command: %w" , err )
}
continue
case mdm_types . CmdStatus , mdm_types . CmdResults :
continue
}
// CmdStatusOK is returned for the rest of the operations
2024-02-07 19:24:03 +00:00
responseCmds = append ( responseCmds , NewSyncMLCmdStatus ( reqMessageID , protoCMD . Cmd . CmdID . Value , protoCMD . Verb , syncml . CmdStatusOK ) )
2023-11-01 14:13:12 +00:00
}
return responseCmds , nil
}
// getPendingMDMCmds returns the list of pending MDM commands for the device
func ( svc * Service ) getPendingMDMCmds ( ctx context . Context , deviceID string ) ( [ ] * mdm_types . SyncMLCmd , error ) {
pendingCmds , err := svc . ds . MDMWindowsGetPendingCommands ( ctx , deviceID )
if err != nil {
return nil , fmt . Errorf ( "getting incoming cmds %w" , err )
}
// Converting the pending commands to its target SyncML types
var cmds [ ] * mdm_types . SyncMLCmd
for _ , pendingCmd := range pendingCmds {
2024-12-18 21:27:35 +00:00
// The raw MDM command may contain a $FLEET_SECRET_XXX, the value of which should never be exposed or stored unencrypted.
rawCommandWithSecret , err := svc . ds . ExpandEmbeddedSecrets ( ctx , string ( pendingCmd . RawCommand ) )
if err != nil {
// This error should never happen since we validate the presence of needed secrets on profile upload.
return nil , ctxerr . Wrap ( ctx , err , "expanding embedded secrets for Windows pending commands" )
}
2023-11-01 14:13:12 +00:00
cmd := new ( mdm_types . SyncMLCmd )
2024-12-18 21:27:35 +00:00
if err := xml . Unmarshal ( [ ] byte ( rawCommandWithSecret ) , cmd ) ; err != nil {
2023-11-01 14:13:12 +00:00
logging . WithErr ( ctx , ctxerr . Wrap ( ctx , err , "getPendingMDMCmds syncML cmd creation" ) )
continue
}
cmds = append ( cmds , cmd )
}
return cmds , nil
}
// createResponseSyncML returns a valid SyncML message
func ( svc * Service ) createResponseSyncML ( ctx context . Context , req * fleet . SyncML , responseOps [ ] * mdm_types . SyncMLCmd ) ( * fleet . SyncML , error ) {
// Get the DeviceID
deviceID , err := req . GetSource ( )
if err != nil || deviceID == "" {
return nil , fmt . Errorf ( "invalid SyncML message %w" , err )
}
// Get SessionID
sessionID , err := req . GetSessionID ( )
if err != nil {
return nil , fmt . Errorf ( "session ID processing error %w" , err )
}
// Get MessageID
messageID , err := req . GetMessageID ( )
if err != nil {
return nil , fmt . Errorf ( "message ID processing error %w" , err )
}
// Getting the Management endpoint URL
appConfig , err := svc . ds . AppConfig ( ctx )
if err != nil {
return nil , fmt . Errorf ( "appconfig was not available %w" , err )
}
2023-11-30 12:17:07 +00:00
urlManagementEndpoint , err := microsoft_mdm . ResolveWindowsMDMManagement ( appConfig . ServerSettings . ServerURL )
2023-11-01 14:13:12 +00:00
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "resolve management endpoint" )
}
// Create the SyncML message with the response operations
msg , err := createSyncMLMessage ( sessionID , messageID , deviceID , urlManagementEndpoint , responseOps )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err , "creation of SyncML message" )
}
return msg , nil
}
// getManagementResponse returns a valid SyncML response message
func ( svc * Service ) getManagementResponse ( ctx context . Context , reqMsg * fleet . SyncML ) ( * mdm_types . SyncML , error ) {
if reqMsg == nil {
return nil , fleet . NewInvalidArgumentError ( "syncml req message" , "message is not present" )
}
// Get the DeviceID
deviceID , err := reqMsg . GetSource ( )
if err != nil || deviceID == "" {
return nil , fmt . Errorf ( "invalid SyncML message %w" , err )
}
// Process the incoming MDM protocol commands and get the response MDM protocol commands
resIncomingCmds , err := svc . processIncomingMDMCmds ( ctx , deviceID , reqMsg )
if err != nil {
return nil , fmt . Errorf ( "message processing error %w" , err )
}
// Process the pending operations and get the MDM response protocol commands
resPendingCmds , err := svc . getPendingMDMCmds ( ctx , deviceID )
2023-07-20 14:54:04 +00:00
if err != nil {
2023-11-01 14:13:12 +00:00
return nil , fmt . Errorf ( "message processing error %w" , err )
}
// Create the response SyncML message
2025-01-20 15:12:33 +00:00
msg , err := svc . createResponseSyncML ( ctx , reqMsg , append ( resIncomingCmds , resPendingCmds ... ) )
2023-11-01 14:13:12 +00:00
if err != nil {
return nil , fmt . Errorf ( "message syncML creation error %w" , err )
}
return msg , nil
2023-07-20 14:54:04 +00:00
}
2023-07-05 13:06:37 +00:00
// removeWindowsDeviceIfAlreadyMDMEnrolled removes the device if already MDM enrolled
// HW DeviceID is used to check the list of enrolled devices
func ( svc * Service ) removeWindowsDeviceIfAlreadyMDMEnrolled ( ctx context . Context , secTokenMsg * fleet . RequestSecurityToken ) error {
// Getting the HW DeviceID from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqHWDeviceID , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemHWDevID )
2023-07-05 13:06:37 +00:00
if err != nil {
return err
}
2023-11-01 14:13:12 +00:00
// Device is already enrolled, let's remove it
err = svc . ds . MDMWindowsDeleteEnrolledDevice ( ctx , reqHWDeviceID )
2023-07-05 13:06:37 +00:00
if err != nil {
if fleet . IsNotFound ( err ) {
return nil
}
return err
}
return nil
}
// getDeviceProvisioningInformation returns a valid WapProvisioningDoc
// This is the provisioning information that will be sent to the Windows MDM Enrollment Client
// This information is used to configure the device management client
// See section 2.2.9.1 for more details on the XML provision schema used here
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/35e1aca6-1b8a-48ba-bbc0-23af5d46907a
func ( svc * Service ) getDeviceProvisioningInformation ( ctx context . Context , secTokenMsg * fleet . RequestSecurityToken ) ( string , error ) {
// Getting the HW DeviceID from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqHWDeviceID , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemHWDevID )
2023-07-05 13:06:37 +00:00
if err != nil {
return "" , err
}
// Getting the EnrollmentType information from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqEnrollType , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemEnrollmentType )
2023-07-05 13:06:37 +00:00
if err != nil {
return "" , err
}
// Getting the BinarySecurityToken from the RequestSecurityToken msg
binSecurityTokenData , err := secTokenMsg . GetBinarySecurityTokenData ( )
if err != nil {
return "" , err
}
// Getting the BinarySecurityToken type from the RequestSecurityToken msg
binSecurityTokenType , err := secTokenMsg . GetBinarySecurityTokenType ( )
if err != nil {
return "" , err
}
// Getting the client CSR request from the device
2023-11-30 12:17:07 +00:00
clientCSR , err := microsoft_mdm . GetClientCSR ( binSecurityTokenData , binSecurityTokenType )
2023-07-05 13:06:37 +00:00
if err != nil {
return "" , err
}
// Getting the signed, DER-encoded certificate bytes and its uppercased, hex-endcoded SHA1 fingerprint
rawSignedCertDER , rawSignedCertFingerprint , err := svc . SignMDMMicrosoftClientCSR ( ctx , reqHWDeviceID , clientCSR )
if err != nil {
return "" , err
}
// Preparing client certificate and identity certificate information to be sent to the Windows MDM Enrollment Client
certStoreProvisioningData := NewCertStoreProvisioningData (
reqEnrollType ,
svc . wstepCertManager . IdentityFingerprint ( ) ,
svc . wstepCertManager . IdentityCert ( ) . Raw ,
rawSignedCertFingerprint ,
rawSignedCertDER )
// Preparing the provisioning information that includes the location of the Device Management Service (DMS)
appCfg , err := svc . ds . AppConfig ( ctx )
if err != nil {
return "" , err
}
// Getting the MS-MDM management URL to provision the device
2023-11-30 12:17:07 +00:00
urlManagementEndpoint , err := microsoft_mdm . ResolveWindowsMDMManagement ( appCfg . ServerSettings . ServerURL )
2023-07-05 13:06:37 +00:00
if err != nil {
return "" , err
}
// Preparing the Application Provisioning information
appConfigProvisioningData := NewApplicationProvisioningData ( urlManagementEndpoint )
// Preparing the DM Client Provisioning information
appDMClientProvisioningData := NewDMClientProvisioningData ( )
// And finally returning the Base64 encoded representation of the Provisioning Doc XML
provDoc := NewProvisioningDoc ( certStoreProvisioningData , appConfigProvisioningData , appDMClientProvisioningData )
encodedProvDoc , err := provDoc . GetEncodedB64Representation ( )
if err != nil {
return "" , err
}
return encodedProvDoc , nil
}
// storeWindowsMDMEnrolledDevice stores the device information to the list of MDM enrolled devices
2023-11-01 14:13:12 +00:00
func ( svc * Service ) storeWindowsMDMEnrolledDevice ( ctx context . Context , userID string , hostUUID string , secTokenMsg * fleet . RequestSecurityToken ) error {
2023-07-05 13:06:37 +00:00
const (
error_tag = "windows MDM enrolled storage: "
)
// Getting the DeviceID context information from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqDeviceID , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemDeviceID )
2023-07-05 13:06:37 +00:00
if err != nil {
return fmt . Errorf ( "%s %v" , error_tag , err )
}
// Getting the HWDevID context information from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqHWDevID , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemHWDevID )
2023-07-05 13:06:37 +00:00
if err != nil {
return fmt . Errorf ( "%s %v" , error_tag , err )
}
// Getting the Enroll DeviceType context information from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqDeviceType , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemDeviceType )
2023-07-05 13:06:37 +00:00
if err != nil {
return fmt . Errorf ( "%s %v" , error_tag , err )
}
// Getting the Enroll DeviceName context information from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqDeviceName , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemDeviceName )
2023-07-05 13:06:37 +00:00
if err != nil {
return fmt . Errorf ( "%s %v" , error_tag , err )
}
// Getting the Enroll RequestVersion context information from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqEnrollVersion , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemRequestVersion )
2023-07-05 13:06:37 +00:00
if err != nil {
2023-07-21 17:36:26 +00:00
reqEnrollVersion = "request_version_not_present"
2023-07-05 13:06:37 +00:00
}
// Getting the RequestVersion context information from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqAppVersion , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemApplicationVersion )
2023-07-05 13:06:37 +00:00
if err != nil {
return fmt . Errorf ( "%s %v" , error_tag , err )
}
// Getting the EnrollmentType information from the RequestSecurityToken msg
2023-11-30 12:17:07 +00:00
reqEnrollType , err := GetContextItem ( secTokenMsg , syncml . ReqSecTokenContextItemEnrollmentType )
2023-07-05 13:06:37 +00:00
if err != nil {
return fmt . Errorf ( "%s %v" , error_tag , err )
}
// Getting the Windows Enrolled Device Information
enrolledDevice := & fleet . MDMWindowsEnrolledDevice {
MDMDeviceID : reqDeviceID ,
MDMHardwareID : reqHWDevID ,
2023-11-30 12:17:07 +00:00
MDMDeviceState : microsoft_mdm . MDMDeviceStateEnrolled ,
2023-07-05 13:06:37 +00:00
MDMDeviceType : reqDeviceType ,
MDMDeviceName : reqDeviceName ,
MDMEnrollType : reqEnrollType ,
2023-07-19 16:30:24 +00:00
MDMEnrollUserID : userID , // This could be Host UUID or UPN email
2023-07-05 13:06:37 +00:00
MDMEnrollProtoVersion : reqEnrollVersion ,
MDMEnrollClientVersion : reqAppVersion ,
MDMNotInOOBE : false ,
2023-11-01 14:13:12 +00:00
HostUUID : hostUUID ,
2023-07-05 13:06:37 +00:00
}
if err := svc . ds . MDMWindowsInsertEnrolledDevice ( ctx , enrolledDevice ) ; err != nil {
return err
}
2024-04-29 19:43:15 +00:00
// TODO: azure enrollments come with an empty uuid, I haven't figured
// out a good way to identify the device.
2024-12-04 22:19:18 +00:00
displayName := reqDeviceName
var serial string
2024-04-29 19:43:15 +00:00
if hostUUID != "" {
mdmLifecycle := mdmlifecycle . New ( svc . ds , svc . logger )
err = mdmLifecycle . Do ( ctx , mdmlifecycle . HostOptions {
Action : mdmlifecycle . HostActionTurnOn ,
Platform : "windows" ,
UUID : hostUUID ,
} )
if err != nil {
return err
}
2024-12-04 22:19:18 +00:00
// Get the host in order to get the correct display name and serial number for the activity
adminTeamFilter := fleet . TeamFilter {
User : & fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } ,
}
hosts , err := svc . ds . ListHostsLiteByUUIDs ( ctx , adminTeamFilter , [ ] string { hostUUID } )
if err != nil {
// Do not abort; this call was only made to get better data for the activity, so shouldn't
// fail the request. We fall back to `reqDeviceName` for the display name in this case.
logging . WithExtras ( logging . WithNoUser ( ctx ) ,
"msg" , "failed to get host data for windows MDM enrollment activity" ,
)
}
if len ( hosts ) == 1 {
// then we found the host, so use the data from there for the activity
displayName = hosts [ 0 ] . DisplayName ( )
serial = hosts [ 0 ] . HardwareSerial
}
2024-04-29 19:43:15 +00:00
}
2024-05-24 16:25:27 +00:00
err = svc . NewActivity (
ctx , nil , & fleet . ActivityTypeMDMEnrolled {
2024-12-04 22:19:18 +00:00
HostDisplayName : displayName ,
2024-05-24 16:25:27 +00:00
MDMPlatform : fleet . MDMPlatformMicrosoft ,
2025-07-24 15:28:50 +00:00
HostSerial : & serial ,
2024-05-24 16:25:27 +00:00
} )
2023-07-06 18:33:40 +00:00
if err != nil {
// only logging, the device is enrolled at this point, and we
// wouldn't want to fail the request because there was a problem
// creating an activity feed item.
logging . WithExtras ( logging . WithNoUser ( ctx ) ,
"msg" , "failed to generate windows MDM enrolled activity" ,
)
}
2023-07-05 13:06:37 +00:00
return nil
}
// GetContextItem returns the context item from the RequestSecurityToken message
func GetContextItem ( secTokenMsg * fleet . RequestSecurityToken , contextItem string ) ( string , error ) {
reqHWDeviceID , err := secTokenMsg . GetContextItem ( contextItem )
if err != nil {
return "" , fmt . Errorf ( "%s token context information is not present: %v" , contextItem , err )
}
return reqHWDeviceID , nil
}
2023-06-22 20:31:17 +00:00
// GetAuthorizedSoapFault authorize the request so SoapFault message can be returned
func ( svc * Service ) GetAuthorizedSoapFault ( ctx context . Context , eType string , origMsg int , errorMsg error ) * fleet . SoapFault {
svc . authz . SkipAuthorization ( ctx )
2023-09-14 17:29:12 +00:00
logging . WithErr ( ctx , ctxerr . Wrap ( ctx , errorMsg , "soap fault" ) )
2023-06-22 20:31:17 +00:00
soapFault := NewSoapFault ( eType , origMsg , errorMsg )
return & soapFault
}
2023-06-29 22:31:53 +00:00
func ( svc * Service ) SignMDMMicrosoftClientCSR ( ctx context . Context , subject string , csr * x509 . CertificateRequest ) ( [ ] byte , string , error ) {
2023-07-10 20:36:17 +00:00
if svc . wstepCertManager == nil {
return nil , "" , errors . New ( "windows mdm identity keypair was not configured" )
}
2023-06-29 22:31:53 +00:00
cert , fpHex , err := svc . wstepCertManager . SignClientCSR ( ctx , subject , csr )
if err != nil {
return nil , "signing wstep client csr" , ctxerr . Wrap ( ctx , err )
}
// TODO: if desired, the signature of this method can be modified to accept a device UUID so
// that we can associate the certificate with the host here by calling
// svc.wstepCertManager.AssociateCertHash
return cert , fpHex , nil
}
2023-07-20 14:54:04 +00:00
2023-11-01 14:13:12 +00:00
// MS-MDM Commands helpers
// createSyncMLMessage takes input data and returns a SyncML struct
func createSyncMLMessage ( sessionID string , msgID string , deviceID string , source string , protoCommands [ ] * mdm_types . SyncMLCmd ) ( * mdm_types . SyncML , error ) {
// Sanity check on input
if len ( sessionID ) == 0 || len ( msgID ) == 0 || len ( deviceID ) == 0 || len ( source ) == 0 {
return nil , errors . New ( "invalid parameters" )
}
if msgID == "0" {
return nil , errors . New ( "invalid msg ID" )
2023-07-20 14:54:04 +00:00
}
2023-11-01 14:13:12 +00:00
if len ( protoCommands ) == 0 {
return nil , errors . New ( "invalid operations" )
}
// Setting source LocURI
var sourceLocURI * mdm_types . LocURI
if len ( source ) > 0 {
sourceLocURI = & mdm_types . LocURI {
LocURI : & source ,
2023-07-20 14:54:04 +00:00
}
}
2023-11-01 14:13:12 +00:00
// setting up things on the SyncML message
var msg mdm_types . SyncML
2023-11-30 12:17:07 +00:00
msg . Xmlns = syncml . SyncCmdNamespace
2023-11-01 14:13:12 +00:00
msg . SyncHdr = mdm_types . SyncHdr {
2023-11-30 12:17:07 +00:00
VerDTD : syncml . SyncMLSupportedVersion ,
VerProto : syncml . SyncMLVerProto ,
2023-11-01 14:13:12 +00:00
SessionID : sessionID ,
MsgID : msgID ,
Target : & mdm_types . LocURI { LocURI : & deviceID } ,
Source : sourceLocURI ,
}
// iterate over operations and append them to the SyncML message
for _ , protoCmd := range protoCommands {
msg . AppendCommand ( fleet . MDMRaw , * protoCmd )
}
// If there was no error, return the SyncML and a nil error
return & msg , nil
}
// newSyncMLCmdWithNoItem creates a new SyncML command
func newSyncMLCmdWithNoItem ( cmdVerb * string , cmdData * string ) * mdm_types . SyncMLCmd {
return & mdm_types . SyncMLCmd {
XMLName : xml . Name { Local : * cmdVerb } ,
Data : cmdData ,
Items : nil ,
}
}
// newSyncMLCmdWithItem creates a new SyncML command
func newSyncMLCmdWithItem ( cmdVerb * string , cmdData * string , cmdItem * mdm_types . CmdItem ) * mdm_types . SyncMLCmd {
return & mdm_types . SyncMLCmd {
XMLName : xml . Name { Local : * cmdVerb } ,
Data : cmdData ,
Items : [ ] mdm_types . CmdItem { * cmdItem } ,
}
}
// newSyncMLItem creates a new SyncML command
func newSyncMLItem ( cmdSource * string , cmdTarget * string , cmdDataType * string , cmdDataFormat * string , cmdDataValue * string ) * mdm_types . CmdItem {
var metaFormat * mdm_types . MetaAttr
var metaType * mdm_types . MetaAttr
var meta * mdm_types . Meta
2023-12-14 18:05:00 +00:00
var data * mdm_types . RawXmlData
2023-11-01 14:13:12 +00:00
if cmdDataFormat != nil && len ( * cmdDataFormat ) > 0 {
metaFormat = & mdm_types . MetaAttr {
XMLNS : "syncml:metinf" ,
Content : cmdDataFormat ,
2023-07-20 14:54:04 +00:00
}
}
2023-11-01 14:13:12 +00:00
if cmdDataType != nil && len ( * cmdDataType ) > 0 {
metaType = & mdm_types . MetaAttr {
XMLNS : "syncml:metinf" ,
Content : cmdDataType ,
}
}
if metaFormat != nil || metaType != nil {
meta = & mdm_types . Meta {
Format : metaFormat ,
Type : metaType ,
}
}
2023-12-14 18:05:00 +00:00
if cmdDataValue != nil {
data = & mdm_types . RawXmlData {
Content : * cmdDataValue ,
}
}
2023-11-01 14:13:12 +00:00
return & mdm_types . CmdItem {
Meta : meta ,
2023-12-14 18:05:00 +00:00
Data : data ,
2023-11-01 14:13:12 +00:00
Target : cmdTarget ,
Source : cmdSource ,
}
2023-07-20 14:54:04 +00:00
}
2023-11-01 14:13:12 +00:00
// NewSyncMLCmd creates a new SyncML command
func NewSyncMLCmd ( cmdVerb string , cmdSource string , cmdTarget string , cmdDataType string , cmdDataFormat string , cmdDataValue string ) * mdm_types . SyncMLCmd {
var workCmdVerb * string
var workCmdSource * string
var workCmdTarget * string
var workCmdDataType * string
var workCmdDataFormat * string
var workCmdDataValue * string
if len ( cmdVerb ) > 0 {
workCmdVerb = & cmdVerb
}
if len ( cmdSource ) > 0 {
workCmdSource = & cmdSource
}
if len ( cmdTarget ) > 0 {
workCmdTarget = & cmdTarget
}
if len ( cmdDataType ) > 0 {
workCmdDataType = & cmdDataType
}
if len ( cmdDataFormat ) > 0 {
workCmdDataFormat = & cmdDataFormat
}
if len ( cmdDataValue ) > 0 {
workCmdDataValue = & cmdDataValue
}
item := newSyncMLItem ( workCmdSource , workCmdTarget , workCmdDataType , workCmdDataFormat , workCmdDataValue )
return newSyncMLCmdWithItem ( workCmdVerb , nil , item )
}
func NewTypedSyncMLCmd ( dataType mdm_types . SyncMLDataType , cmdVerb string , cmdTarget string , cmdData string ) ( * mdm_types . SyncMLCmd , error ) {
errInvalidParameters := errors . New ( "invalid parameters" )
// Checking if command verb is present
if cmdVerb == "" {
return nil , errInvalidParameters
}
// Returning command based on input command data type
switch dataType {
case mdm_types . SFEmpty :
if len ( cmdData ) > 0 {
rawCmd := newSyncMLNoItem ( cmdVerb , cmdData )
return rawCmd , nil
}
return nil , errInvalidParameters
case mdm_types . SFNoFormat :
if len ( cmdData ) > 0 && len ( cmdTarget ) > 0 {
rawCmd := newSyncMLNoFormat ( cmdVerb , cmdTarget )
return rawCmd , nil
}
return nil , errInvalidParameters
case mdm_types . SFText :
if len ( cmdData ) > 0 && len ( cmdTarget ) > 0 && len ( cmdData ) > 0 {
rawCmd := newSyncMLCmdText ( cmdVerb , cmdTarget , cmdData )
return rawCmd , nil
}
return nil , errInvalidParameters
case mdm_types . SFXml :
if len ( cmdData ) > 0 && len ( cmdTarget ) > 0 && len ( cmdData ) > 0 {
rawCmd := newSyncMLCmdXml ( cmdVerb , cmdTarget , cmdData )
return rawCmd , nil
}
return nil , errInvalidParameters
case mdm_types . SFInteger :
if len ( cmdData ) > 0 && len ( cmdTarget ) > 0 && len ( cmdData ) > 0 {
rawCmd := newSyncMLCmdInt ( cmdVerb , cmdTarget , cmdData )
return rawCmd , nil
}
return nil , errInvalidParameters
2023-07-20 14:54:04 +00:00
2023-11-01 14:13:12 +00:00
case mdm_types . SFBase64 :
if len ( cmdData ) > 0 && len ( cmdTarget ) > 0 && len ( cmdData ) > 0 {
rawCmd := newSyncMLCmdBase64 ( cmdVerb , cmdTarget , cmdData )
return rawCmd , nil
2023-07-20 14:54:04 +00:00
}
2023-11-01 14:13:12 +00:00
return nil , errInvalidParameters
case mdm_types . SFBoolean :
if len ( cmdData ) > 0 && len ( cmdTarget ) > 0 && len ( cmdData ) > 0 {
rawCmd := newSyncMLCmdBool ( cmdVerb , cmdTarget , cmdData )
return rawCmd , nil
}
return nil , errInvalidParameters
2023-07-20 14:54:04 +00:00
}
2023-11-01 14:13:12 +00:00
return nil , errInvalidParameters
}
// newSyncMLNoItem creates a new SyncML command with no item
// This is used for commands that do not have any items such as Alerts
func newSyncMLNoItem ( cmdVerb string , cmdData string ) * mdm_types . SyncMLCmd {
return newSyncMLCmdWithNoItem ( & cmdVerb , & cmdData )
}
// newSyncMLNoFormat creates a new SyncML command with no format
// This is used for commands that do not have any data such as Get
func newSyncMLNoFormat ( cmdVerb string , cmdTarget string ) * mdm_types . SyncMLCmd {
item := newSyncMLItem ( nil , & cmdTarget , nil , nil , nil )
return newSyncMLCmdWithItem ( & cmdVerb , nil , item )
}
// newSyncMLCmdText creates a new SyncML command with text data
func newSyncMLCmdText ( cmdVerb string , cmdTarget string , cmdDataValue string ) * mdm_types . SyncMLCmd {
cmdType := "text/plain"
cmdFormat := "chr"
item := newSyncMLItem ( nil , & cmdTarget , & cmdType , & cmdFormat , & cmdDataValue )
return newSyncMLCmdWithItem ( & cmdVerb , nil , item )
}
// newSyncMLCmdXml creates a new SyncML command with XML data
func newSyncMLCmdXml ( cmdVerb string , cmdTarget string , cmdDataValue string ) * mdm_types . SyncMLCmd {
cmdType := "text/plain"
cmdFormat := "xml"
escapedXML := html . EscapeString ( cmdDataValue )
item := newSyncMLItem ( nil , & cmdTarget , & cmdType , & cmdFormat , & escapedXML )
return newSyncMLCmdWithItem ( & cmdVerb , nil , item )
}
// newSyncMLCmdBase64 creates a new SyncML command with Base64 encoded data
func newSyncMLCmdBase64 ( cmdVerb string , cmdTarget string , cmdDataValue string ) * mdm_types . SyncMLCmd {
cmdFormat := "b64"
escapedXML := html . EscapeString ( cmdDataValue )
item := newSyncMLItem ( nil , & cmdTarget , nil , & cmdFormat , & escapedXML )
return newSyncMLCmdWithItem ( & cmdVerb , nil , item )
}
// newSyncMLCmdInt creates a new SyncML command with text data
func newSyncMLCmdInt ( cmdVerb string , cmdTarget string , cmdDataValue string ) * mdm_types . SyncMLCmd {
cmdType := "text/plain"
cmdFormat := "int"
item := newSyncMLItem ( nil , & cmdTarget , & cmdType , & cmdFormat , & cmdDataValue )
return newSyncMLCmdWithItem ( & cmdVerb , nil , item )
}
// newSyncMLCmdBool creates a new SyncML command with text data
func newSyncMLCmdBool ( cmdVerb string , cmdTarget string , cmdDataValue string ) * mdm_types . SyncMLCmd {
cmdType := "text/plain"
cmdFormat := "bool"
item := newSyncMLItem ( nil , & cmdTarget , & cmdType , & cmdFormat , & cmdDataValue )
return newSyncMLCmdWithItem ( & cmdVerb , nil , item )
}
// NewSyncMLCmdStatus creates a new SyncML command with text data
func NewSyncMLCmdStatus ( msgRef string , cmdRef string , cmdOrig string , statusCode string ) * mdm_types . SyncMLCmd {
return & mdm_types . SyncMLCmd {
XMLName : xml . Name { Local : mdm_types . CmdStatus } ,
MsgRef : & msgRef ,
CmdRef : & cmdRef ,
Cmd : & cmdOrig ,
Data : & statusCode ,
Items : nil ,
2024-02-07 19:24:03 +00:00
CmdID : mdm_types . CmdID {
Value : uuid . NewString ( ) ,
} ,
2023-11-01 14:13:12 +00:00
}
2023-07-20 14:54:04 +00:00
}
2023-11-10 14:05:10 +00:00
2023-11-17 16:49:30 +00:00
func ( svc * Service ) GetMDMWindowsProfilesSummary ( ctx context . Context , teamID * uint ) ( * fleet . MDMProfilesSummary , error ) {
if err := svc . authz . Authorize ( ctx , fleet . MDMConfigProfileAuthz { TeamID : teamID } , fleet . ActionRead ) ; err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
if err := svc . VerifyMDMWindowsConfigured ( ctx ) ; err != nil {
return & fleet . MDMProfilesSummary { } , nil
}
ps , err := svc . ds . GetMDMWindowsProfilesSummary ( ctx , teamID )
if err != nil {
return nil , ctxerr . Wrap ( ctx , err )
}
return ps , nil
}
2023-11-10 14:05:10 +00:00
func ReconcileWindowsProfiles ( ctx context . Context , ds fleet . Datastore , logger kitlog . Logger ) error {
2023-11-30 12:17:07 +00:00
appConfig , err := ds . AppConfig ( ctx )
if err != nil {
return fmt . Errorf ( "reading app config: %w" , err )
}
if ! appConfig . MDM . WindowsEnabledAndConfigured {
return nil
}
2023-11-10 14:05:10 +00:00
// retrieve the profiles to install/remove.
toInstall , err := ds . ListMDMWindowsProfilesToInstall ( ctx )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting profiles to install" )
}
toRemove , err := ds . ListMDMWindowsProfilesToRemove ( ctx )
if err != nil {
return ctxerr . Wrap ( ctx , err , "getting profiles to remove" )
}
// toGetContents contains the IDs of all the profiles from which we
// need to retrieve contents. Since the previous query returns one row
// per host, it would be too expensive to retrieve the profile contents
// there, so we make another request. Using a map to deduplicate.
toGetContents := make ( map [ string ] bool )
2025-03-20 19:43:04 +00:00
// hostProfilesToUpdate tracks each host_mdm_windows_profile we need to upsert
2023-11-10 14:05:10 +00:00
// with the new status, operation_type, etc.
2025-03-20 19:43:04 +00:00
hostProfilesToUpdate := make ( [ ] * fleet . MDMWindowsBulkUpsertHostProfilePayload , 0 , len ( toInstall ) )
2023-11-10 14:05:10 +00:00
2025-08-10 10:24:38 +00:00
// hostProfilesMap provides O(1) lookup for host profiles by host UUID and profile UUID
// Key format: "hostUUID|profileUUID"
hostProfilesMap := make ( map [ string ] * fleet . MDMWindowsBulkUpsertHostProfilePayload , len ( toInstall ) )
2023-12-04 15:04:06 +00:00
// install are maps from profileUUID -> command uuid and host
2023-11-10 14:05:10 +00:00
// UUIDs as the underlying MDM services are optimized to send one command to
// multiple hosts at the same time. Note that the same command uuid is used
// for all hosts in a given install/remove target operation.
type cmdTarget struct {
cmdUUID string
profID string
hostUUIDs [ ] string
}
installTargets := make ( map [ string ] * cmdTarget )
for _ , p := range toInstall {
toGetContents [ p . ProfileUUID ] = true
target := installTargets [ p . ProfileUUID ]
if target == nil {
target = & cmdTarget {
cmdUUID : uuid . New ( ) . String ( ) ,
profID : p . ProfileUUID ,
}
installTargets [ p . ProfileUUID ] = target
}
target . hostUUIDs = append ( target . hostUUIDs , p . HostUUID )
2025-08-10 10:24:38 +00:00
hp := & fleet . MDMWindowsBulkUpsertHostProfilePayload {
2023-11-10 14:05:10 +00:00
ProfileUUID : p . ProfileUUID ,
HostUUID : p . HostUUID ,
ProfileName : p . ProfileName ,
CommandUUID : target . cmdUUID ,
OperationType : fleet . MDMOperationTypeInstall ,
Status : & fleet . MDMDeliveryPending ,
2025-03-20 19:43:04 +00:00
Checksum : p . Checksum ,
2025-08-10 10:24:38 +00:00
}
hostProfilesToUpdate = append ( hostProfilesToUpdate , hp )
// Add to map for fast lookup
hostProfilesMap [ p . HostUUID + "|" + p . ProfileUUID ] = hp
2023-12-04 15:04:06 +00:00
level . Debug ( logger ) . Log ( "msg" , "installing profile" , "profile_uuid" , p . ProfileUUID , "host_id" , p . HostUUID , "name" , p . ProfileName )
2023-11-10 14:05:10 +00:00
}
// Grab the contents of all the profiles we need to install
profileUUIDs := make ( [ ] string , 0 , len ( toGetContents ) )
for pid := range toGetContents {
profileUUIDs = append ( profileUUIDs , pid )
}
profileContents , err := ds . GetMDMWindowsProfilesContents ( ctx , profileUUIDs )
if err != nil {
return ctxerr . Wrap ( ctx , err , "get profile contents" )
}
2023-12-04 15:04:06 +00:00
for profUUID , target := range installTargets {
p , ok := profileContents [ profUUID ]
2023-11-10 14:05:10 +00:00
if ! ok {
// this should never happen
2023-12-04 15:04:06 +00:00
return ctxerr . Wrapf ( ctx , err , "missing profile content for profile %s" , profUUID )
2023-11-10 14:05:10 +00:00
}
2025-08-11 12:47:55 +00:00
if ! variables . ContainsBytes ( p . SyncML ) {
2025-08-10 10:24:38 +00:00
// No Fleet variables, send the same command to all hosts
command , err := buildCommandFromProfileBytes ( p . SyncML , target . cmdUUID )
if err != nil {
level . Info ( logger ) . Log ( "err" , err , "profile_uuid" , profUUID )
continue
}
if err := ds . MDMWindowsInsertCommandForHosts ( ctx , target . hostUUIDs , command ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "inserting commands for hosts" )
}
} else {
// Profile contains Fleet variables, process each host individually
for _ , hostUUID := range target . hostUUIDs {
mapKey := hostUUID + "|" + profUUID
hp := hostProfilesMap [ mapKey ]
if hp == nil {
// This should never happen, but handle gracefully
level . Error ( logger ) . Log ( "msg" , "host profile not found in map" , "profile_uuid" , profUUID , "host_uuid" , hostUUID )
continue
}
// Preprocess the profile content for this specific host
2025-08-11 12:47:55 +00:00
processedContent := microsoft_mdm . PreprocessWindowsProfileContents ( hostUUID , string ( p . SyncML ) )
2025-08-10 10:24:38 +00:00
// Create a unique command UUID for this host since the content is unique
hostCmdUUID := uuid . New ( ) . String ( )
// Build the command with the processed content
command , err := buildCommandFromProfileBytes ( [ ] byte ( processedContent ) , hostCmdUUID )
if err != nil {
level . Info ( logger ) . Log ( "err" , err , "profile_uuid" , profUUID , "host_uuid" , hostUUID )
// Mark this host's profile as failed
hp . Status = & fleet . MDMDeliveryFailed
hp . Detail = fmt . Sprintf ( "Failed to build command from profile: %s" , err . Error ( ) )
continue
}
// Insert the command for this specific host
if err := ds . MDMWindowsInsertCommandForHosts ( ctx , [ ] string { hostUUID } , command ) ; err != nil {
level . Error ( logger ) . Log ( "err" , err , "msg" , "inserting command for host" , "host_uuid" , hostUUID )
// Mark this host's profile as failed
hp . Status = & fleet . MDMDeliveryFailed
hp . Detail = fmt . Sprintf ( "Failed to insert command for host: %s" , err . Error ( ) )
continue
}
// Update the command UUID for this specific host
hp . CommandUUID = hostCmdUUID
}
2023-11-10 14:05:10 +00:00
}
}
2025-03-20 19:43:04 +00:00
// Since we are not using DB transactions here, there is a small chance that the profile contents don't match
// the checksum we retrieved earlier. Update the checksums if needed.
for _ , p := range hostProfilesToUpdate {
if _ , ok := profileContents [ p . ProfileUUID ] ; ok {
p . Checksum = profileContents [ p . ProfileUUID ] . Checksum
}
}
2023-11-10 14:05:10 +00:00
// Windows profiles are just deleted from the DB, the notion of sending
// a command to remove a profile doesn't exist.
if err := ds . BulkDeleteMDMWindowsHostsConfigProfiles ( ctx , toRemove ) ; err != nil {
return ctxerr . Wrap ( ctx , err , "deleting profiles that didn't change" )
}
2025-03-20 19:43:04 +00:00
// Upsert the host profiles we need to track.
if err := ds . BulkUpsertMDMWindowsHostProfiles ( ctx , hostProfilesToUpdate ) ; err != nil {
2023-11-10 14:05:10 +00:00
return ctxerr . Wrap ( ctx , err , "updating host profiles" )
}
return nil
}
2023-11-29 14:32:42 +00:00
// TODO(roberto): I think this should live separately in the
// Windows equivalent of Apple's Commander struct, but I'd like
// to keep it simpler for now until we understand more.
func buildCommandFromProfileBytes ( profileBytes [ ] byte , commandUUID string ) ( * fleet . MDMWindowsCommand , error ) {
rawCommand := [ ] byte ( fmt . Sprintf ( ` <Atomic>%s</Atomic> ` , profileBytes ) )
cmd := new ( mdm_types . SyncMLCmd )
if err := xml . Unmarshal ( rawCommand , cmd ) ; err != nil {
return nil , fmt . Errorf ( "unmarshalling profile: %w" , err )
}
// set the CmdID for the <Atomic> command
2024-02-07 19:24:03 +00:00
cmd . CmdID = mdm_types . CmdID {
Value : commandUUID ,
IncludeFleetComment : true ,
}
2023-11-29 14:32:42 +00:00
// generate a CmdID for any nested <Replace>
for i := range cmd . ReplaceCommands {
2024-02-07 19:24:03 +00:00
cmd . ReplaceCommands [ i ] . CmdID = mdm_types . CmdID {
Value : uuid . NewString ( ) ,
IncludeFleetComment : true ,
}
2023-11-29 14:32:42 +00:00
}
2024-03-12 19:21:17 +00:00
// generate a CmdID for any nested <Add>
for i := range cmd . AddCommands {
cmd . AddCommands [ i ] . CmdID = mdm_types . CmdID {
Value : uuid . NewString ( ) ,
IncludeFleetComment : true ,
}
}
2023-11-29 14:32:42 +00:00
rawCommand , err := xml . Marshal ( cmd )
if err != nil {
return nil , fmt . Errorf ( "marshalling command: %w" , err )
}
command := & fleet . MDMWindowsCommand {
CommandUUID : commandUUID ,
RawCommand : rawCommand ,
// Atomic commands don't have a Target element.
TargetLocURI : "" ,
}
return command , nil
}
2025-04-18 16:13:30 +00:00
// truncateString truncates a string to maxLen characters, adding "..." if truncated
func truncateString ( s string , maxLen int ) string {
if len ( s ) <= maxLen {
return s
}
return s [ : maxLen ] + "..."
}