Add new self-service auth method for iOS/iPadOS (#36659)
Implements #36542. Adds URL/UDID-based authentication for the My Device page on iOS/iPadOS.
This commit is contained in:
@@ -1417,7 +1417,9 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof
|
||||
// we need to use ds.Host because ds.HostLite doesn't return the orbit node key
|
||||
host, err := svc.ds.Host(ctx, hostID)
|
||||
|
||||
fromMyDevicePage := svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) || svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate)
|
||||
fromMyDevicePage := svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceURL)
|
||||
|
||||
if err != nil {
|
||||
// if error is because the host does not exist, check first if the user
|
||||
@@ -1536,7 +1538,9 @@ func (svc *Service) insertSoftwareUninstallRequest(ctx context.Context, executio
|
||||
}
|
||||
|
||||
func (svc *Service) GetSoftwareInstallResults(ctx context.Context, resultUUID string) (*fleet.HostSoftwareInstallerResult, error) {
|
||||
if svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) || svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) {
|
||||
if svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceURL) {
|
||||
return svc.getDeviceSoftwareInstallResults(ctx, resultUUID)
|
||||
}
|
||||
|
||||
|
||||
@@ -743,7 +743,9 @@ func (svc *Service) GetTeam(ctx context.Context, teamID uint) (*fleet.Team, erro
|
||||
return team, nil
|
||||
}
|
||||
|
||||
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) || svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate)
|
||||
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceURL)
|
||||
if alreadyAuthd {
|
||||
// device-authenticated request can only get the device's team
|
||||
host, ok := hostctx.FromContext(ctx)
|
||||
|
||||
@@ -52,6 +52,9 @@ const (
|
||||
// backed by the device's SCEP Identity certificate. This authentication method does not support
|
||||
// granular authorization.
|
||||
AuthnHTTPMessageSignature
|
||||
// AuthnDeviceURL is when authentication is done via a UUID in the URL.
|
||||
// This authentication mode does not support granular authorization.
|
||||
AuthnDeviceURL
|
||||
)
|
||||
|
||||
// AuthorizationContext contains the context information used for the
|
||||
|
||||
@@ -358,6 +358,10 @@ type Service interface {
|
||||
// This is used for iOS/iPadOS devices accessing My Device page via client certificates.
|
||||
// Returns an error if the certificate doesn't match the host or if the host is not iOS/iPadOS.
|
||||
AuthenticateDeviceByCertificate(ctx context.Context, certSerial uint64, hostUUID string) (host *Host, debug bool, err error)
|
||||
// AuthenticateIDeviceByURL loads host identified by the URL UUID.
|
||||
// This is used for iOS/iPadOS devices (iDevices) accessing endpoints via a unique URL parameter.
|
||||
// Returns an error if the UUID doesn't exist or if the host is not iOS/iPadOS.
|
||||
AuthenticateIDeviceByURL(ctx context.Context, urlUUID string) (host *Host, debug bool, err error)
|
||||
|
||||
ListHosts(ctx context.Context, opt HostListOptions) (hosts []*Host, err error)
|
||||
// GetHost returns the host with the provided ID.
|
||||
|
||||
@@ -208,6 +208,8 @@ type AuthenticateDeviceFunc func(ctx context.Context, authToken string) (host *f
|
||||
|
||||
type AuthenticateDeviceByCertificateFunc func(ctx context.Context, certSerial uint64, hostUUID string) (host *fleet.Host, debug bool, err error)
|
||||
|
||||
type AuthenticateIDeviceByURLFunc func(ctx context.Context, urlUUID string) (host *fleet.Host, debug bool, err error)
|
||||
|
||||
type ListHostsFunc func(ctx context.Context, opt fleet.HostListOptions) (hosts []*fleet.Host, err error)
|
||||
|
||||
type GetHostFunc func(ctx context.Context, id uint, opts fleet.HostDetailOptions) (host *fleet.HostDetail, err error)
|
||||
@@ -1152,6 +1154,9 @@ type Service struct {
|
||||
AuthenticateDeviceByCertificateFunc AuthenticateDeviceByCertificateFunc
|
||||
AuthenticateDeviceByCertificateFuncInvoked bool
|
||||
|
||||
AuthenticateIDeviceByURLFunc AuthenticateIDeviceByURLFunc
|
||||
AuthenticateIDeviceByURLFuncInvoked bool
|
||||
|
||||
ListHostsFunc ListHostsFunc
|
||||
ListHostsFuncInvoked bool
|
||||
|
||||
@@ -2807,6 +2812,13 @@ func (s *Service) AuthenticateDeviceByCertificate(ctx context.Context, certSeria
|
||||
return s.AuthenticateDeviceByCertificateFunc(ctx, certSerial, hostUUID)
|
||||
}
|
||||
|
||||
func (s *Service) AuthenticateIDeviceByURL(ctx context.Context, urlUUID string) (host *fleet.Host, debug bool, err error) {
|
||||
s.mu.Lock()
|
||||
s.AuthenticateIDeviceByURLFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.AuthenticateIDeviceByURLFunc(ctx, urlUUID)
|
||||
}
|
||||
|
||||
func (s *Service) ListHosts(ctx context.Context, opt fleet.HostListOptions) (hosts []*fleet.Host, err error) {
|
||||
s.mu.Lock()
|
||||
s.ListHostsFuncInvoked = true
|
||||
|
||||
@@ -233,7 +233,9 @@ func (svc *Service) SandboxEnabled() bool {
|
||||
}
|
||||
|
||||
func (svc *Service) AppConfigObfuscated(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceURL) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -158,6 +158,25 @@ func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.S
|
||||
return getDeviceHostResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
// Scrub sensitive data from the host response for iOS and iPadOS devices
|
||||
if authzCtx, ok := authz.FromContext(ctx); ok && authzCtx.AuthnMethod() == authz.AuthnDeviceURL {
|
||||
if host.Platform == "ios" || host.Platform == "ipados" {
|
||||
resp.HardwareSerial = ""
|
||||
resp.UUID = ""
|
||||
resp.PrimaryMac = ""
|
||||
resp.TeamName = nil
|
||||
resp.MDM.Profiles = nil
|
||||
resp.Labels = nil
|
||||
|
||||
// Scrub sensitive data from the license response
|
||||
scrubbedLicense := *license
|
||||
scrubbedLicense.Organization = ""
|
||||
scrubbedLicense.DeviceCount = 0
|
||||
scrubbedLicense.Expiration = time.Time{}
|
||||
license = &scrubbedLicense
|
||||
}
|
||||
}
|
||||
|
||||
resp.DEPAssignedToFleet = ptr.Bool(false)
|
||||
if ac.MDM.EnabledAndConfigured && license.IsPremium() {
|
||||
hdep, err := svc.GetHostDEPAssignment(ctx, host)
|
||||
@@ -214,7 +233,9 @@ func getDeviceHostEndpoint(ctx context.Context, request interface{}, svc fleet.S
|
||||
}
|
||||
|
||||
func (svc *Service) GetHostDEPAssignment(ctx context.Context, host *fleet.Host) (*fleet.HostDEPAssignment, error) {
|
||||
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) || svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate)
|
||||
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceURL)
|
||||
if !alreadyAuthd {
|
||||
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
@@ -304,6 +325,36 @@ func (svc *Service) AuthenticateDeviceByCertificate(ctx context.Context, certSer
|
||||
return host, svc.debugEnabledForHost(ctx, host.ID), nil
|
||||
}
|
||||
|
||||
// AuthenticateIDeviceByURL returns the host identified by the URL UUID.
|
||||
// This is used for iOS/iPadOS devices (iDevices) accessing endpoints via a unique URL parameter.
|
||||
// Returns an error if the UUID doesn't exist or if the host is not iOS/iPadOS.
|
||||
func (svc *Service) AuthenticateIDeviceByURL(ctx context.Context, urlUUID string) (*fleet.Host, bool, error) {
|
||||
// skipauth: Authorization is currently for user endpoints only.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
if urlUUID == "" {
|
||||
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing host UUID"))
|
||||
}
|
||||
|
||||
// Look up the host by UUID
|
||||
host, err := svc.ds.HostByIdentifier(ctx, urlUUID)
|
||||
switch {
|
||||
case err == nil:
|
||||
// OK
|
||||
case fleet.IsNotFound(err):
|
||||
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: host not found"))
|
||||
default:
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "lookup host by UUID")
|
||||
}
|
||||
|
||||
// Verify host platform is iOS or iPadOS
|
||||
if host.Platform != "ios" && host.Platform != "ipados" {
|
||||
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: URL authentication only supported for iOS and iPadOS devices"))
|
||||
}
|
||||
|
||||
return host, svc.debugEnabledForHost(ctx, host.ID), nil
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// Refetch Current Device's Host
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -684,7 +735,9 @@ func fleetdError(ctx context.Context, request interface{}, svc fleet.Service) (f
|
||||
}
|
||||
|
||||
func (svc *Service) LogFleetdError(ctx context.Context, fleetdError fleet.FleetdError) error {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate) {
|
||||
// iOS/iPadOS devices don't have fleetd, so URL auth is not allowed here.
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate) {
|
||||
return ctxerr.Wrap(ctx, fleet.NewPermissionError("forbidden: only device-authenticated hosts can access this endpoint"))
|
||||
}
|
||||
|
||||
@@ -739,7 +792,9 @@ func getDeviceMDMManualEnrollProfileEndpoint(ctx context.Context, request interf
|
||||
|
||||
func (svc *Service) GetDeviceMDMAppleEnrollmentProfile(ctx context.Context) (*url.URL, error) {
|
||||
// must be device-authenticated, no additional authorization is required
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate) {
|
||||
// iOS/iPadOS devices are enrolled via MDM profile or ABM, so URL auth is not allowed here.
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceCertificate) {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewPermissionError("forbidden: only device-authenticated hosts can access this endpoint"))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/host"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetDeviceHostEndpointScrubbing(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{SkipCreateTestUsers: true})
|
||||
|
||||
h := &fleet.Host{
|
||||
ID: 1,
|
||||
Hostname: "test-host",
|
||||
UUID: "sensitive-uuid",
|
||||
HardwareSerial: "sensitive-serial",
|
||||
PrimaryMac: "sensitive-mac",
|
||||
TeamName: ptr.String("sensitive-team"),
|
||||
Platform: "ios",
|
||||
MDM: fleet.MDMHostData{
|
||||
Profiles: &[]fleet.HostMDMProfile{
|
||||
{Identifier: "sensitive-profile"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
ds.GetHostIssuesLastUpdatedFunc = func(ctx context.Context, hostID uint) (time.Time, error) {
|
||||
return time.Now(), nil
|
||||
}
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
OrgInfo: fleet.OrgInfo{
|
||||
OrgLogoURL: "http://example.com/logo.png",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeVulnerabilities bool) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostUsersFunc = func(ctx context.Context, hostID uint) ([]fleet.HostUser, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMCheckinInfoFunc = func(ctx context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListLabelsForHostFunc = func(ctx context.Context, hostID uint) ([]*fleet.Label, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListPacksForHostFunc = func(ctx context.Context, hostID uint) ([]*fleet.Pack, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostBatteriesFunc = func(ctx context.Context, id uint) ([]*fleet.HostBattery, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostMaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Inject host into context
|
||||
ctx = host.NewContext(ctx, h)
|
||||
// Inject authz context with URL-based auth method (scrubbing only happens for URL auth)
|
||||
authzCtx := &authz.AuthorizationContext{}
|
||||
authzCtx.SetAuthnMethod(authz.AuthnDeviceURL)
|
||||
ctx = authz.NewContext(ctx, authzCtx)
|
||||
|
||||
req := &getDeviceHostRequest{
|
||||
Token: "test-token",
|
||||
}
|
||||
|
||||
resp, err := getDeviceHostEndpoint(ctx, req, svc)
|
||||
require.NoError(t, err)
|
||||
|
||||
deviceResp, ok := resp.(getDeviceHostResponse)
|
||||
require.True(t, ok)
|
||||
require.NoError(t, deviceResp.Err)
|
||||
require.NotNil(t, deviceResp.Host)
|
||||
|
||||
// Verify scrubbed fields in Host
|
||||
assert.Empty(t, deviceResp.Host.HardwareSerial)
|
||||
assert.Empty(t, deviceResp.Host.UUID)
|
||||
assert.Empty(t, deviceResp.Host.PrimaryMac)
|
||||
assert.Nil(t, deviceResp.Host.TeamName)
|
||||
assert.Nil(t, deviceResp.Host.MDM.Profiles)
|
||||
assert.Nil(t, deviceResp.Host.Labels)
|
||||
|
||||
// Verify scrubbed fields in License
|
||||
assert.Empty(t, deviceResp.License.Organization)
|
||||
assert.Zero(t, deviceResp.License.DeviceCount)
|
||||
assert.True(t, deviceResp.License.Expiration.IsZero())
|
||||
|
||||
// Verify other fields are present
|
||||
assert.Equal(t, "test-host", deviceResp.Host.Hostname)
|
||||
assert.Equal(t, "http://example.com/logo.png", deviceResp.OrgLogoURL)
|
||||
}
|
||||
|
||||
func TestGetDeviceHostEndpointNoScrubbingForMacOS(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
testLicense := &fleet.LicenseInfo{
|
||||
Tier: fleet.TierPremium,
|
||||
Organization: "Test Org",
|
||||
DeviceCount: 100,
|
||||
Expiration: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{
|
||||
SkipCreateTestUsers: true,
|
||||
License: testLicense,
|
||||
})
|
||||
|
||||
h := &fleet.Host{
|
||||
ID: 1,
|
||||
Hostname: "test-host-mac",
|
||||
UUID: "visible-uuid",
|
||||
HardwareSerial: "visible-serial",
|
||||
PrimaryMac: "visible-mac",
|
||||
TeamName: ptr.String("visible-team"),
|
||||
Platform: "darwin",
|
||||
MDM: fleet.MDMHostData{
|
||||
Profiles: &[]fleet.HostMDMProfile{
|
||||
{Identifier: "visible-profile"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
ds.GetHostIssuesLastUpdatedFunc = func(ctx context.Context, hostID uint) (time.Time, error) {
|
||||
return time.Now(), nil
|
||||
}
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
OrgInfo: fleet.OrgInfo{
|
||||
OrgLogoURL: "http://example.com/logo.png",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeVulnerabilities bool) error {
|
||||
return nil
|
||||
}
|
||||
ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostUsersFunc = func(ctx context.Context, hostID uint) ([]fleet.HostUser, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetHostMDMCheckinInfoFunc = func(ctx context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListLabelsForHostFunc = func(ctx context.Context, hostID uint) ([]*fleet.Label, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListPacksForHostFunc = func(ctx context.Context, hostID uint) ([]*fleet.Pack, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostBatteriesFunc = func(ctx context.Context, id uint) ([]*fleet.HostBattery, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListUpcomingHostMaintenanceWindowsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostMaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
ds.GetHostLockWipeStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostLockWipeStatus, error) {
|
||||
return &fleet.HostLockWipeStatus{}, nil
|
||||
}
|
||||
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Inject host into context
|
||||
ctx = host.NewContext(ctx, h)
|
||||
// Inject authz context
|
||||
authzCtx := &authz.AuthorizationContext{}
|
||||
authzCtx.SetAuthnMethod(authz.AuthnDeviceToken)
|
||||
ctx = authz.NewContext(ctx, authzCtx)
|
||||
|
||||
req := &getDeviceHostRequest{
|
||||
Token: "test-token",
|
||||
}
|
||||
|
||||
resp, err := getDeviceHostEndpoint(ctx, req, svc)
|
||||
require.NoError(t, err)
|
||||
|
||||
deviceResp, ok := resp.(getDeviceHostResponse)
|
||||
require.True(t, ok)
|
||||
require.NoError(t, deviceResp.Err)
|
||||
require.NotNil(t, deviceResp.Host)
|
||||
|
||||
// Verify fields are NOT scrubbed
|
||||
assert.Equal(t, "visible-serial", deviceResp.Host.HardwareSerial)
|
||||
assert.Equal(t, "visible-uuid", deviceResp.Host.UUID)
|
||||
assert.Equal(t, "visible-mac", deviceResp.Host.PrimaryMac)
|
||||
assert.NotNil(t, deviceResp.Host.TeamName)
|
||||
assert.Equal(t, "visible-team", *deviceResp.Host.TeamName)
|
||||
assert.NotNil(t, deviceResp.Host.MDM.Profiles)
|
||||
|
||||
// Verify License is NOT scrubbed (values match what we set in testLicense)
|
||||
assert.Equal(t, "Test Org", deviceResp.License.Organization)
|
||||
assert.Equal(t, 100, deviceResp.License.DeviceCount)
|
||||
assert.False(t, deviceResp.License.Expiration.IsZero())
|
||||
}
|
||||
@@ -143,7 +143,6 @@ func TestGetFleetDesktopSummary(t *testing.T) {
|
||||
require.EqualValues(t, 1, *sum.FailingPolicies)
|
||||
assert.Equal(t, ptr.Bool(true), sum.SelfService)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("different app config values for unmanaged host", func(t *testing.T) {
|
||||
@@ -247,7 +246,6 @@ func TestGetFleetDesktopSummary(t *testing.T) {
|
||||
require.Equal(t, c.out, sum.Notifications, fmt.Sprintf("enabled_and_configured: %t | macos_migration.enable: %t", c.mdm.EnabledAndConfigured, c.mdm.MacOSMigration.Enable))
|
||||
require.EqualValues(t, 1, *sum.FailingPolicies)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("different host attributes", func(t *testing.T) {
|
||||
@@ -474,7 +472,6 @@ func TestGetFleetDesktopSummary(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuthenticatedDeviceFallbackAuth(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, _ := newTestService(t, ds, nil, nil)
|
||||
|
||||
// Mock AppConfig to avoid panic in debugEnabledForHost
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
||||
middleware := authenticatedDevice(svc, log.NewNopLogger(), func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return "success", nil
|
||||
})
|
||||
|
||||
t.Run("success_token_auth_for_macos", func(t *testing.T) {
|
||||
// macOS device with valid token - token auth succeeds first (hot path)
|
||||
ds.LoadHostByDeviceAuthTokenFunc = func(ctx context.Context, authToken string, ttl time.Duration) (*fleet.Host, error) {
|
||||
if authToken == "valid-device-token" {
|
||||
return &fleet.Host{
|
||||
ID: 1,
|
||||
UUID: "macos-device-uuid",
|
||||
Platform: "darwin",
|
||||
}, nil
|
||||
}
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
||||
req := mockDeviceAuthRequest{Token: "valid-device-token"}
|
||||
_, err := middleware(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("fallback_to_uuid_auth_for_ios", func(t *testing.T) {
|
||||
// iOS device with UUID in URL - token auth fails, falls back to UUID auth
|
||||
ds.LoadHostByDeviceAuthTokenFunc = func(ctx context.Context, authToken string, ttl time.Duration) (*fleet.Host, error) {
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
if identifier == "ios-device-uuid" {
|
||||
return &fleet.Host{
|
||||
ID: 1,
|
||||
UUID: "ios-device-uuid",
|
||||
Platform: "ios",
|
||||
}, nil
|
||||
}
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
||||
req := mockDeviceAuthRequest{Token: "ios-device-uuid"}
|
||||
_, err := middleware(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("fallback_to_uuid_auth_for_ipados", func(t *testing.T) {
|
||||
// iPadOS device with UUID in URL - token auth fails, falls back to UUID auth
|
||||
ds.LoadHostByDeviceAuthTokenFunc = func(ctx context.Context, authToken string, ttl time.Duration) (*fleet.Host, error) {
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
if identifier == "ipados-device-uuid" {
|
||||
return &fleet.Host{
|
||||
ID: 2,
|
||||
UUID: "ipados-device-uuid",
|
||||
Platform: "ipados",
|
||||
}, nil
|
||||
}
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
||||
req := mockDeviceAuthRequest{Token: "ipados-device-uuid"}
|
||||
_, err := middleware(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("failure_when_both_auth_methods_fail", func(t *testing.T) {
|
||||
// Neither token nor UUID auth succeeds
|
||||
ds.LoadHostByDeviceAuthTokenFunc = func(ctx context.Context, authToken string, ttl time.Duration) (*fleet.Host, error) {
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
||||
req := mockDeviceAuthRequest{Token: "invalid-token"}
|
||||
_, err := middleware(context.Background(), req)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
type mockDeviceAuthRequest struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func (m mockDeviceAuthRequest) deviceAuthToken() string {
|
||||
return m.Token
|
||||
}
|
||||
|
||||
func TestAuthenticateIDeviceByURL(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, _ := newTestService(t, ds, nil, nil)
|
||||
|
||||
// Mock AppConfig to avoid panic in debugEnabledForHost
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
||||
t.Run("success - valid UUID for iOS device", func(t *testing.T) {
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
return &fleet.Host{
|
||||
ID: 1,
|
||||
UUID: "valid-uuid",
|
||||
Platform: "ios",
|
||||
}, nil
|
||||
}
|
||||
|
||||
host, debug, err := svc.AuthenticateIDeviceByURL(context.Background(), "valid-uuid")
|
||||
require.NoError(t, err)
|
||||
require.False(t, debug)
|
||||
require.NotNil(t, host)
|
||||
require.Equal(t, uint(1), host.ID)
|
||||
})
|
||||
|
||||
t.Run("success - valid UUID for iPadOS device", func(t *testing.T) {
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
return &fleet.Host{
|
||||
ID: 1,
|
||||
UUID: "valid-uuid",
|
||||
Platform: "ipados",
|
||||
}, nil
|
||||
}
|
||||
|
||||
host, debug, err := svc.AuthenticateIDeviceByURL(context.Background(), "valid-uuid")
|
||||
require.NoError(t, err)
|
||||
require.False(t, debug)
|
||||
require.NotNil(t, host)
|
||||
require.Equal(t, uint(1), host.ID)
|
||||
})
|
||||
|
||||
t.Run("error - missing host UUID", func(t *testing.T) {
|
||||
host, debug, err := svc.AuthenticateIDeviceByURL(context.Background(), "")
|
||||
require.Error(t, err)
|
||||
var authReqErr *fleet.AuthRequiredError
|
||||
require.ErrorAs(t, err, &authReqErr)
|
||||
require.Equal(t, "authentication error: missing host UUID", authReqErr.Internal())
|
||||
require.Nil(t, host)
|
||||
require.False(t, debug)
|
||||
})
|
||||
|
||||
t.Run("error - host not found", func(t *testing.T) {
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
return nil, newNotFoundError()
|
||||
}
|
||||
|
||||
host, debug, err := svc.AuthenticateIDeviceByURL(context.Background(), "invalid-uuid")
|
||||
require.Error(t, err)
|
||||
var authReqErr *fleet.AuthRequiredError
|
||||
require.ErrorAs(t, err, &authReqErr)
|
||||
require.Contains(t, authReqErr.Internal(), "host not found")
|
||||
require.Nil(t, host)
|
||||
require.False(t, debug)
|
||||
})
|
||||
|
||||
t.Run("error - host platform is not iOS or iPadOS (macOS)", func(t *testing.T) {
|
||||
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
|
||||
return &fleet.Host{
|
||||
ID: 1,
|
||||
UUID: "valid-uuid",
|
||||
Platform: "darwin",
|
||||
}, nil
|
||||
}
|
||||
|
||||
host, debug, err := svc.AuthenticateIDeviceByURL(context.Background(), "valid-uuid")
|
||||
require.Error(t, err)
|
||||
var authReqErr *fleet.AuthRequiredError
|
||||
require.ErrorAs(t, err, &authReqErr)
|
||||
require.Equal(t, "authentication error: URL authentication only supported for iOS and iPadOS devices", authReqErr.Internal())
|
||||
require.Nil(t, host)
|
||||
require.False(t, debug)
|
||||
})
|
||||
}
|
||||
@@ -62,6 +62,9 @@ func instrumentHostLogger(ctx context.Context, hostID uint, extras ...interface{
|
||||
)
|
||||
}
|
||||
|
||||
// authenticatedDevice checks the validity of the device auth token
|
||||
// provided in the request, and attaches the corresponding host to the
|
||||
// context for the request.
|
||||
func authenticatedDevice(svc fleet.Service, logger log.Logger, next endpoint.Endpoint) endpoint.Endpoint {
|
||||
authDeviceFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
identifier, err := getDeviceAuthToken(request)
|
||||
@@ -78,8 +81,16 @@ func authenticatedDevice(svc fleet.Service, logger log.Logger, next endpoint.End
|
||||
host, debug, err = svc.AuthenticateDeviceByCertificate(ctx, certSerial, identifier)
|
||||
authnMethod = authz_ctx.AuthnDeviceCertificate
|
||||
} else {
|
||||
// Try token auth first (hot path for Fleet Desktop).
|
||||
host, debug, err = svc.AuthenticateDevice(ctx, identifier)
|
||||
authnMethod = authz_ctx.AuthnDeviceToken
|
||||
if err == nil {
|
||||
authnMethod = authz_ctx.AuthnDeviceToken
|
||||
} else {
|
||||
// Fallback to UUID auth for iOS/iPadOS self-service via URL.
|
||||
// The identifier (from {token}) is treated as the device UUID.
|
||||
host, debug, err = svc.AuthenticateIDeviceByURL(ctx, identifier)
|
||||
authnMethod = authz_ctx.AuthnDeviceURL
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
+22
-6
@@ -575,7 +575,9 @@ func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service
|
||||
}
|
||||
|
||||
func (svc *Service) GetHost(ctx context.Context, id uint, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
|
||||
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) || svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate)
|
||||
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceURL)
|
||||
if !alreadyAuthd {
|
||||
// First ensure the user has access to list hosts, then check the specific
|
||||
// host once team_id is loaded.
|
||||
@@ -1113,7 +1115,9 @@ func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
|
||||
var host *fleet.Host
|
||||
// iOS and iPadOS refetch are not authenticated with device token because these devices do not have Fleet Desktop,
|
||||
// so we don't handle that case
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceURL) {
|
||||
var err error
|
||||
if err = svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||||
return err
|
||||
@@ -1625,7 +1629,9 @@ func listHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc
|
||||
}
|
||||
|
||||
func (svc *Service) ListHostDeviceMapping(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceURL) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1908,7 +1914,13 @@ func getMacadminsDataEndpoint(ctx context.Context, request interface{}, svc flee
|
||||
}
|
||||
|
||||
func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.MacadminsData, error) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) {
|
||||
// iOS/iPadOS devices don't have macadmins data (Munki, etc.), return nil early.
|
||||
if svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceURL) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -3112,7 +3124,9 @@ func (svc *Service) ListHostSoftware(ctx context.Context, hostID uint, opts flee
|
||||
var includeAvailableForInstall bool
|
||||
|
||||
var host *fleet.Host
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceURL) {
|
||||
includeAvailableForInstall = true
|
||||
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||||
@@ -3229,7 +3243,9 @@ func listHostCertificatesEndpoint(ctx context.Context, request interface{}, svc
|
||||
}
|
||||
|
||||
func (svc *Service) ListHostCertificates(ctx context.Context, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificatePayload, *fleet.PaginationMetadata, error) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceURL) {
|
||||
host, err := svc.ds.HostLite(ctx, hostID)
|
||||
if err != nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
@@ -22276,7 +22276,7 @@ func generateTestCertForDeviceAuth(t *testing.T, certSerial uint64, deviceUUID s
|
||||
return string(certPEM), certHash, cert
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestDeviceCertificateAuthentication() {
|
||||
func (s *integrationEnterpriseTestSuite) TestDeviceAuthenticationMethods() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -22370,8 +22370,19 @@ func (s *integrationEnterpriseTestSuite) TestDeviceCertificateAuthentication() {
|
||||
require.Equal(t, "ios", getHostResp.Host.Platform)
|
||||
})
|
||||
|
||||
t.Run("iOS device without certificate header", func(t *testing.T) {
|
||||
res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/device/%s", iosHost.UUID), nil, http.StatusUnauthorized)
|
||||
t.Run("iOS device without certificate header (UUID fallback auth)", func(t *testing.T) {
|
||||
// Without cert header, UUID auth is used as fallback for iOS/iPadOS devices
|
||||
var getHostResp getDeviceHostResponse
|
||||
res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/device/%s", iosHost.UUID), nil, http.StatusOK)
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&getHostResp))
|
||||
require.NoError(t, res.Body.Close())
|
||||
require.Equal(t, iosHost.ID, getHostResp.Host.ID)
|
||||
require.Equal(t, "ios", getHostResp.Host.Platform)
|
||||
})
|
||||
|
||||
t.Run("iOS device with invalid UUID (no fallback)", func(t *testing.T) {
|
||||
// Invalid UUID should fail both UUID auth and token auth
|
||||
res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid-uuid-does-not-exist", nil, http.StatusUnauthorized)
|
||||
res.Body.Close()
|
||||
})
|
||||
|
||||
@@ -22505,6 +22516,14 @@ func (s *integrationEnterpriseTestSuite) TestDeviceCertificateAuthentication() {
|
||||
res.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("macOS device UUID in URL should be rejected (not iOS/iPadOS)", func(t *testing.T) {
|
||||
// Using macOS host UUID directly in URL should fail:
|
||||
// - Token auth fails (macHost.UUID is not a valid token)
|
||||
// - UUID auth fails (platform is darwin, not iOS/iPadOS)
|
||||
res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/device/%s", macHost.UUID), nil, http.StatusUnauthorized)
|
||||
res.Body.Close()
|
||||
})
|
||||
|
||||
t.Run("iOS device with token auth should be rejected", func(t *testing.T) {
|
||||
// Create a device token for the iOS host
|
||||
iosToken := "ios-device-token"
|
||||
|
||||
@@ -950,14 +950,17 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
|
||||
}
|
||||
s.addHostIdentityCertificate(data.host.UUID, data.certSerial)
|
||||
|
||||
// self-install with no authentication
|
||||
s.DoRawNoAuth("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", data.host.UUID, 999), nil, http.StatusUnauthorized)
|
||||
|
||||
// self-install a non-existing title
|
||||
res := s.DoRawWithHeaders("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", data.host.UUID, 999), nil, http.StatusBadRequest, headers)
|
||||
// self-install without cert header (UUID auth fallback for iOS/iPadOS)
|
||||
// With fallback auth, UUID auth succeeds for iOS/iPadOS devices, so we get 400 (bad title) instead of 401
|
||||
res := s.DoRawNoAuth("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", data.host.UUID, 999), nil, http.StatusBadRequest)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Software title is not available for install.")
|
||||
|
||||
// self-install a non-existing title (with cert header - same result)
|
||||
res = s.DoRawWithHeaders("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", data.host.UUID, 999), nil, http.StatusBadRequest, headers)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Software title is not available for install.")
|
||||
|
||||
// self-install an existing title not available for self-install
|
||||
res = s.DoRawWithHeaders("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", data.host.UUID, data.titleID), nil, http.StatusBadRequest, headers)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
@@ -1592,14 +1595,17 @@ func (s *integrationMDMTestSuite) TestInHouseAppSelfInstall() {
|
||||
}
|
||||
s.addHostIdentityCertificate(iosHost.UUID, certSerial)
|
||||
|
||||
// self-install with no authentication
|
||||
s.DoRawNoAuth("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", iosHost.UUID, 999), nil, http.StatusUnauthorized)
|
||||
|
||||
// self-install a non-existing title
|
||||
res := s.DoRawWithHeaders("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", iosHost.UUID, 999), nil, http.StatusBadRequest, headers)
|
||||
// self-install without cert header (UUID auth fallback for iOS)
|
||||
// With fallback auth, UUID auth succeeds for iOS devices, so we get 400 (bad title) instead of 401
|
||||
res := s.DoRawNoAuth("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", iosHost.UUID, 999), nil, http.StatusBadRequest)
|
||||
errMsg := extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Software title is not available for install.")
|
||||
|
||||
// self-install a non-existing title (with cert header - same result)
|
||||
res = s.DoRawWithHeaders("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", iosHost.UUID, 999), nil, http.StatusBadRequest, headers)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
require.Contains(t, errMsg, "Software title is not available for install.")
|
||||
|
||||
// self-install an existing title not available for self-install
|
||||
res = s.DoRawWithHeaders("POST", fmt.Sprintf("/api/v1/fleet/device/%s/software/install/%d", iosHost.UUID, titleID), nil, http.StatusBadRequest, headers)
|
||||
errMsg = extractServerErrorText(res.Body)
|
||||
|
||||
@@ -708,7 +708,9 @@ func getMDMCommandResultsEndpoint(ctx context.Context, request interface{}, svc
|
||||
}
|
||||
|
||||
func (svc *Service) GetMDMCommandResults(ctx context.Context, commandUUID string) ([]*fleet.MDMCommandResult, error) {
|
||||
if svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) || svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) {
|
||||
if svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceURL) {
|
||||
return svc.getDeviceSoftwareMDMCommandResults(ctx, commandUUID)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,9 @@ func cleanupURL(url string) string {
|
||||
}
|
||||
|
||||
func (svc *Service) License(ctx context.Context) (*fleet.LicenseInfo, error) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) && !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) {
|
||||
if !svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceToken) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceCertificate) &&
|
||||
!svc.authz.IsAuthenticatedWith(ctx, authz_ctx.AuthnDeviceURL) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppConfig{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -864,7 +864,9 @@ func submitDeviceSoftwareUninstall(ctx context.Context, request interface{}, svc
|
||||
}
|
||||
|
||||
func (svc *Service) HasSelfServiceSoftwareInstallers(ctx context.Context, host *fleet.Host) (bool, error) {
|
||||
alreadyAuthenticated := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) || svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate)
|
||||
alreadyAuthenticated := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceCertificate) ||
|
||||
svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceURL)
|
||||
if !alreadyAuthenticated {
|
||||
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
|
||||
return false, err
|
||||
|
||||
Reference in New Issue
Block a user