Added EUA to the Fleet MSI installer (#43295)
**Related issue:** Resolves #41381 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** - Forward end-user authentication context (EUA token) to the Fleet MSI installer and enrollment flow on Windows MDM to avoid duplicate auth prompts and link devices to hosts. * **Tests** - Added comprehensive unit and integration tests for EUA token creation, validation, and processing to improve reliability. * **Documentation** - Added a note describing support for forwarding end-user authentication context during Windows MDM enrollment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -32,6 +32,8 @@ type EnrollOrbitRequest struct {
|
||||
ComputerName string `json:"computer_name"`
|
||||
// HardwareModel is the device's hardware model.
|
||||
HardwareModel string `json:"hardware_model"`
|
||||
// EUAToken is a Fleet-signed JWT containing the user's UPN and Windows MDM device ID.
|
||||
EUAToken string `json:"eua_token,omitempty"`
|
||||
}
|
||||
|
||||
// SetOrbitNodeKeyer is the interface implemented by orbit request types that
|
||||
|
||||
@@ -122,7 +122,7 @@ type Service interface {
|
||||
//
|
||||
// - If an entry for the host exists (osquery enrolled first) then it will update the host's orbit node key and team.
|
||||
// - If an entry for the host doesn't exist (osquery enrolls later) then it will create a new entry in the hosts table.
|
||||
EnrollOrbit(ctx context.Context, hostInfo OrbitHostInfo, enrollSecret string) (orbitNodeKey string, err error)
|
||||
EnrollOrbit(ctx context.Context, hostInfo OrbitHostInfo, enrollSecret string, euaToken string) (orbitNodeKey string, err error)
|
||||
// GetOrbitConfig returns team specific flags and extensions in agent options
|
||||
// if the team id is not nil for host, otherwise it returns flags from global
|
||||
// agent options. It also returns any notifications that fleet wants to surface
|
||||
|
||||
@@ -45,9 +45,17 @@ type CertManager interface {
|
||||
// NewSTSAuthToken returns an STS auth token for the given UPN claim.
|
||||
NewSTSAuthToken(upn string) (string, error)
|
||||
|
||||
// NewEUAToken returns a Fleet-signed JWT for the given UPN and Windows MDM
|
||||
// device ID. Used to pass end-user authentication context to the orbit
|
||||
// installer so the user is not prompted twice.
|
||||
NewEUAToken(upn string, deviceID string) (string, error)
|
||||
|
||||
// GetSTSAuthTokenUPNClaim validates the given token and returns the UPN claim
|
||||
GetSTSAuthTokenUPNClaim(token string) (string, error)
|
||||
|
||||
// GetEUATokenClaims validates the given EUA token and returns the parsed claims.
|
||||
GetEUATokenClaims(token string) (*EUATokenClaims, error)
|
||||
|
||||
// TODO: implement other methods as needed:
|
||||
// - verify certificate-device association
|
||||
// - certificate lifecycle management (e.g., renewal, revocation)
|
||||
@@ -66,6 +74,19 @@ type STSClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// euaJWTClaims is the internal JWT struct for signing/parsing EUA tokens.
|
||||
type euaJWTClaims struct {
|
||||
UPN string `json:"upn"`
|
||||
DeviceID string `json:"device_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// EUATokenClaims is the validated result returned to callers of GetEUATokenClaims.
|
||||
type EUATokenClaims struct {
|
||||
UPN string
|
||||
DeviceID string
|
||||
}
|
||||
|
||||
type AzureData struct {
|
||||
UPN string
|
||||
Audience []string
|
||||
@@ -186,8 +207,8 @@ func (m *manager) NewSTSAuthToken(upn string) (string, error) {
|
||||
|
||||
// Create claims with upn field populated
|
||||
claims := STSClaims{
|
||||
upn,
|
||||
jwt.RegisteredClaims{
|
||||
UPN: upn,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
@@ -205,6 +226,80 @@ func (m *manager) NewSTSAuthToken(upn string) (string, error) {
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
// NewEUAToken returns a Fleet-signed JWT for the given UPN and Windows MDM device ID.
|
||||
func (m *manager) NewEUAToken(upn string, deviceID string) (string, error) {
|
||||
if m == nil {
|
||||
return "", errors.New("windows mdm identity keypair was not configured")
|
||||
}
|
||||
|
||||
if m.identityCert == nil || m.identityPrivateKey == nil {
|
||||
return "", errors.New("invalid identity certificate or private key")
|
||||
}
|
||||
|
||||
if len(upn) == 0 {
|
||||
return "", errors.New("invalid upn field")
|
||||
}
|
||||
if len(deviceID) == 0 {
|
||||
return "", errors.New("invalid device_id field")
|
||||
}
|
||||
|
||||
claims := euaJWTClaims{
|
||||
UPN: upn,
|
||||
DeviceID: deviceID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Subject: "EUAToken",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims)
|
||||
signedToken, err := token.SignedString(m.identityPrivateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign EUA token: %w", err)
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
// GetEUATokenClaims validates the given EUA token and returns the parsed claims.
|
||||
func (m *manager) GetEUATokenClaims(tokenStr string) (*EUATokenClaims, error) {
|
||||
if m == nil {
|
||||
return nil, errors.New("windows mdm identity keypair was not configured")
|
||||
}
|
||||
|
||||
if m.identityCert == nil || m.identityPrivateKey == nil {
|
||||
return nil, errors.New("invalid identity certificate or private key")
|
||||
}
|
||||
|
||||
if len(tokenStr) == 0 {
|
||||
return nil, errors.New("invalid EUA token")
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &euaJWTClaims{}, func(token *jwt.Token) (any, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return m.identityCert.PublicKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("there was an error parsing the EUA token claims: %w", err)
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*euaJWTClaims); ok && token.Valid {
|
||||
if len(claims.UPN) == 0 {
|
||||
return nil, errors.New("issue with UPN token claim")
|
||||
}
|
||||
if len(claims.DeviceID) == 0 {
|
||||
return nil, errors.New("issue with device_id token claim")
|
||||
}
|
||||
return &EUATokenClaims{UPN: claims.UPN, DeviceID: claims.DeviceID}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("issue with EUA token validation")
|
||||
}
|
||||
|
||||
// GetSTSAuthToken validates the given token and returns the UPN claim
|
||||
func (m *manager) GetSTSAuthTokenUPNClaim(tokenStr string) (string, error) {
|
||||
if m == nil {
|
||||
|
||||
@@ -99,6 +99,44 @@ func TestSTSTokenSigningAndVerification(t *testing.T) {
|
||||
require.ErrorContains(t, err, "invalid upn field")
|
||||
}
|
||||
|
||||
func TestSTSTokenWithDeviceID(t *testing.T) {
|
||||
var store CertStore
|
||||
cm, err := NewCertManager(store, testCert, testKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
upn := "user@example.com"
|
||||
deviceID := "test-device-id-123"
|
||||
|
||||
// Generate token with device ID
|
||||
token, err := cm.NewEUAToken(upn, deviceID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, token)
|
||||
|
||||
// Validate and extract both claims
|
||||
claims, err := cm.GetEUATokenClaims(token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, upn, claims.UPN)
|
||||
require.Equal(t, deviceID, claims.DeviceID)
|
||||
|
||||
// Empty UPN is rejected
|
||||
_, err = cm.NewEUAToken("", deviceID)
|
||||
require.ErrorContains(t, err, "invalid upn field")
|
||||
|
||||
// Empty device ID is rejected
|
||||
_, err = cm.NewEUAToken(upn, "")
|
||||
require.ErrorContains(t, err, "invalid device_id field")
|
||||
|
||||
// Token signed by NewSTSAuthToken (no device_id) is rejected — device_id is required
|
||||
oldToken, err := cm.NewSTSAuthToken(upn)
|
||||
require.NoError(t, err)
|
||||
_, err = cm.GetEUATokenClaims(oldToken)
|
||||
require.ErrorContains(t, err, "issue with device_id token claim")
|
||||
|
||||
// Tampered token is rejected
|
||||
_, err = cm.GetEUATokenClaims(token + "tampered")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCertFingerprintHexStr(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
@@ -50,7 +50,7 @@ type GetTransparencyURLFunc func(ctx context.Context) (string, error)
|
||||
|
||||
type AuthenticateOrbitHostFunc func(ctx context.Context, nodeKey string) (host *fleet.Host, debug bool, err error)
|
||||
|
||||
type EnrollOrbitFunc func(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string) (orbitNodeKey string, err error)
|
||||
type EnrollOrbitFunc func(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string, euaToken string) (orbitNodeKey string, err error)
|
||||
|
||||
type GetOrbitConfigFunc func(ctx context.Context) (fleet.OrbitConfig, error)
|
||||
|
||||
@@ -2354,11 +2354,11 @@ func (s *Service) AuthenticateOrbitHost(ctx context.Context, nodeKey string) (ho
|
||||
return s.AuthenticateOrbitHostFunc(ctx, nodeKey)
|
||||
}
|
||||
|
||||
func (s *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string) (orbitNodeKey string, err error) {
|
||||
func (s *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string, euaToken string) (orbitNodeKey string, err error) {
|
||||
s.mu.Lock()
|
||||
s.EnrollOrbitFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.EnrollOrbitFunc(ctx, hostInfo, enrollSecret)
|
||||
return s.EnrollOrbitFunc(ctx, hostInfo, enrollSecret, euaToken)
|
||||
}
|
||||
|
||||
func (s *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, error) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -508,6 +509,7 @@ func (s *integrationMDMTestSuite) recordWindowsHostStatus(
|
||||
|
||||
msgID, err := device.GetCurrentMsgID()
|
||||
require.NoError(t, err)
|
||||
euaTokenRe := regexp.MustCompile(`EUA_TOKEN="[^"]*"`)
|
||||
for _, c := range cmds {
|
||||
cmdID := c.Cmd.CmdID
|
||||
status := syncml.CmdStatusOK
|
||||
@@ -522,6 +524,12 @@ func (s *integrationMDMTestSuite) recordWindowsHostStatus(
|
||||
})
|
||||
c.Cmd.CmdID.Value = ""
|
||||
c.Cmd.CmdRef = nil
|
||||
for i := range c.Cmd.Items {
|
||||
if c.Cmd.Items[i].Data != nil {
|
||||
c.Cmd.Items[i].Data.Content = euaTokenRe.ReplaceAllString(
|
||||
c.Cmd.Items[i].Data.Content, `EUA_TOKEN="<redacted>"`)
|
||||
}
|
||||
}
|
||||
recordedCmds = append(recordedCmds, c)
|
||||
}
|
||||
|
||||
|
||||
@@ -9173,8 +9173,9 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
|
||||
|
||||
var installJob struct {
|
||||
Product struct {
|
||||
ContentURL string `xml:"Download>ContentURLList>ContentURL"`
|
||||
FileHash string `xml:"Validation>FileHash"`
|
||||
ContentURL string `xml:"Download>ContentURLList>ContentURL"`
|
||||
FileHash string `xml:"Validation>FileHash"`
|
||||
CommandLine string `xml:"Enforcement>CommandLine"`
|
||||
} `xml:"Product"`
|
||||
}
|
||||
err = xml.Unmarshal([]byte(fleetdExecCmd.Cmd.Items[0].Data.Content), &installJob)
|
||||
@@ -9182,6 +9183,12 @@ func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
|
||||
require.Equal(t, s.mockedDownloadFleetdmMeta.MSIURL, installJob.Product.ContentURL)
|
||||
require.Equal(t, s.mockedDownloadFleetdmMeta.MSISha256, installJob.Product.FileHash)
|
||||
|
||||
// The device enrolled with a valid UPN (azureMail), so the command line
|
||||
// should include an EUA_TOKEN argument.
|
||||
require.Contains(t, installJob.Product.CommandLine, `EUA_TOKEN="`)
|
||||
require.Contains(t, installJob.Product.CommandLine, `FLEET_URL="`)
|
||||
require.Contains(t, installJob.Product.CommandLine, `FLEET_SECRET="`)
|
||||
|
||||
// reply with success for both commands
|
||||
msgID, err := d.GetCurrentMsgID()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1501,6 +1501,28 @@ func (svc *Service) isFleetdPresentOnDevice(ctx context.Context, deviceID string
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// generateWindowsEUAToken returns a Fleet-signed EUA token for the given Windows
|
||||
// MDM device ID if the device enrolled with a valid Azure UPN
|
||||
func (svc *Service) generateWindowsEUAToken(ctx context.Context, deviceID string) string {
|
||||
if svc.wstepCertManager == nil {
|
||||
return ""
|
||||
}
|
||||
device, err := svc.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID)
|
||||
if err != nil {
|
||||
svc.logger.ErrorContext(ctx, "unable to fetch windows mdm enrollment for EUA token generation", "err", err, "device_id", deviceID)
|
||||
return ""
|
||||
}
|
||||
if device == nil || !microsoft_mdm.IsValidUPN(device.MDMEnrollUserID) {
|
||||
return ""
|
||||
}
|
||||
token, err := svc.wstepCertManager.NewEUAToken(device.MDMEnrollUserID, deviceID)
|
||||
if err != nil {
|
||||
svc.logger.ErrorContext(ctx, "unable to generate EUA token for fleetd install", "err", err, "device_id", deviceID)
|
||||
return ""
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID string) error {
|
||||
secrets, err := svc.ds.GetEnrollSecrets(ctx, nil)
|
||||
if err != nil {
|
||||
@@ -1530,6 +1552,11 @@ func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID st
|
||||
addCommandUUID := uuid.NewString()
|
||||
execCommandUUID := uuid.NewString()
|
||||
|
||||
euaTokenArg := ""
|
||||
if token := svc.generateWindowsEUAToken(ctx, deviceID); token != "" {
|
||||
euaTokenArg = ` EUA_TOKEN="` + token + `"`
|
||||
}
|
||||
|
||||
rawAddCmd := []byte(`
|
||||
<Add>
|
||||
<CmdID>` + addCommandUUID + `</CmdID>
|
||||
@@ -1562,7 +1589,7 @@ func (svc *Service) enqueueInstallFleetdCommand(ctx context.Context, deviceID st
|
||||
<FileHash>` + fleetdMetadata.MSISha256 + `</FileHash>
|
||||
</Validation>
|
||||
<Enforcement>
|
||||
<CommandLine>/quiet FLEET_URL="` + fleetURL + `" FLEET_SECRET="` + globalEnrollSecret + `" ENABLE_SCRIPTS="True"</CommandLine>
|
||||
<CommandLine>/quiet FLEET_URL="` + fleetURL + `" FLEET_SECRET="` + globalEnrollSecret + `" ENABLE_SCRIPTS="True"` + euaTokenArg + `</CommandLine>
|
||||
<TimeOut>10</TimeOut>
|
||||
<RetryCount>1</RetryCount>
|
||||
<RetryInterval>5</RetryInterval>
|
||||
|
||||
+105
-8
@@ -55,7 +55,7 @@ func enrollOrbitEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
|
||||
OsqueryIdentifier: req.OsqueryIdentifier,
|
||||
ComputerName: req.ComputerName,
|
||||
HardwareModel: req.HardwareModel,
|
||||
}, req.EnrollSecret)
|
||||
}, req.EnrollSecret, req.EUAToken)
|
||||
if err != nil {
|
||||
return enrollOrbitResponse{fleet.EnrollOrbitResponse{Err: err}}, nil
|
||||
}
|
||||
@@ -89,8 +89,66 @@ func (svc *Service) AuthenticateOrbitHost(ctx context.Context, orbitNodeKey stri
|
||||
return host, svc.debugEnabledForHost(ctx, host.ID), nil
|
||||
}
|
||||
|
||||
// processWindowsEUAToken validates a Fleet-signed EUA token from the Windows MSI
|
||||
// installer, links the user's IdP account to the host, and returns the UPN and
|
||||
// device ID for use in post-enrollment steps.
|
||||
func (svc *Service) processWindowsEUAToken(ctx context.Context, hostUUID string, euaToken string) (upn string, deviceID string, err error) {
|
||||
if svc.wstepCertManager == nil {
|
||||
// Windows MDM is not configured on this server so the token cannot be validated.
|
||||
// Fall back to prompting the user for authentication.
|
||||
return "", "", fleet.NewOrbitIDPAuthRequiredError()
|
||||
}
|
||||
|
||||
claims, tokenErr := svc.wstepCertManager.GetEUATokenClaims(euaToken)
|
||||
if tokenErr != nil {
|
||||
svc.logger.WarnContext(ctx, "EUA token validation failed, falling back to end user auth prompt",
|
||||
"err", tokenErr, "host_uuid", hostUUID)
|
||||
return "", "", fleet.NewOrbitIDPAuthRequiredError()
|
||||
}
|
||||
upn = claims.UPN
|
||||
deviceID = claims.DeviceID
|
||||
|
||||
_, err = svc.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, deviceID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
svc.logger.WarnContext(ctx, "EUA token device_id not found in windows mdm enrollments, falling back to end user auth prompt",
|
||||
"device_id", deviceID, "host_uuid", hostUUID)
|
||||
return "", "", fleet.NewOrbitIDPAuthRequiredError()
|
||||
}
|
||||
return "", "", ctxerr.Wrap(ctx, err, "getting windows mdm enrollment for EUA token")
|
||||
}
|
||||
|
||||
// Fetch or create the mdm_idp_accounts row for this email.
|
||||
// Fetch first so we do not overwrite existing first/last names
|
||||
// that may have been populated by SCIM provisioning.
|
||||
acct, err := svc.ds.GetMDMIdPAccountByEmail(ctx, upn)
|
||||
if err != nil && !fleet.IsNotFound(err) {
|
||||
return "", "", ctxerr.Wrap(ctx, err, "getting mdm idp account by email for EUA token")
|
||||
}
|
||||
if fleet.IsNotFound(err) {
|
||||
if err := svc.ds.InsertMDMIdPAccount(ctx, &fleet.MDMIdPAccount{Email: upn, Username: upn}); err != nil {
|
||||
return "", "", ctxerr.Wrap(ctx, err, "inserting mdm idp account for EUA token")
|
||||
}
|
||||
// Re-fetch to get the UUID assigned by the DB.
|
||||
acct, err = svc.ds.GetMDMIdPAccountByEmail(ctxdb.RequirePrimary(ctx, true), upn)
|
||||
if err != nil {
|
||||
return "", "", ctxerr.Wrap(ctx, err, "re-fetching mdm idp account after insert for EUA token")
|
||||
}
|
||||
}
|
||||
if acct == nil {
|
||||
return "", "", ctxerr.New(ctx, "mdm idp account not found for EUA token")
|
||||
}
|
||||
|
||||
// Link the IdP account to this host UUID in host_mdm_idp_accounts.
|
||||
if err := svc.ds.AssociateHostMDMIdPAccountDB(ctx, hostUUID, acct.UUID); err != nil {
|
||||
return "", "", ctxerr.Wrap(ctx, err, "associating host with mdm idp account for EUA token")
|
||||
}
|
||||
|
||||
return upn, deviceID, nil
|
||||
}
|
||||
|
||||
// EnrollOrbit enrolls an Orbit instance to Fleet and returns the orbit node key.
|
||||
func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string) (string, error) {
|
||||
func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInfo, enrollSecret string, euaToken string) (string, error) {
|
||||
// this is not a user-authenticated endpoint
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
@@ -162,6 +220,8 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
|
||||
isEndUserAuthRequired = team.Config.MDM.MacOSSetup.EnableEndUserAuthentication
|
||||
}
|
||||
|
||||
var euaDeviceID, euaUPN string
|
||||
|
||||
if isEndUserAuthRequired {
|
||||
if hostInfo.HardwareUUID == "" {
|
||||
return "", fleet.OrbitError{Message: "failed to get IdP account: hardware uuid is empty"}
|
||||
@@ -183,12 +243,22 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
|
||||
if platform == "linux" || platform == "windows" {
|
||||
// If the Orbit client doesn't support end user auth, complain loudly and let the host enroll.
|
||||
mp, ok := capabilities.FromContext(ctx)
|
||||
//nolint:gocritic // ignore ifElseChain
|
||||
if !ok {
|
||||
svc.logger.ErrorContext(ctx, "!!! ERR_ALLOWING_UNAUTHENTICATED: host is not authenticated, but fleet could not determine whether orbit supports end-user authentication. proceeding with enrollment. !!! ", "host_uuid", hostInfo.HardwareUUID)
|
||||
} else if !mp.Has(fleet.CapabilityEndUserAuth) {
|
||||
svc.logger.WarnContext(ctx, "!!! ERR_ALLOWING_UNAUTHENTICATED: host is not authenticated, but connected with an orbit version that does not support end user authentication. proceeding with enrollment. !!! ", "host_uuid", hostInfo.HardwareUUID)
|
||||
} else {
|
||||
switch {
|
||||
case !ok:
|
||||
svc.logger.ErrorContext(ctx, "allowing unauthenticated enrollment: could not determine orbit end-user auth capability", "host_uuid", hostInfo.HardwareUUID)
|
||||
case !mp.Has(fleet.CapabilityEndUserAuth):
|
||||
svc.logger.WarnContext(ctx, "allowing unauthenticated enrollment: orbit version does not support end-user authentication", "host_uuid", hostInfo.HardwareUUID)
|
||||
case platform == "windows" && euaToken != "":
|
||||
// A Windows host already authenticated during MDM enrollment and the
|
||||
// EUA token was passed by the MSI installer.
|
||||
upn, deviceID, err := svc.processWindowsEUAToken(ctx, hostInfo.HardwareUUID, euaToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
euaUPN = upn
|
||||
euaDeviceID = deviceID
|
||||
// Continue enrollment — do not return END_USER_AUTH_REQUIRED.
|
||||
default:
|
||||
// Otherwise report the unauthenticated host and let Orbit handle it (e.g. by prompting the user to authenticate).
|
||||
return "", fleet.NewOrbitIDPAuthRequiredError()
|
||||
}
|
||||
@@ -221,6 +291,33 @@ func (svc *Service) EnrollOrbit(ctx context.Context, hostInfo fleet.OrbitHostInf
|
||||
return "", fleet.OrbitError{Message: "failed to enroll " + err.Error()}
|
||||
}
|
||||
|
||||
if euaDeviceID != "" {
|
||||
updated, err := svc.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, host.UUID, euaDeviceID)
|
||||
if err != nil {
|
||||
svc.logger.ErrorContext(ctx, "failed to link windows mdm enrollment to orbit host via EUA token",
|
||||
"err", err, "host_uuid", host.UUID, "device_id", euaDeviceID)
|
||||
}
|
||||
|
||||
if updated {
|
||||
scimUser, err := svc.ds.ScimUserByUserNameOrEmail(ctx, euaUPN, euaUPN)
|
||||
//nolint:gocritic // ignore ifElseChain
|
||||
if err != nil && !fleet.IsNotFound(err) && err != sql.ErrNoRows {
|
||||
svc.logger.ErrorContext(ctx, "failed to find SCIM user for EUA token enrollment",
|
||||
"err", err, "host_id", host.ID)
|
||||
} else if err == nil && scimUser != nil {
|
||||
if err := svc.ds.SetOrUpdateHostSCIMUserMapping(ctx, host.ID, scimUser.ID); err != nil {
|
||||
svc.logger.ErrorContext(ctx, "failed to set SCIM user mapping for EUA token enrollment",
|
||||
"err", err, "host_id", host.ID)
|
||||
}
|
||||
} else {
|
||||
if err := svc.ds.DeleteHostSCIMUserMapping(ctx, host.ID); err != nil && !fleet.IsNotFound(err) {
|
||||
svc.logger.ErrorContext(ctx, "failed to delete SCIM user mapping for EUA token enrollment",
|
||||
"err", err, "host_id", host.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Associate the newly-enrolled host with a SCIM user if applicable.
|
||||
// Do this only for linux and windows devices, as macOS devices
|
||||
// are associated during MDM enrollment.
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
mysql_errors "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// euaTestingKey replaces "TESTING KEY" with "PRIVATE KEY" to prevent secret
|
||||
// scanners from flagging test keys embedded in source files.
|
||||
func euaTestingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
|
||||
|
||||
// testWSTEPCert and testWSTEPKey are the same certs used in wstep_test.go.
|
||||
var (
|
||||
testWSTEPCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIDGzCCAgOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAvMQkwBwYD
|
||||
VQQGEwAxEDAOBgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1NDRVAg
|
||||
Q0EwHhcNMjIxMjIyMTM0NDMzWhcNMzIxMjIyMTM0NDMzWjAvMQkw
|
||||
BwYDVQQGEwAxEDAOBgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1ND
|
||||
RVAgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDV
|
||||
u9YVfl7gu0UgUkOJoES/XrN0WZdIjgvS2upKfvP4LSJOq1Mnp3bH
|
||||
wWOA2NkHem/kjOVeotOk1aEYIzxbic6VlvNOz9huOhbJyoV4TO5v
|
||||
tp/GFFcJ4IXh+f1Q4vm/NeH/XxEWn9S20B9OkSMOUievYsAu6iSi
|
||||
oWaa74q1mnfpzM29p3dNM82mCKutYdkW0EusixU/CQxcVhdcxC+R
|
||||
RyM4jzBFIipa7H20UtqdkZ03/9BoowJb/h/r4X7TN4tKg2vcwpZK
|
||||
uJo7VcTBNPxhBowzg3JUmzjCnxPbuU/Ow5kPGOLJtbf4766ToNTM
|
||||
/J63i3UPshKUBqAE8mIZO3qb7s25AgMBAAGjQjBAMA4GA1UdDwEB
|
||||
/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTxPEY4
|
||||
WvsLCt+HDQfnEPOKrHu0gTANBgkqhkiG9w0BAQsFAAOCAQEAGNf5
|
||||
R60vRxIfvSOUyV3X7lUk+fVvi1CKC43DsP5OsQ6g5YVGcVXN40U4
|
||||
2o7JUeb9K1jvqnzWB/3k+lSCkEb0a5KabjZE5Vpdt9xctmgrfNnQ
|
||||
PBCfDdyb0Upjm61CJeB2SW9+ibT2L+OtL/nZjjlugL7ir9ramQBh
|
||||
0IY6oB9Yc3TyZyPjnXwbi0jv5cildzIYaYPvPkPPTjezOUqUDgUH
|
||||
JtdWRBQeJ/6WxAAm9il0KVXOsRPgAsdiDJTF6FdW4lsY8V/R6y0H
|
||||
hTN1ZSyqklKAuvEZZznfmJsrNYRII2Fv2zOk0Uv/+E+EKTOHbgcC
|
||||
PQAARDBzDlWvlMGWcbdrdypdeA==
|
||||
-----END CERTIFICATE-----
|
||||
`)
|
||||
|
||||
testWSTEPKey = []byte(euaTestingKey(`-----BEGIN RSA TESTING KEY-----
|
||||
MIIEowIBAAKCAQEA1bvWFX5e4LtFIFJDiaBEv16zdFmXSI4L0trqSn7z+C0iTqtT
|
||||
J6d2x8FjgNjZB3pv5IzlXqLTpNWhGCM8W4nOlZbzTs/YbjoWycqFeEzub7afxhRX
|
||||
CeCF4fn9UOL5vzXh/18RFp/UttAfTpEjDlInr2LALuokoqFmmu+KtZp36czNvad3
|
||||
TTPNpgirrWHZFtBLrIsVPwkMXFYXXMQvkUcjOI8wRSIqWux9tFLanZGdN//QaKMC
|
||||
W/4f6+F+0zeLSoNr3MKWSriaO1XEwTT8YQaMM4NyVJs4wp8T27lPzsOZDxjiybW3
|
||||
+O+uk6DUzPyet4t1D7ISlAagBPJiGTt6m+7NuQIDAQABAoIBAE6LXL1BV3SW3Wxn
|
||||
TtKAx0Lcdm5HjkTnjojKUldWGCoXzAfFBiYIcKov83UiO394Cy6eaJxCkix9JVpN
|
||||
eJzbI8PtWTSZRRwc1MsLVclD3EvJfSW5y9KhZBILYIAdKVKPZqIGOa1qxyz3hsnE
|
||||
pHFa16KoU5/qA9SQI7jEVuEuBusv4D/dRlEWvva7QOhnLrBPrSnTSZ5LxCFKRviS
|
||||
XrEQ9AuRJeXCKx4WzXd4IZPpgldYHMJSSGMr0TeVcURbsfveI2IWvOLag0ofTHhx
|
||||
tolBT2sKzInItLTwt/irZEp5lV08mMGxHuxoCdzhxjFQP8eGOZzPW65c6/D9hEXd
|
||||
DzWnjdECgYEA9QtTQosOTtAyU1i4Fm76ltT6nywHy23KAMhBaoKgTMccNtjaOCg/
|
||||
5FCCRD+qoo7TF4jdliP2NrMIbAIhr4jEfHSMKaD/rae1xqInseDCrGi9gzvm8UxG
|
||||
84VG30Id8s70ZQWZjR/PFFDeNZjNhlk8COO0XoLaqJSZr+A30aSyeUsCgYEA30ok
|
||||
3EvO1+/gjZv28J9vApdbiEwtO9xoteghElFzdtuEuzA+wL83w8xvKvdb4Rk5xigE
|
||||
6mV69dBPj8zSyGp0lFTYLFvry5N4S8L6QPzt2nk+Lc3cDKSA5CkAkQ5Dmt5JwhxF
|
||||
qIPDNZGXmoldIWJ0p/ZSu98/1yXBMQ9gCje/losCgYBwuk4KLbheT27nYsgFIfbL
|
||||
zpyg/vty/UXRiE53tjISQALdxHLXJMUHvnW++d8Au12m1QLDIDYTQdddALoIa42g
|
||||
h2k3eWZFuAJqp4xFS1WjROfx6Gu8k8+MFcLd0CfA3K4XjzTtdDWqbe1bkLjz1jdF
|
||||
C6OdWutGZF4zR53GJtMn8wKBgCfA95cRGB5x4rTTk797YzQ+5lj51wPVVf8s+NZe
|
||||
EgSTSKpbCJEgejkt6IzpxT3qU9LnxRhGQQIKuF+Nw+lSqrbN9D7RjsWL19sFN7Di
|
||||
VyaSd3OINyk5EImOkz9AHuEvukoI5o3+B38+EJO+6QnMkaBlxo0UTjVrz12As0Se
|
||||
cEnJAoGBAOUXjez9oUSzLzqG/WJFrIfHyjDA1vBS1j39XuhDuJGqMdNLlCE8Yr7h
|
||||
d3gpZeuV3ZC33QAuwAXfRBNnKIDtDGpcrozM1NndcBVDs9GYvobaTiUaODGjsH44
|
||||
oHwpyQbv9Qs+3bjPOQ7DkwekT+w1cptEKudBCC3WQKui1P0NNL0R
|
||||
-----END RSA TESTING KEY-----
|
||||
`))
|
||||
)
|
||||
|
||||
// newTestServiceWithWSTEP returns a Service with a real wstepCertManager built
|
||||
// from the inline test cert/key, backed by a mock datastore.
|
||||
func newTestServiceWithWSTEP(t *testing.T, ds *mock.Store) *Service {
|
||||
t.Helper()
|
||||
certManager, err := microsoft_mdm.NewCertManager(nil, testWSTEPCert, testWSTEPKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &Service{
|
||||
ds: ds,
|
||||
wstepCertManager: certManager,
|
||||
logger: slog.New(slog.DiscardHandler),
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessWindowsEUAToken(t *testing.T) {
|
||||
const (
|
||||
testUPN = "user@example.com"
|
||||
testDeviceID = "device-abc-123"
|
||||
testHostUUID = "host-uuid-xyz"
|
||||
testAcctUUID = "acct-uuid-456"
|
||||
)
|
||||
|
||||
// Helper to generate a valid token for test cases.
|
||||
makeToken := func(t *testing.T, svc *Service, upn, deviceID string) string {
|
||||
t.Helper()
|
||||
tok, err := svc.wstepCertManager.NewEUAToken(upn, deviceID)
|
||||
require.NoError(t, err)
|
||||
return tok
|
||||
}
|
||||
|
||||
t.Run("valid token, new enrollment, account not yet in db", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
token := makeToken(t, svc, testUPN, testDeviceID)
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
require.Equal(t, testDeviceID, mdmDeviceID)
|
||||
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, HostUUID: ""}, nil
|
||||
}
|
||||
// First call returns not-found; second call (after insert) returns the account.
|
||||
getByEmailCalls := 0
|
||||
ds.GetMDMIdPAccountByEmailFunc = func(ctx context.Context, email string) (*fleet.MDMIdPAccount, error) {
|
||||
require.Equal(t, testUPN, email)
|
||||
getByEmailCalls++
|
||||
if getByEmailCalls == 1 {
|
||||
return nil, mysql_errors.NotFound("MDMIdPAccount")
|
||||
}
|
||||
return &fleet.MDMIdPAccount{UUID: testAcctUUID, Email: testUPN, Username: testUPN}, nil
|
||||
}
|
||||
ds.InsertMDMIdPAccountFunc = func(ctx context.Context, account *fleet.MDMIdPAccount) error {
|
||||
require.Equal(t, testUPN, account.Email)
|
||||
return nil
|
||||
}
|
||||
ds.AssociateHostMDMIdPAccountDBFunc = func(ctx context.Context, hostUUID, acctUUID string) error {
|
||||
require.Equal(t, testHostUUID, hostUUID)
|
||||
require.Equal(t, testAcctUUID, acctUUID)
|
||||
return nil
|
||||
}
|
||||
|
||||
upn, deviceID, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testUPN, upn)
|
||||
require.Equal(t, testDeviceID, deviceID)
|
||||
require.True(t, ds.AssociateHostMDMIdPAccountDBFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("valid token, account already exists in db", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
token := makeToken(t, svc, testUPN, testDeviceID)
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, HostUUID: ""}, nil
|
||||
}
|
||||
// Account exists — Insert should NOT be called.
|
||||
ds.GetMDMIdPAccountByEmailFunc = func(ctx context.Context, email string) (*fleet.MDMIdPAccount, error) {
|
||||
return &fleet.MDMIdPAccount{UUID: testAcctUUID, Email: testUPN, Username: "existing-username", Fullname: "Existing Name"}, nil
|
||||
}
|
||||
ds.AssociateHostMDMIdPAccountDBFunc = func(ctx context.Context, hostUUID, acctUUID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, _, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, token)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ds.InsertMDMIdPAccountFuncInvoked, "should not insert when account already exists")
|
||||
require.True(t, ds.AssociateHostMDMIdPAccountDBFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("valid token, enrollment already has host_uuid — still links idp account", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
token := makeToken(t, svc, testUPN, testDeviceID)
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
// HostUUID already set — device was previously enrolled.
|
||||
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, HostUUID: "existing-host-uuid"}, nil
|
||||
}
|
||||
// Account already exists — re-enrollment after host deletion may
|
||||
// have left the enrollment row populated but the mapping missing.
|
||||
ds.GetMDMIdPAccountByEmailFunc = func(ctx context.Context, email string) (*fleet.MDMIdPAccount, error) {
|
||||
return &fleet.MDMIdPAccount{UUID: testAcctUUID, Email: testUPN, Username: testUPN}, nil
|
||||
}
|
||||
ds.AssociateHostMDMIdPAccountDBFunc = func(ctx context.Context, hostUUID, acctUUID string) error {
|
||||
require.Equal(t, testHostUUID, hostUUID)
|
||||
require.Equal(t, testAcctUUID, acctUUID)
|
||||
return nil
|
||||
}
|
||||
|
||||
upn, deviceID, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testUPN, upn)
|
||||
require.Equal(t, testDeviceID, deviceID)
|
||||
require.True(t, ds.GetMDMIdPAccountByEmailFuncInvoked, "should still fetch idp account even when enrollment has host_uuid")
|
||||
require.True(t, ds.AssociateHostMDMIdPAccountDBFuncInvoked, "should still link idp account even when enrollment has host_uuid")
|
||||
})
|
||||
|
||||
t.Run("invalid token falls back to END_USER_AUTH_REQUIRED", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
|
||||
_, _, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, "this.is.not.a.valid.token")
|
||||
require.Error(t, err)
|
||||
var orbitErr *fleet.OrbitError
|
||||
require.ErrorAs(t, err, &orbitErr)
|
||||
require.Equal(t, "END_USER_AUTH_REQUIRED", orbitErr.Message)
|
||||
require.False(t, ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("nil wstepCertManager falls back to END_USER_AUTH_REQUIRED without panic", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := &Service{ds: ds, logger: slog.New(slog.DiscardHandler)}
|
||||
|
||||
_, _, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, "any.token.value")
|
||||
require.Error(t, err)
|
||||
var orbitErr *fleet.OrbitError
|
||||
require.ErrorAs(t, err, &orbitErr)
|
||||
require.Equal(t, "END_USER_AUTH_REQUIRED", orbitErr.Message)
|
||||
require.False(t, ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked)
|
||||
})
|
||||
|
||||
t.Run("device not found falls back to END_USER_AUTH_REQUIRED", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
token := makeToken(t, svc, testUPN, testDeviceID)
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
return nil, mysql_errors.NotFound("MDMWindowsEnrolledDevice")
|
||||
}
|
||||
|
||||
_, _, err := svc.processWindowsEUAToken(context.Background(), testHostUUID, token)
|
||||
require.Error(t, err)
|
||||
var orbitErr *fleet.OrbitError
|
||||
require.ErrorAs(t, err, &orbitErr)
|
||||
require.Equal(t, "END_USER_AUTH_REQUIRED", orbitErr.Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateWindowsEUAToken(t *testing.T) {
|
||||
const (
|
||||
testUPN = "user@example.com"
|
||||
testDeviceID = "device-abc-123"
|
||||
)
|
||||
|
||||
t.Run("returns token for device with valid UPN", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, MDMEnrollUserID: testUPN}, nil
|
||||
}
|
||||
|
||||
token := svc.generateWindowsEUAToken(context.Background(), testDeviceID)
|
||||
require.NotEmpty(t, token)
|
||||
|
||||
// Token should be valid and contain expected claims.
|
||||
claims, err := svc.wstepCertManager.GetEUATokenClaims(token)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testUPN, claims.UPN)
|
||||
require.Equal(t, testDeviceID, claims.DeviceID)
|
||||
})
|
||||
|
||||
t.Run("returns empty string when device has no UPN", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, MDMEnrollUserID: ""}, nil
|
||||
}
|
||||
|
||||
require.Empty(t, svc.generateWindowsEUAToken(context.Background(), testDeviceID))
|
||||
})
|
||||
|
||||
t.Run("returns empty string when device not found", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
return nil, mysql_errors.NotFound("MDMWindowsEnrolledDevice")
|
||||
}
|
||||
|
||||
require.Empty(t, svc.generateWindowsEUAToken(context.Background(), testDeviceID))
|
||||
})
|
||||
|
||||
t.Run("returns empty string when datastore returns error", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := newTestServiceWithWSTEP(t, ds)
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
return nil, sql.ErrConnDone
|
||||
}
|
||||
|
||||
require.Empty(t, svc.generateWindowsEUAToken(context.Background(), testDeviceID))
|
||||
})
|
||||
|
||||
t.Run("returns empty string when wstepCertManager is nil", func(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc := &Service{ds: ds, logger: slog.New(slog.DiscardHandler)}
|
||||
|
||||
ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFunc = func(ctx context.Context, mdmDeviceID string) (*fleet.MDMWindowsEnrolledDevice, error) {
|
||||
return &fleet.MDMWindowsEnrolledDevice{MDMDeviceID: testDeviceID, MDMEnrollUserID: testUPN}, nil
|
||||
}
|
||||
|
||||
require.Empty(t, svc.generateWindowsEUAToken(context.Background(), testDeviceID))
|
||||
require.False(t, ds.MDMWindowsGetEnrolledDeviceWithDeviceIDFuncInvoked, "should not query db when cert manager is nil")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user