mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
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:
parent
52eb9df88c
commit
501ef480b0
8 changed files with 152 additions and 28 deletions
1
changes/issue-12600-windows-installer
Normal file
1
changes/issue-12600-windows-installer
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Add MSI installer deployement support through MS-MDM
|
||||
1
changes/issue-12604-azure-tos-endpoint
Normal file
1
changes/issue-12604-azure-tos-endpoint
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Adding support for MDM TOS endpoint
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue