Windows mdm TOS endpoint (#12900)

This relates to https://github.com/fleetdm/fleet/issues/12604 and
https://github.com/fleetdm/fleet/issues/12600

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
This commit is contained in:
Marcos Oviedo 2023-07-21 14:36:26 -03:00 committed by GitHub
parent 52eb9df88c
commit 501ef480b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 152 additions and 28 deletions

View file

@ -0,0 +1 @@
* Add MSI installer deployement support through MS-MDM

View file

@ -0,0 +1 @@
* Adding support for MDM TOS endpoint

View file

@ -223,11 +223,6 @@ func (req *SoapRequest) IsValidRequestSecurityTokenMsg() error {
return errors.New("invalid requestsecuritytoken message: AdditionalContext.ContextItems missing")
}
reqVersion, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemRequestVersion)
if err != nil || (reqVersion != mdm.EnrollmentVersionV5 && reqVersion != mdm.EnrollmentVersionV4) {
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemRequestVersion, reqVersion, err)
}
reqEnrollType, err := req.Body.RequestSecurityToken.GetContextItem(mdm.ReqSecTokenContextItemEnrollmentType)
if err != nil || reqEnrollType != mdm.ReqSecTokenEnrollType {
return fmt.Errorf("invalid requestsecuritytoken message %s: %s - %v", mdm.ReqSecTokenContextItemEnrollmentType, reqEnrollType, err)

View file

@ -780,4 +780,7 @@ type Service interface {
// GetMDMWindowsManagementResponse returns a valid SyncML response message
GetMDMWindowsManagementResponse(ctx context.Context, reqSyncML *SyncMLMessage) (*string, error)
// GetMDMWindowsTOSContent returns TOS content
GetMDMWindowsTOSContent(ctx context.Context, redirectUri string, reqID string) (string, error)
}

View file

@ -42,6 +42,9 @@ const (
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/5b02c625-ced2-4a01-a8e1-da0ae84f5bb7
MDE2ManagementPath = MDMPath + "/management"
// MDE2TOSPath is the HTTP endpoint path that delivers Terms of Service Content
MDE2TOSPath = MDMPath + "/tos"
// These are the entry points for the Microsoft Device Enrollment (MS-MDE) and Microsoft Device Enrollment v2 (MS-MDE2) protocols.
// These are required to be implemented by the MDM server to support user-driven enrollments
MSEnrollEntryPoint = "/EnrollmentServer/Discovery.svc"
@ -138,6 +141,9 @@ const (
// HTTP Content Type for SyncML MDM responses
SyncMLContentType = "application/vnd.syncml.dm+xml"
// HTTP Content Type for Webcontainer responses
WebContainerContentType = "text/html; charset=UTF-8"
// Minimal Key Length for SHA1WithRSA encryption
PolicyMinKeyLength = "2048"
@ -232,6 +238,12 @@ const (
// Login related query param expected by STS Auth endpoint
STSLoginHint = "login_hint"
// redirect_uri query param expected by TOS endpoint
TOCRedirectURI = "redirect_uri"
// client-request-id query param expected by TOS endpoint
TOCReqID = "client-request-id"
// Alert Command IDs
DeviceUnenrollmentID = "1226"
HostInitMessageID = "1201"

View file

@ -614,6 +614,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// It should be authenticated through TLS headers once proper implementation is in place
neWindowsMDM.POST(microsoft_mdm.MDE2ManagementPath, mdmMicrosoftManagementEndpoint, SyncMLReqMsgContainer{})
// This endpoint is unauthenticated and is used by to retrieve the MDM enrollment Terms of Use
neWindowsMDM.GET(microsoft_mdm.MDE2TOSPath, mdmMicrosoftTOSEndpoint, MDMWebContainer{})
ne.POST("/api/fleet/orbit/enroll", enrollOrbitEndpoint, EnrollOrbitRequest{})
// For some reason osquery does not provide a node key with the block data.

View file

@ -6181,6 +6181,22 @@ func (s *integrationMDMTestSuite) TestInvalidGetAuthRequest() {
require.Contains(t, resContent, "forbidden")
}
func (s *integrationMDMTestSuite) TestValidGetTOC() {
t := s.T()
resp := s.DoRaw("GET", microsoft_mdm.MDE2TOSPath+"?api-version=1.0&redirect_uri=ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin&client-request-id=f2cf3127-1e80-4d73-965d-42a3b84bdb40", nil, http.StatusOK)
resBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, resp.Header["Content-Type"], microsoft_mdm.WebContainerContentType)
resTOCcontent := string(resBytes)
require.Contains(t, resTOCcontent, "Microsoft.AAD.BrokerPlugin")
require.Contains(t, resTOCcontent, "IsAccepted=true")
require.Contains(t, resTOCcontent, "OpaqueBlob=")
}
func (s *integrationMDMTestSuite) TestValidSyncMLRequestNoAuth() {
t := s.T()

View file

@ -10,12 +10,12 @@ import (
"errors"
"fmt"
"html"
"html/template"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"text/template"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
@ -134,6 +134,43 @@ func (r SyncMLResponseMsgContainer) hijackRender(ctx context.Context, w http.Res
}
}
type MDMWebContainer struct {
Data *string
Params url.Values
Err error
}
// MDM SOAP request decoder
func (req *MDMWebContainer) DecodeBody(ctx context.Context, r io.Reader, u url.Values) error {
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
}
func (req MDMWebContainer) error() error { return req.Err }
// hijackRender writes the response header and the RAW HTML output
func (req MDMWebContainer) hijackRender(ctx context.Context, w http.ResponseWriter) {
resData := []byte(*req.Data + "\n")
w.Header().Set("Content-Type", mdm.WebContainerContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(resData)))
w.WriteHeader(http.StatusOK)
if n, err := w.Write(resData); err != nil {
logging.WithExtras(ctx, "err", err, "written", n)
}
}
type MDMAuthContainer struct {
Data *string
Err error
@ -163,7 +200,7 @@ func getUtcTime(minutes int) string {
}
// NewDiscoverResponse creates a new DiscoverResponse struct based on the auth policy, policy url, and enrollment url
func NewDiscoverResponse(authPolicy string, policyUrl string, enrollmentUrl string, authUrl *string) (mdm_types.DiscoverResponse, error) {
func NewDiscoverResponse(authPolicy string, policyUrl string, enrollmentUrl string) (mdm_types.DiscoverResponse, error) {
if (len(authPolicy) == 0) || (len(policyUrl) == 0) || (len(enrollmentUrl) == 0) {
return mdm_types.DiscoverResponse{}, errors.New("invalid parameters")
}
@ -175,7 +212,6 @@ func NewDiscoverResponse(authPolicy string, policyUrl string, enrollmentUrl stri
EnrollmentVersion: mdm.EnrollmentVersionV4,
EnrollmentPolicyServiceUrl: policyUrl,
EnrollmentServiceUrl: enrollmentUrl,
AuthServiceUrl: authUrl,
},
}, nil
}
@ -843,6 +879,32 @@ func mdmMicrosoftManagementEndpoint(ctx context.Context, request interface{}, sv
}, nil
}
// mdmMicrosoftTOSEndpoint handles the TOS content for the incoming MDM enrollment request
func mdmMicrosoftTOSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
params := request.(*MDMWebContainer).Params
// Sanity check on the expected query params
if !params.Has(mdm.TOCRedirectURI) || !params.Has(mdm.TOCReqID) {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, errors.New("invalid params"))
return getSoapResponseFault(mdm.SoapErrorInternalServiceFault, soapFault), nil
}
redirectURI := params.Get(mdm.TOCRedirectURI)
reqID := params.Get(mdm.TOCReqID)
// Getting the TOS content message
resTOCData, err := svc.GetMDMWindowsTOSContent(ctx, redirectURI, reqID)
if err != nil {
soapFault := svc.GetAuthorizedSoapFault(ctx, mdm.SoapErrorMessageFormat, mdm_types.MDEEnrollment, err)
return getSoapResponseFault(mdm.SoapErrorInternalServiceFault, soapFault), nil
}
return MDMWebContainer{
Data: &resTOCData,
Err: nil,
}, nil
}
// authBinarySecurityToken checks if the provided token is valid
func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *fleet.HeaderBinarySecurityToken) (string, error) {
if authToken == nil {
@ -936,18 +998,7 @@ func (svc *Service) GetMDMMicrosoftDiscoveryResponse(ctx context.Context, upnEma
return nil, ctxerr.Wrap(ctx, err, "resolve enroll endpoint")
}
// Only adding STS Auth endpoint if the UPN email is provided
var urlSTSAuthEndpoint *string
if len(upnEmail) > 0 {
workUrlSTSAuthEndpoint, err := mdm.ResolveWindowsMDMAuth(appCfg.ServerSettings.ServerURL)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "resolve enroll endpoint")
}
urlSTSAuthEndpoint = &workUrlSTSAuthEndpoint
}
discoveryMsg, err := NewDiscoverResponse(mdm.AuthOnPremise, urlPolicyEndpoint, urlEnrollEndpoint, urlSTSAuthEndpoint)
discoveryMsg, err := NewDiscoverResponse(mdm.AuthOnPremise, urlPolicyEndpoint, urlEnrollEndpoint)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "creation of DiscoverResponse message")
}
@ -979,12 +1030,12 @@ func (svc *Service) GetMDMMicrosoftSTSAuthResponse(ctx context.Context, appru st
// Dinamically create a form element to submit the request
var form = document.createElement('form');
form.method = 'POST';
form.action = "` + appru + `"
form.action = "{{.ActionURL}}"
var inputToken = document.createElement('input');
inputToken.type = 'hidden';
inputToken.name = 'wresult';
inputToken.value = '` + encodedBST + `';
inputToken.value = '{{.Token}}';
form.appendChild(inputToken);
// Submit the form
@ -1001,7 +1052,7 @@ func (svc *Service) GetMDMMicrosoftSTSAuthResponse(ctx context.Context, appru st
}
var htmlBuf bytes.Buffer
err = tmpl.Execute(&htmlBuf, map[string][]byte{"ActionURL": []byte(appru), "Token": []byte(encodedBST)})
err = tmpl.Execute(&htmlBuf, map[string]string{"ActionURL": appru, "Token": encodedBST})
if err != nil {
return "", ctxerr.Wrap(ctx, err, "creation of STS content")
}
@ -1112,6 +1163,52 @@ func (svc *Service) GetMDMWindowsManagementResponse(ctx context.Context, reqSync
return resSyncMLmsg, nil
}
// GetMDMWindowsTOSContent returns valid TOC content
func (svc *Service) GetMDMWindowsTOSContent(ctx context.Context, redirectUri string, reqID string) (string, error) {
tmpl, err := template.New("").Parse(`
<html>
<head>
<style>
button {
background-color: #008CBA;
color: white;
padding: 10px 60px;
border: none;
cursor: pointer;
}
</style>
</head>
<body>
<center>
<img src="https://fleetdm.com/images/logo-blue-162x92@2x.png">
<br>
<h2>Terms and conditions</h2>
<br> Terms and Conditions PDF content should go here <center>
<br>
<button type="button" onClick="acceptBtn()">Accept</button>
<script>
function acceptBtn() {
window.location = "{{.RedirectURL}}" + "?IsAccepted=true&OpaqueBlob={{.ClientData}}";
}
</script>
</body>
</html>`)
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
}
func (svc *Service) getManagementResponse(ctx context.Context, reqSyncML *fleet.SyncMLMessage) (*string, error) {
if reqSyncML == nil {
return nil, fleet.NewInvalidArgumentError("syncml req message", "message is not present")
@ -1371,7 +1468,7 @@ func (svc *Service) storeWindowsMDMEnrolledDevice(ctx context.Context, userID st
// Getting the Enroll RequestVersion context information from the RequestSecurityToken msg
reqEnrollVersion, err := GetContextItem(secTokenMsg, mdm.ReqSecTokenContextItemRequestVersion)
if err != nil {
return fmt.Errorf("%s %v", error_tag, err)
reqEnrollVersion = "request_version_not_present"
}
// Getting the RequestVersion context information from the RequestSecurityToken msg
@ -1457,10 +1554,6 @@ func (svc *Service) SignMDMMicrosoftClientCSR(ctx context.Context, subject strin
}
func (svc *Service) getConfigProfilesToEnforce(ctx context.Context, commandID *int) string {
// fleetctl package
// --fleet-url=https://dashboard.fleetdm.ngrok.dev
// --enroll-secret=6EM269jFhXlEcWn9nr/kCQGNa5sIh3GM
// Getting the management URL
appCfg, _ := svc.ds.AppConfig(ctx)
fleetEnrollUrl := appCfg.ServerSettings.ServerURL