mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Implement clear passcode backend (#43072)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #42368 # 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. For the overall story - [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. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
This commit is contained in:
parent
3371b48373
commit
6a9d394e62
29 changed files with 702 additions and 70 deletions
1
changes/39570-clear-passcode
Normal file
1
changes/39570-clear-passcode
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Implemented Clear Passcode feature for iOS and iPadOS.
|
||||
|
|
@ -263,8 +263,8 @@ func TestMDMRunCommand(t *testing.T) {
|
|||
}
|
||||
return res, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
|
||||
return false, nil
|
||||
|
|
@ -1401,8 +1401,8 @@ func setupDSMocks(ds *mock.Store, hostByUUID map[string]testhost, hostsByID map[
|
|||
ds.GetMDMWindowsBitLockerStatusFunc = func(ctx context.Context, host *fleet.Host) (*fleet.HostMDMDiskEncryption, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
h, ok := hostsByID[hostID]
|
||||
|
|
|
|||
|
|
@ -1648,3 +1648,85 @@ func (svc *Service) decryptUploadedABMToken(ctx context.Context, token io.Reader
|
|||
}
|
||||
return encryptedToken, decryptedToken, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ClearPasscode(ctx context.Context, hostID uint) (*fleet.CommandEnqueueResult, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
host, err := svc.ds.HostLite(ctx, hostID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "host lite")
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appCfg, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get app config")
|
||||
}
|
||||
|
||||
if fleet.IsApplePlatform(host.Platform) {
|
||||
return svc.clearPasscodeApple(ctx, host, appCfg)
|
||||
}
|
||||
|
||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: "Clearing passcode is only supported on Apple mobile platforms",
|
||||
})
|
||||
}
|
||||
|
||||
func (svc *Service) clearPasscodeApple(ctx context.Context, host *fleet.Host, appCfg *fleet.AppConfig) (*fleet.CommandEnqueueResult, error) {
|
||||
if !appCfg.MDM.EnabledAndConfigured {
|
||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: "Apple MDM is not enabled and configured",
|
||||
})
|
||||
}
|
||||
|
||||
if !fleet.IsAppleMobilePlatform(host.Platform) {
|
||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: "Clearing passcode is only supported on Apple mobile platforms",
|
||||
})
|
||||
}
|
||||
|
||||
mdmData, err := svc.ds.GetHostMDM(ctx, host.ID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get host MDM data")
|
||||
}
|
||||
|
||||
if mdmData.IsPersonalEnrollment {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: fleet.CantClearPasscodePersonalHostsMessage,
|
||||
}
|
||||
}
|
||||
|
||||
nanoDetails, err := svc.ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get nanomdm enrollment details")
|
||||
}
|
||||
|
||||
if nanoDetails == nil || nanoDetails.UnlockToken == nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: fleet.CantClearPasscodePersonalHostsMessage,
|
||||
}
|
||||
}
|
||||
|
||||
commandUUID := uuid.NewString()
|
||||
if err := svc.mdmAppleCommander.ClearPasscode(ctx, []string{host.UUID}, commandUUID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "clearing passcode")
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeClearedPasscode{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
}); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "creating activity for cleared passcode")
|
||||
}
|
||||
|
||||
return &fleet.CommandEnqueueResult{
|
||||
CommandUUID: commandUUID,
|
||||
RequestType: fleet.AppleMDMCommandTypeClearPasscode,
|
||||
Platform: host.Platform,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -9,11 +10,20 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
nanomdm_mdm "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
||||
nanomdm_pushsvc "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/service"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
mdmmock "github.com/fleetdm/fleet/v4/server/mock/mdm"
|
||||
mocksvc "github.com/fleetdm/fleet/v4/server/mock/service"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
svcmock "github.com/fleetdm/fleet/v4/server/service/mock"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/micromdm/nanolib/log/stdlogfmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"howett.net/plist"
|
||||
|
|
@ -218,3 +228,217 @@ func TestCountABMTokensAuth(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClearPasscode(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := new(mock.Store)
|
||||
authorizer, err := authz.NewAuthorizer()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set up the real commander with mocked storage and pusher.
|
||||
mdmStorage := &mdmmock.MDMAppleStore{}
|
||||
pushProvider := &svcmock.APNSPushProvider{}
|
||||
pushProvider.PushFunc = func(_ context.Context, pushes []*nanomdm_mdm.Push) (map[string]*nanomdm_push.Response, error) {
|
||||
res := make(map[string]*nanomdm_push.Response, len(pushes))
|
||||
for _, p := range pushes {
|
||||
res[p.Token.String()] = &nanomdm_push.Response{Id: "ok"}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
pushFactory := &svcmock.APNSPushProviderFactory{}
|
||||
pushFactory.NewPushProviderFunc = func(*tls.Certificate) (nanomdm_push.PushProvider, error) {
|
||||
return pushProvider, nil
|
||||
}
|
||||
pusher := nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushFactory, stdlogfmt.New())
|
||||
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
||||
svc := Service{ds: ds, authz: authorizer, mdmAppleCommander: commander, Service: &mocksvc.Service{
|
||||
NewActivityFunc: func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
return nil
|
||||
},
|
||||
}}
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
|
||||
}
|
||||
|
||||
// Common mdmStorage mocks for enqueue + push.
|
||||
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *nanomdm_mdm.CommandWithSubtype) (map[string]error, error) {
|
||||
return nil, nil
|
||||
}
|
||||
mdmStorage.RetrievePushInfoFunc = func(ctx context.Context, targets []string) (map[string]*nanomdm_mdm.Push, error) {
|
||||
pushes := make(map[string]*nanomdm_mdm.Push, len(targets))
|
||||
for _, uuid := range targets {
|
||||
pushes[uuid] = &nanomdm_mdm.Push{
|
||||
PushMagic: "magic" + uuid,
|
||||
Token: []byte("token" + uuid),
|
||||
Topic: "topic" + uuid,
|
||||
}
|
||||
}
|
||||
return pushes, nil
|
||||
}
|
||||
mdmStorage.RetrievePushCertFunc = func(ctx context.Context, topic string) (*tls.Certificate, string, error) {
|
||||
cert, err := tls.LoadX509KeyPair("../../../server/service/testdata/server.pem", "../../../server/service/testdata/server.key")
|
||||
return &cert, "", err
|
||||
}
|
||||
mdmStorage.IsPushCertStaleFunc = func(ctx context.Context, topic string, staleToken string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
t.Run("authorization", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: hostID, Platform: "ipados"}, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
return &fleet.HostMDM{}, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{UnlockToken: new("fake-token")}, nil
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
user *fleet.User
|
||||
shoudFailWithAuth bool
|
||||
}{
|
||||
{"no role", test.UserNoRoles, true},
|
||||
{"observer", test.UserObserver, true},
|
||||
{"observer+", test.UserObserverPlus, true},
|
||||
{"technician", test.UserTechnician, true},
|
||||
{"gitops", test.UserGitOps, true},
|
||||
{"maintainer", test.UserMaintainer, false},
|
||||
{"admin", test.UserAdmin, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
ctx := test.UserContext(t.Context(), c.user)
|
||||
_, err := svc.ClearPasscode(ctx, 1)
|
||||
checkAuthErr(t, c.shoudFailWithAuth, err)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("happy path ipados", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: hostID, UUID: "host-uuid-1", Platform: "ipados"}, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
return &fleet.HostMDM{}, nil
|
||||
}
|
||||
|
||||
ctx := test.UserContext(t.Context(), test.UserAdmin)
|
||||
_, err := svc.ClearPasscode(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
|
||||
mdmStorage.EnqueueCommandFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("happy path ios", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: hostID, UUID: "host-uuid-2", Platform: "ios"}, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
return &fleet.HostMDM{}, nil
|
||||
}
|
||||
|
||||
ctx := test.UserContext(t.Context(), test.UserAdmin)
|
||||
_, err := svc.ClearPasscode(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
|
||||
mdmStorage.EnqueueCommandFuncInvoked = false
|
||||
})
|
||||
|
||||
t.Run("non-apple platform", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: hostID, Platform: "windows"}, nil
|
||||
}
|
||||
|
||||
ctx := test.UserContext(t.Context(), test.UserAdmin)
|
||||
_, err := svc.ClearPasscode(ctx, 1)
|
||||
require.Error(t, err)
|
||||
var badReq *fleet.BadRequestError
|
||||
require.ErrorAs(t, err, &badReq)
|
||||
assert.Contains(t, badReq.Message, "only supported on Apple mobile platforms")
|
||||
})
|
||||
|
||||
t.Run("macOS not supported", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: hostID, Platform: "darwin"}, nil
|
||||
}
|
||||
|
||||
ctx := test.UserContext(t.Context(), test.UserAdmin)
|
||||
_, err := svc.ClearPasscode(ctx, 1)
|
||||
require.Error(t, err)
|
||||
var badReq *fleet.BadRequestError
|
||||
require.ErrorAs(t, err, &badReq)
|
||||
assert.Contains(t, badReq.Message, "only supported on Apple mobile platforms")
|
||||
})
|
||||
|
||||
t.Run("MDM not enabled", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: hostID, Platform: "ipados"}, nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: false}}, nil
|
||||
}
|
||||
|
||||
ctx := test.UserContext(t.Context(), test.UserAdmin)
|
||||
_, err := svc.ClearPasscode(ctx, 1)
|
||||
require.Error(t, err)
|
||||
var badReq *fleet.BadRequestError
|
||||
require.ErrorAs(t, err, &badReq)
|
||||
assert.Contains(t, badReq.Message, "Apple MDM is not enabled")
|
||||
|
||||
// Restore for subsequent tests.
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}}, nil
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("personal enrollment", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: hostID, Platform: "ipados"}, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
return &fleet.HostMDM{IsPersonalEnrollment: true}, nil
|
||||
}
|
||||
|
||||
ctx := test.UserContext(t.Context(), test.UserAdmin)
|
||||
_, err := svc.ClearPasscode(ctx, 1)
|
||||
require.Error(t, err)
|
||||
var badReq *fleet.BadRequestError
|
||||
require.ErrorAs(t, err, &badReq)
|
||||
assert.Contains(t, badReq.Message, "Unlock token is not available")
|
||||
})
|
||||
|
||||
t.Run("enqueue command error", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{ID: hostID, UUID: "host-uuid-3", Platform: "ipados"}, nil
|
||||
}
|
||||
ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) {
|
||||
return &fleet.HostMDM{}, nil
|
||||
}
|
||||
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *nanomdm_mdm.CommandWithSubtype) (map[string]error, error) {
|
||||
return nil, errors.New("enqueue failed")
|
||||
}
|
||||
|
||||
ctx := test.UserContext(t.Context(), test.UserAdmin)
|
||||
_, err := svc.ClearPasscode(ctx, 1)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "enqueue failed")
|
||||
|
||||
// Restore for subsequent tests.
|
||||
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *nanomdm_mdm.CommandWithSubtype) (map[string]error, error) {
|
||||
return nil, nil
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("host not found", func(t *testing.T) {
|
||||
ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) {
|
||||
return nil, ¬FoundError{}
|
||||
}
|
||||
|
||||
ctx := test.UserContext(t.Context(), test.UserAdmin)
|
||||
_, err := svc.ClearPasscode(ctx, 999)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1017,9 +1017,11 @@ func (c *TestAppleMDMClient) Authenticate() error {
|
|||
func (c *TestAppleMDMClient) TokenUpdate(awaitingConfiguration bool) error {
|
||||
pushMagic := "pushmagic" + c.SerialNumber
|
||||
token := []byte("token" + c.SerialNumber)
|
||||
unlockToken := []byte("unlocktoken" + c.SerialNumber)
|
||||
if c.SerialNumber == "" {
|
||||
pushMagic = "pushmagic" + c.Identifier()
|
||||
token = []byte("token" + c.Identifier())
|
||||
unlockToken = []byte("unlocktoken" + c.Identifier())
|
||||
}
|
||||
payload := map[string]any{
|
||||
"MessageType": "TokenUpdate",
|
||||
|
|
@ -1028,6 +1030,7 @@ func (c *TestAppleMDMClient) TokenUpdate(awaitingConfiguration bool) error {
|
|||
"NotOnConsole": "false",
|
||||
"PushMagic": pushMagic,
|
||||
"Token": token,
|
||||
"UnlockToken": unlockToken,
|
||||
}
|
||||
if c.UUID != "" {
|
||||
payload["UDID"] = c.UUID
|
||||
|
|
|
|||
|
|
@ -7207,31 +7207,27 @@ LIMIT ?
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetNanoMDMEnrollmentDetails(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
res := []struct {
|
||||
LastMDMEnrollmentTime *time.Time `db:"authenticate_at"`
|
||||
LastMDMSeenTime *time.Time `db:"last_seen_at"`
|
||||
HardwareAttested bool `db:"hardware_attested"`
|
||||
}{}
|
||||
func (ds *Datastore) GetNanoMDMEnrollmentDetails(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
res := []*fleet.NanoMDMEnrollmentDetails{}
|
||||
// We are specifically only looking at the singular device enrollment row and not the
|
||||
// potentially many user enrollment rows that will exist for a given device. The device
|
||||
// enrollment row is the only one that gets regularly updated with the last seen time. Along
|
||||
// those same lines authenticate_at gets updated only at the authenticate step during the
|
||||
// enroll process and as such is a good indicator of the last enrollment or reenrollment.
|
||||
query := `
|
||||
SELECT nd.authenticate_at, ne.last_seen_at, ne.hardware_attested
|
||||
SELECT nd.authenticate_at, ne.last_seen_at, ne.hardware_attested, nd.unlock_token
|
||||
FROM nano_devices nd
|
||||
INNER JOIN nano_enrollments ne ON ne.id = nd.id
|
||||
WHERE ne.type IN ('Device', 'User Enrollment (Device)') AND nd.id = ?`
|
||||
err := sqlx.SelectContext(ctx, ds.reader(ctx), &res, query, hostUUID)
|
||||
|
||||
if err == sql.ErrNoRows || len(res) == 0 {
|
||||
return nil, nil, false, nil
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, false, ctxerr.Wrap(ctx, err, "get mdm enrollment times")
|
||||
return nil, ctxerr.Wrap(ctx, err, "get mdm enrollment times")
|
||||
}
|
||||
return res[0].LastMDMEnrollmentTime, res[0].LastMDMSeenTime, res[0].HardwareAttested, nil
|
||||
return res[0], nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) AssociateHostMDMIdPAccount(ctx context.Context, hostUUID, idpAcctUUID string) error {
|
||||
|
|
|
|||
|
|
@ -5417,9 +5417,9 @@ func testMDMAppleResetEnrollment(t *testing.T, ds *Datastore) {
|
|||
`, host.UUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, hardwareAttested, err := ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
details, err := ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, hardwareAttested)
|
||||
require.True(t, details.HardwareAttested)
|
||||
|
||||
// host has no boostrap package command yet
|
||||
_, err = ds.GetHostBootstrapPackageCommand(ctx, host.UUID)
|
||||
|
|
@ -5472,9 +5472,9 @@ func testMDMAppleResetEnrollment(t *testing.T, ds *Datastore) {
|
|||
require.Zero(t, sum.Installed)
|
||||
require.Zero(t, sum.Pending)
|
||||
|
||||
_, _, hardwareAttested, err = ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
details, err = ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, hardwareAttested)
|
||||
require.False(t, details.HardwareAttested)
|
||||
}
|
||||
|
||||
func testMDMAppleDeleteHostDEPAssignments(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -9117,19 +9117,19 @@ func testGetNanoMDMEnrollmentDetails(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
lastMDMEnrolledAt, lastMDMSeenAt, _, err := ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
details, err := ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, lastMDMEnrolledAt)
|
||||
require.Nil(t, lastMDMSeenAt)
|
||||
require.Nil(t, details.LastMDMEnrollmentTime)
|
||||
require.Nil(t, details.LastMDMSeenTime)
|
||||
|
||||
// add user and device enrollment for this device. Timestamps should not be updated so nothing
|
||||
// returned yet
|
||||
nanoEnroll(t, ds, host, true)
|
||||
|
||||
lastMDMEnrolledAt, lastMDMSeenAt, _, err = ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
details, err = ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, lastMDMEnrolledAt) // defaults to current time on creation
|
||||
require.NotNil(t, lastMDMSeenAt) // defaults to time 0 value
|
||||
require.NotNil(t, details.LastMDMEnrollmentTime) // defaults to current time on creation
|
||||
require.NotNil(t, details.LastMDMSeenTime) // defaults to time 0 value
|
||||
|
||||
// Add a BYOD host
|
||||
byodHost, err := ds.NewHost(ctx, &fleet.Host{
|
||||
|
|
@ -9143,10 +9143,10 @@ func testGetNanoMDMEnrollmentDetails(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
nanoEnrollUserDevice(t, ds, byodHost)
|
||||
|
||||
lastMDMEnrolledAt, lastMDMSeenAt, _, err = ds.GetNanoMDMEnrollmentDetails(ctx, byodHost.UUID)
|
||||
details, err = ds.GetNanoMDMEnrollmentDetails(ctx, byodHost.UUID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, lastMDMEnrolledAt) // defaults to current time on creation
|
||||
require.NotNil(t, lastMDMSeenAt) // defaults to time 0 value
|
||||
require.NotNil(t, details.LastMDMEnrollmentTime) // defaults to current time on creation
|
||||
require.NotNil(t, details.LastMDMSeenTime) // defaults to time 0 value
|
||||
|
||||
authenticateTime := time.Now().Add(-1 * time.Hour).UTC().Round(time.Second)
|
||||
deviceEnrollTime := time.Now().Add(-2 * time.Hour).UTC().Round(time.Second)
|
||||
|
|
@ -9179,19 +9179,19 @@ func testGetNanoMDMEnrollmentDetails(t *testing.T, ds *Datastore) {
|
|||
return nil
|
||||
})
|
||||
|
||||
lastMDMEnrolledAt, lastMDMSeenAt, _, err = ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
details, err = ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, lastMDMEnrolledAt)
|
||||
assert.Equal(t, authenticateTime, *lastMDMEnrolledAt)
|
||||
require.NotNil(t, lastMDMSeenAt)
|
||||
assert.Equal(t, deviceEnrollTime, *lastMDMSeenAt)
|
||||
require.NotNil(t, details)
|
||||
assert.Equal(t, authenticateTime, *details.LastMDMEnrollmentTime)
|
||||
require.NotNil(t, details.LastMDMSeenTime)
|
||||
assert.Equal(t, deviceEnrollTime, *details.LastMDMSeenTime)
|
||||
|
||||
lastMDMEnrolledAt, lastMDMSeenAt, _, err = ds.GetNanoMDMEnrollmentDetails(ctx, byodHost.UUID)
|
||||
details, err = ds.GetNanoMDMEnrollmentDetails(ctx, byodHost.UUID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, lastMDMEnrolledAt)
|
||||
assert.Equal(t, byodDeviceAuthenticateTime, *lastMDMEnrolledAt)
|
||||
require.NotNil(t, lastMDMSeenAt)
|
||||
assert.Equal(t, byodDeviceEnrollTime, *lastMDMSeenAt)
|
||||
require.NotNil(t, details)
|
||||
assert.Equal(t, byodDeviceAuthenticateTime, *details.LastMDMEnrollmentTime)
|
||||
require.NotNil(t, details.LastMDMSeenTime)
|
||||
assert.Equal(t, byodDeviceEnrollTime, *details.LastMDMSeenTime)
|
||||
}
|
||||
|
||||
func testGetNanoMDMUserEnrollment(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -9217,10 +9217,10 @@ func testGetNanoMDMUserEnrollment(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
lastMDMEnrolledAt, lastMDMSeenAt, _, err := ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
details, err := ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, lastMDMEnrolledAt)
|
||||
require.Nil(t, lastMDMSeenAt)
|
||||
require.Nil(t, details.LastMDMEnrollmentTime)
|
||||
require.Nil(t, details.LastMDMSeenTime)
|
||||
|
||||
userEnrollment, err = ds.GetNanoMDMUserEnrollment(ctx, host.UUID)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package mysql
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -464,6 +465,21 @@ func (ds *Datastore) ExpandHostSecrets(ctx context.Context, document string, enr
|
|||
return "", ctxerr.Wrapf(ctx, err, "getting pending recovery lock password for host %s", enrollmentID)
|
||||
}
|
||||
secretValues[secretType] = password
|
||||
case fleet.HostSecretMDMUnlockToken:
|
||||
details, err := ds.GetNanoMDMEnrollmentDetails(ctx, enrollmentID)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrapf(ctx, err, "getting MDM enrollment details for host %s", enrollmentID)
|
||||
}
|
||||
if details == nil {
|
||||
return "", ctxerr.Errorf(ctx, "no MDM enrollment details found for host %s", enrollmentID)
|
||||
}
|
||||
if details.UnlockToken == nil {
|
||||
return "", ctxerr.Errorf(ctx, "%s", fleet.CantClearPasscodePersonalHostsMessage)
|
||||
}
|
||||
|
||||
// We need to send base64 encoded data in the <data> field.
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(*details.UnlockToken))
|
||||
secretValues[secretType] = encoded
|
||||
default:
|
||||
return "", ctxerr.Errorf(ctx, "unknown host secret type: %s", secretType)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"testing"
|
||||
|
|
@ -8,6 +9,7 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -322,6 +324,37 @@ func testExpandHostSecrets(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(t, `Password: Pass&word<with>special"chars'`, expandedNonXML)
|
||||
})
|
||||
|
||||
t.Run("mdm unlock token expansion", func(t *testing.T) {
|
||||
// Create a host with an MDM unlock token
|
||||
hostMDM, err := ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
OsqueryHostID: ptr.String("host-mdm-unlock-token-test"),
|
||||
NodeKey: ptr.String("host-mdm-unlock-token-test-key"),
|
||||
UUID: "host-mdm-unlock-token-test-uuid",
|
||||
Hostname: "host-mdm-unlock-token-test-hostname",
|
||||
Platform: "ios",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
unlockToken := "TEST-MDM-UNLOCK-TOKEN" // nolint:gosec // G101: this is a constant identifier, not a credential
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `INSERT INTO nano_devices (id, unlock_token, authenticate, platform) VALUES (?, ?, 'fake-auth', 'ios')`, hostMDM.UUID, unlockToken)
|
||||
require.NoError(t, err)
|
||||
_, err = q.ExecContext(ctx, `INSERT INTO nano_enrollments (id, device_id, type, topic, push_magic, token_hex, last_seen_at) VALUES (?, ?, 'Device', 'fake-topic', 'fake-push-magic', 'fake-token-hex', NOW())`, hostMDM.UUID, hostMDM.UUID)
|
||||
return err
|
||||
})
|
||||
|
||||
b64Encoded := base64.StdEncoding.EncodeToString([]byte(unlockToken))
|
||||
doc := `<string>$FLEET_HOST_SECRET_MDM_UNLOCK_TOKEN</string>`
|
||||
expected := `<string>` + b64Encoded + `</string>`
|
||||
expanded, err := ds.ExpandHostSecrets(ctx, doc, hostMDM.UUID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, expanded)
|
||||
})
|
||||
}
|
||||
|
||||
func testCreateSecretVariable(t *testing.T, ds *Datastore) {
|
||||
|
|
|
|||
|
|
@ -1852,3 +1852,20 @@ func (a ActivityTypeInstalledCertificate) WasFromAutomation() bool {
|
|||
func (a ActivityTypeInstalledCertificate) HostOnly() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type ActivityTypeClearedPasscode struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeClearedPasscode) ActivityName() string {
|
||||
return "cleared_passcode"
|
||||
}
|
||||
|
||||
func (a ActivityTypeClearedPasscode) HostIDs() []uint {
|
||||
return []uint{a.HostID}
|
||||
}
|
||||
|
||||
func (a ActivityTypeClearedPasscode) HostOnly() bool {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ type MDMAppleCommandIssuer interface {
|
|||
DeviceConfigured(ctx context.Context, hostUUID, cmdUUID string) error
|
||||
SetRecoveryLock(ctx context.Context, hostUUIDs []string, cmdUUID string) error
|
||||
RotateRecoveryLock(ctx context.Context, hostUUID string, cmdUUID string) error
|
||||
ClearPasscode(ctx context.Context, hostUUID []string, cmdUUID string) error
|
||||
}
|
||||
|
||||
// MDMAppleEnrollmentType is the type for Apple MDM enrollments.
|
||||
|
|
|
|||
|
|
@ -1469,7 +1469,7 @@ type Datastore interface {
|
|||
|
||||
// GetNanoMDMEnrollmentDetails returns the time of the most recent enrollment, the most recent
|
||||
// MDM protocol seen time, and whether the enrollment is hardware attested for the host with the given UUID
|
||||
GetNanoMDMEnrollmentDetails(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error)
|
||||
GetNanoMDMEnrollmentDetails(ctx context.Context, hostUUID string) (*NanoMDMEnrollmentDetails, error)
|
||||
|
||||
// IncreasePolicyAutomationIteration marks the policy to fire automation again.
|
||||
IncreasePolicyAutomationIteration(ctx context.Context, policyID uint) error
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ var (
|
|||
CantTurnOffMDMForPersonalHostsMessage = "Couldn't turn off MDM. This command isn't available for personal hosts."
|
||||
CantWipePersonalHostsMessage = "Couldn't wipe. This command isn't available for personal hosts."
|
||||
CantLockPersonalHostsMessage = "Couldn't lock. This command isn't available for personal hosts."
|
||||
CantClearPasscodePersonalHostsMessage = "Unlock token is not available for this device. Unable to issue ClearPasscode command."
|
||||
CantLockManualIOSIpadOSHostsMessage = "Couldn't lock. This command isn't available for manually enrolled iOS/iPadOS hosts."
|
||||
CantDisableDiskEncryptionIfPINRequiredErrMsg = "Couldn't disable disk encryption, you need to disable the BitLocker PIN requirement first."
|
||||
CantEnablePINRequiredIfDiskEncryptionEnabled = "Couldn't enable BitLocker PIN requirement, you must enable disk encryption first."
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ const (
|
|||
// We wrap the key in braces to make Redis hash the keys to the same slot, avoiding CrossSlot errors.
|
||||
MDMProfileProcessingKeyPrefix = "{mdm_profile_processing}" // + :hostUUID
|
||||
MDMProfileProcessingTTL = 1 * time.Minute // We use a low time here, to avoid letting it sit for too long in case of errors.
|
||||
|
||||
AppleMDMCommandTypeClearPasscode = "ClearPasscode"
|
||||
)
|
||||
|
||||
// FleetVarName represents the name of a Fleet variable (without the FLEET_VAR_ prefix).
|
||||
|
|
@ -1285,3 +1287,10 @@ type HostMDMIdentifiers struct {
|
|||
Platform string `db:"platform"`
|
||||
TeamID *uint `db:"team_id"`
|
||||
}
|
||||
|
||||
type NanoMDMEnrollmentDetails struct {
|
||||
LastMDMEnrollmentTime *time.Time `db:"authenticate_at"`
|
||||
LastMDMSeenTime *time.Time `db:"last_seen_at"`
|
||||
HardwareAttested bool `db:"hardware_attested"`
|
||||
UnlockToken *string `db:"unlock_token"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ const (
|
|||
// during password rotation. The pending password is stored encrypted in host_recovery_key_passwords
|
||||
// (pending_encrypted_password column) and injected as the NewPassword during rotation.
|
||||
HostSecretRecoveryLockPendingPassword = "RECOVERY_LOCK_PENDING_PASSWORD"
|
||||
|
||||
// HostSecretMDMUnlockToken is the host secret type for MDM unlock tokens.
|
||||
// The token is stored in the nano_devices table and injected at delivery time for ClearPasscode commands sent to Apple MDM-enrolled hosts.
|
||||
HostSecretMDMUnlockToken = "MDM_UNLOCK_TOKEN" // nolint:gosec // G101: this is a constant identifier, not a credential
|
||||
)
|
||||
|
||||
type MissingSecretsError struct {
|
||||
|
|
|
|||
|
|
@ -1333,6 +1333,10 @@ type Service interface {
|
|||
UnlockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
|
||||
WipeHost(ctx context.Context, hostID uint, metadata *MDMWipeMetadata) error
|
||||
|
||||
// ClearPasscode is a method that clears the passcode on a host, primarily mobile devices.
|
||||
// Not script based, only MDM based.
|
||||
ClearPasscode(ctx context.Context, hostID uint) (*CommandEnqueueResult, error)
|
||||
|
||||
// RotateRecoveryLockPassword rotates the recovery lock password for a macOS host.
|
||||
// This is only available for Apple Silicon Macs that are MDM-enrolled and have
|
||||
// an existing recovery lock password.
|
||||
|
|
|
|||
|
|
@ -492,6 +492,33 @@ func (svc *MDMAppleCommander) DeviceLocation(ctx context.Context, hostUUIDs []st
|
|||
return svc.EnqueueCommand(ctx, hostUUIDs, raw)
|
||||
}
|
||||
|
||||
func (svc *MDMAppleCommander) ClearPasscode(ctx context.Context, hostUUIDs []string, cmdUUID string) error {
|
||||
raw := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Command</key>
|
||||
<dict>
|
||||
<key>RequestType</key>
|
||||
<string>ClearPasscode</string>
|
||||
<key>UnlockToken</key>
|
||||
<data>%s</data>
|
||||
</dict>
|
||||
<key>CommandUUID</key>
|
||||
<string>%s</string>
|
||||
</dict>
|
||||
</plist>`, "$"+fleet.HostSecretPrefix+fleet.HostSecretMDMUnlockToken, cmdUUID)
|
||||
|
||||
// We skip EnqueueCommand here, to avoid decoding the command as <data> is binary, which fails to decode with placeholder.
|
||||
cmd := &mdm.Command{
|
||||
CommandUUID: cmdUUID,
|
||||
Raw: []byte(raw),
|
||||
}
|
||||
cmd.Command.RequestType = fleet.AppleMDMCommandTypeClearPasscode
|
||||
|
||||
return svc.enqueueAndNotify(ctx, hostUUIDs, cmd, mdm.CommandSubtypeNone)
|
||||
}
|
||||
|
||||
// EnqueueCommand takes care of enqueuing the commands and sending push
|
||||
// notifications to the devices.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -623,3 +623,53 @@ func TestMDMAppleCommanderSetRecoveryLock(t *testing.T) {
|
|||
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
|
||||
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
||||
}
|
||||
|
||||
func TestMDMAppleCommanderClearPasscode(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mdmStorage := &mdmmock.MDMAppleStore{}
|
||||
pushFactory, _ := newMockAPNSPushProviderFactory()
|
||||
pusher := nanomdm_pushsvc.New(
|
||||
mdmStorage,
|
||||
mdmStorage,
|
||||
pushFactory,
|
||||
stdlogfmt.New(),
|
||||
)
|
||||
cmdr := NewMDMAppleCommander(mdmStorage, pusher)
|
||||
|
||||
hostUUIDs := []string{"host-uuid-1"}
|
||||
cmdUUID := uuid.New().String()
|
||||
mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.CommandWithSubtype) (map[string]error, error) {
|
||||
require.NotNil(t, cmd)
|
||||
require.Equal(t, "ClearPasscode", cmd.Command.Command.RequestType)
|
||||
require.Contains(t, string(cmd.Raw), "$"+fleet.HostSecretPrefix+fleet.HostSecretMDMUnlockToken, "Clear passcode should not use direct unlock token but rather Host secret")
|
||||
require.Contains(t, string(cmd.Raw), cmdUUID)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
mdmStorage.RetrievePushInfoFunc = func(ctx context.Context, targetUUIDs []string) (map[string]*mdm.Push, error) {
|
||||
require.ElementsMatch(t, hostUUIDs, targetUUIDs)
|
||||
pushes := make(map[string]*mdm.Push, len(targetUUIDs))
|
||||
for _, uuid := range targetUUIDs {
|
||||
pushes[uuid] = &mdm.Push{
|
||||
PushMagic: "magic" + uuid,
|
||||
Token: []byte("token" + uuid),
|
||||
Topic: "topic" + uuid,
|
||||
}
|
||||
}
|
||||
return pushes, nil
|
||||
}
|
||||
|
||||
mdmStorage.RetrievePushCertFunc = func(ctx context.Context, topic string) (*tls.Certificate, string, error) {
|
||||
cert, err := tls.LoadX509KeyPair("../../service/testdata/server.pem", "../../service/testdata/server.key")
|
||||
return &cert, "", err
|
||||
}
|
||||
|
||||
mdmStorage.IsPushCertStaleFunc = func(ctx context.Context, topic string, staleToken string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err := cmdr.ClearPasscode(ctx, hostUUIDs, cmdUUID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, mdmStorage.EnqueueCommandFuncInvoked)
|
||||
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,11 +286,9 @@ func (s *Service) CommandAndReportResults(r *mdm.Request, results *mdm.CommandRe
|
|||
cmd.Raw = []byte(expanded)
|
||||
}
|
||||
|
||||
// Expand host-scoped secrets for SetRecoveryLock commands.
|
||||
// SetRecoveryLock is device-only, so UDID is always present and matches host UUID.
|
||||
if cmd.Command.Command.RequestType == fleet.SetRecoveryLockCmdName {
|
||||
expandHostSecrets := func(cmdRaw string, onError func(hostUUID string, errorMsg string)) (expanded string, didError bool) {
|
||||
hostUUID := results.UDID
|
||||
hostExpanded, err := s.store.ExpandHostSecrets(r.Context, string(cmd.Raw), hostUUID)
|
||||
hostExpanded, err := s.store.ExpandHostSecrets(r.Context, cmdRaw, hostUUID)
|
||||
if err != nil {
|
||||
errorMsg := fmt.Sprintf("failed to expand host secrets: %v", err)
|
||||
logger.Info("level", "error", "msg", "expanding host secrets", "err", err)
|
||||
|
|
@ -308,12 +306,41 @@ func (s *Service) CommandAndReportResults(r *mdm.Request, results *mdm.CommandRe
|
|||
if storeErr := s.store.StoreCommandReport(r, failedResult); storeErr != nil {
|
||||
logger.Info("level", "error", "msg", "storing failed command result", "err", storeErr)
|
||||
}
|
||||
|
||||
onError(hostUUID, errorMsg)
|
||||
|
||||
return "", true
|
||||
}
|
||||
|
||||
return hostExpanded, false
|
||||
}
|
||||
|
||||
// Expand host-scoped secrets for SetRecoveryLock commands.
|
||||
// SetRecoveryLock is device-only, so UDID is always present and matches host UUID.
|
||||
if cmd.Command.Command.RequestType == fleet.SetRecoveryLockCmdName {
|
||||
hostExpanded, didError := expandHostSecrets(string(cmd.Raw), func(hostUUID string, errorMsg string) {
|
||||
// Mark the host's recovery lock status as failed so it's not stuck in pending.
|
||||
if storeErr := s.store.SetRecoveryLockFailed(r.Context, hostUUID, errorMsg); storeErr != nil {
|
||||
logger.Info("level", "error", "msg", "setting recovery lock failed", "err", storeErr)
|
||||
}
|
||||
})
|
||||
if didError {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cmd.Raw = []byte(hostExpanded)
|
||||
}
|
||||
|
||||
// Expand host-scoped secrets for ClearPasscode commands.
|
||||
if cmd.Command.Command.RequestType == fleet.AppleMDMCommandTypeClearPasscode {
|
||||
hostExpanded, didError := expandHostSecrets(string(cmd.Raw), func(hostUUID string, errorMsg string) {
|
||||
// For ClearPasscode command, if we fail to expand host secrets, it likely means the unlock token is missing or invalid.
|
||||
logger.Info("level", "error", "msg", "failed to expand host secrets for ClearPasscode command", "host_uuid", hostUUID, "err", errorMsg)
|
||||
})
|
||||
if didError {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cmd.Raw = []byte(hostExpanded)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1007,7 +1007,7 @@ type GetNanoMDMUserEnrollmentUsernameAndUUIDFunc func(ctx context.Context, devic
|
|||
|
||||
type UpdateNanoMDMUserEnrollmentUsernameFunc func(ctx context.Context, deviceID string, userUUID string, username string) error
|
||||
|
||||
type GetNanoMDMEnrollmentDetailsFunc func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error)
|
||||
type GetNanoMDMEnrollmentDetailsFunc func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error)
|
||||
|
||||
type IncreasePolicyAutomationIterationFunc func(ctx context.Context, policyID uint) error
|
||||
|
||||
|
|
@ -8056,7 +8056,7 @@ func (s *DataStore) UpdateNanoMDMUserEnrollmentUsername(ctx context.Context, dev
|
|||
return s.UpdateNanoMDMUserEnrollmentUsernameFunc(ctx, deviceID, userUUID, username)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetNanoMDMEnrollmentDetails(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
func (s *DataStore) GetNanoMDMEnrollmentDetails(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
s.mu.Lock()
|
||||
s.GetNanoMDMEnrollmentDetailsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -812,6 +812,8 @@ type UnlockHostFunc func(ctx context.Context, hostID uint) (unlockPIN string, er
|
|||
|
||||
type WipeHostFunc func(ctx context.Context, hostID uint, metadata *fleet.MDMWipeMetadata) error
|
||||
|
||||
type ClearPasscodeFunc func(ctx context.Context, hostID uint) (*fleet.CommandEnqueueResult, error)
|
||||
|
||||
type RotateRecoveryLockPasswordFunc func(ctx context.Context, hostID uint) error
|
||||
|
||||
type UploadSoftwareInstallerFunc func(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (*fleet.SoftwareInstaller, error)
|
||||
|
|
@ -2095,6 +2097,9 @@ type Service struct {
|
|||
WipeHostFunc WipeHostFunc
|
||||
WipeHostFuncInvoked bool
|
||||
|
||||
ClearPasscodeFunc ClearPasscodeFunc
|
||||
ClearPasscodeFuncInvoked bool
|
||||
|
||||
RotateRecoveryLockPasswordFunc RotateRecoveryLockPasswordFunc
|
||||
RotateRecoveryLockPasswordFuncInvoked bool
|
||||
|
||||
|
|
@ -5011,6 +5016,13 @@ func (s *Service) WipeHost(ctx context.Context, hostID uint, metadata *fleet.MDM
|
|||
return s.WipeHostFunc(ctx, hostID, metadata)
|
||||
}
|
||||
|
||||
func (s *Service) ClearPasscode(ctx context.Context, hostID uint) (*fleet.CommandEnqueueResult, error) {
|
||||
s.mu.Lock()
|
||||
s.ClearPasscodeFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.ClearPasscodeFunc(ctx, hostID)
|
||||
}
|
||||
|
||||
func (s *Service) RotateRecoveryLockPassword(ctx context.Context, hostID uint) error {
|
||||
s.mu.Lock()
|
||||
s.RotateRecoveryLockPasswordFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -199,8 +199,12 @@ func setupAppleMDMService(t *testing.T, license *fleet.LicenseInfo) (fleet.Servi
|
|||
ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) {
|
||||
return &fleet.NanoEnrollment{Enabled: false}, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{
|
||||
LastMDMEnrollmentTime: nil,
|
||||
LastMDMSeenTime: nil,
|
||||
HardwareAttested: false,
|
||||
}, nil
|
||||
}
|
||||
ds.GetMDMAppleCommandRequestTypeFunc = func(ctx context.Context, commandUUID string) (string, error) {
|
||||
return "", nil
|
||||
|
|
@ -451,6 +455,23 @@ func TestAppleMDMAuthorization(t *testing.T) {
|
|||
_, err = svc.EnqueueMDMAppleCommand(ctx, rawB64PremiumCmd, []string{"host1"})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, fleet.ErrMissingLicense.Error())
|
||||
|
||||
rawB64PremiumCmd = base64.RawStdEncoding.EncodeToString(fmt.Appendf([]byte{}, `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Command</key>
|
||||
<dict>
|
||||
<key>RequestType</key>
|
||||
<string>%s</string>
|
||||
</dict>
|
||||
<key>CommandUUID</key>
|
||||
<string>uuid</string>
|
||||
</dict>
|
||||
</plist>`, "ClearPasscode"))
|
||||
_, err = svc.EnqueueMDMAppleCommand(ctx, rawB64PremiumCmd, []string{"host1"})
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, fleet.ErrMissingLicense.Error())
|
||||
})
|
||||
|
||||
cmdUUIDToHostUUIDs := map[string][]string{
|
||||
|
|
|
|||
|
|
@ -567,6 +567,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/lock", lockHostEndpoint, lockHostRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/unlock", unlockHostEndpoint, unlockHostRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/wipe", wipeHostEndpoint, wipeHostRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/clear_passcode", clearPasscodeEndpoint, clearPasscodeRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/recovery_lock_password/rotate", rotateRecoveryLockPasswordEndpoint, rotateRecoveryLockPasswordRequest{})
|
||||
|
||||
// Generative AI
|
||||
|
|
|
|||
|
|
@ -1788,7 +1788,12 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f
|
|||
|
||||
// fetch host last seen at and last enrolled at times, currently only supported for
|
||||
// Apple platforms
|
||||
mdmLastEnrollment, mdmLastCheckedIn, mdmHardwareAttested, err = svc.ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
details, err := svc.ds.GetNanoMDMEnrollmentDetails(ctx, host.UUID)
|
||||
if details != nil {
|
||||
mdmLastCheckedIn = details.LastMDMSeenTime
|
||||
mdmLastEnrollment = details.LastMDMEnrollmentTime
|
||||
mdmHardwareAttested = details.HardwareAttested
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get host mdm enrollment times")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,8 +152,8 @@ func TestHostDetailsMDMAppleDiskEncryption(t *testing.T) {
|
|||
ds.ConditionalAccessBypassedAtFunc = func(ctx context.Context, hostID uint) (*time.Time, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
|
||||
return false, nil
|
||||
|
|
@ -465,8 +465,12 @@ func TestHostDetailsMDMTimestamps(t *testing.T) {
|
|||
|
||||
ts1 := time.Now().Add(-1 * time.Hour).UTC()
|
||||
ts2 := time.Now().Add(-2 * time.Hour).UTC()
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return &ts1, &ts2, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{
|
||||
LastMDMEnrollmentTime: &ts1,
|
||||
LastMDMSeenTime: &ts2,
|
||||
HardwareAttested: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
|
|
@ -556,8 +560,8 @@ func TestHostDetailsOSSettings(t *testing.T) {
|
|||
ds.ConditionalAccessBypassedAtFunc = func(ctx context.Context, hostID uint) (*time.Time, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
|
||||
return false, nil
|
||||
|
|
@ -798,8 +802,8 @@ func TestHostDetailsRecoveryLockPasswordStatus(t *testing.T) {
|
|||
ds.IsHostDiskEncryptionKeyArchivedFunc = func(ctx context.Context, hostID uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) {
|
||||
return &fleet.HostDiskEncryptionKey{}, nil
|
||||
|
|
@ -2865,8 +2869,8 @@ func TestHostMDMProfileDetail(t *testing.T) {
|
|||
ds.ConditionalAccessBypassedAtFunc = func(ctx context.Context, hostID uint) (*time.Time, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
ds.UpdateHostIssuesFailingPoliciesFunc = func(ctx context.Context, hostIDs []uint) error {
|
||||
return nil
|
||||
|
|
@ -3009,8 +3013,8 @@ func TestHostMDMProfileScopes(t *testing.T) {
|
|||
ds.ConditionalAccessBypassedAtFunc = func(ctx context.Context, hostID uint) (*time.Time, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
ds.UpdateHostIssuesFailingPoliciesFunc = func(ctx context.Context, hostIDs []uint) error {
|
||||
return nil
|
||||
|
|
@ -3204,8 +3208,8 @@ func TestLockUnlockWipeHostAuth(t *testing.T) {
|
|||
ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*time.Time, *time.Time, bool, error) {
|
||||
return nil, nil, false, nil
|
||||
ds.GetNanoMDMEnrollmentDetailsFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoMDMEnrollmentDetails, error) {
|
||||
return &fleet.NanoMDMEnrollmentDetails{}, nil
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package service
|
|||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
|
|
@ -760,3 +761,41 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeWindowsLinux() {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestClearPasscodeCommand() {
|
||||
t := s.T()
|
||||
|
||||
s.enableABM(t.Name())
|
||||
|
||||
// Create iOS host and enroll in MDM
|
||||
iosHost, iosMDMClient := s.createAppleMobileHostThenDEPEnrollMDM("ios", mdmtest.RandSerialNumber())
|
||||
|
||||
// Trigger ClearPasscode endpoint
|
||||
var clearPasscodeResp clearPasscodeResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/clear_passcode", iosHost.ID), nil, http.StatusOK, &clearPasscodeResp)
|
||||
require.Equal(t, fleet.AppleMDMCommandTypeClearPasscode, clearPasscodeResp.RequestType)
|
||||
require.Equal(t, "ios", clearPasscodeResp.Platform)
|
||||
|
||||
s.lastHostActivityMatches(iosHost.ID, fleet.ActivityTypeClearedPasscode{}.ActivityName(), fmt.Sprintf(`{"host_id": %d, "host_display_name": %q}`, iosHost.ID, iosHost.DisplayName()), 0)
|
||||
|
||||
// Check in with the iOS device to receive the ClearPasscode command
|
||||
cmd, err := iosMDMClient.Idle()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cmd)
|
||||
require.Equal(t, fleet.AppleMDMCommandTypeClearPasscode, cmd.Command.RequestType)
|
||||
b64Encoded := base64.StdEncoding.EncodeToString([]byte("unlocktoken" + iosMDMClient.SerialNumber))
|
||||
require.Contains(t, string(cmd.Raw), b64Encoded)
|
||||
|
||||
// Acknowledge the ClearPasscode command
|
||||
_, err = iosMDMClient.Acknowledge(cmd.CommandUUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fetch the command result and check the response is acknowledged (+ Payload has the expected unlock token value)
|
||||
commandResultResp := &getMDMCommandResultsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/commands/results", &getMDMCommandResultsRequest{
|
||||
CommandUUID: clearPasscodeResp.CommandUUID,
|
||||
}, http.StatusOK, commandResultResp)
|
||||
require.Len(t, commandResultResp.Results, 1)
|
||||
require.Equal(t, fleet.AppleMDMCommandTypeClearPasscode, commandResultResp.Results[0].RequestType)
|
||||
require.NotNil(t, commandResultResp.Results[0].Result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16108,6 +16108,9 @@ func (s *integrationMDMTestSuite) TestAppleMDMActionsOnPersonalHost() {
|
|||
r = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusBadRequest)
|
||||
require.Contains(t, extractServerErrorText(r.Body), fleet.CantWipePersonalHostsMessage)
|
||||
|
||||
r = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/clear_passcode", host.ID), nil, http.StatusBadRequest)
|
||||
require.Contains(t, extractServerErrorText(r.Body), fleet.CantClearPasscodePersonalHostsMessage)
|
||||
|
||||
// Confirm that turning off MDM for personal hosts are allowed - NEEDS to be last, to not turn off MDM for the other checks.
|
||||
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", host.ID), nil, http.StatusNoContent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -644,8 +644,9 @@ func (svc *Service) validateAppleMDMCommand(ctx context.Context, rawXMLCmd []byt
|
|||
}
|
||||
|
||||
var appleMDMPremiumCommands = map[string]bool{
|
||||
"EraseDevice": true,
|
||||
"DeviceLock": true,
|
||||
"EraseDevice": true,
|
||||
"DeviceLock": true,
|
||||
"ClearPasscode": true,
|
||||
}
|
||||
|
||||
func (svc *Service) enqueueAppleMDMCommand(ctx context.Context, rawXMLCmd []byte, deviceIDs []string) (result *fleet.CommandEnqueueResult, err error) {
|
||||
|
|
@ -3707,3 +3708,31 @@ func (svc *Service) UnenrollMDM(ctx context.Context, hostID uint) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type clearPasscodeRequest struct {
|
||||
HostID uint `url:"id"`
|
||||
}
|
||||
|
||||
type clearPasscodeResponse struct {
|
||||
*fleet.CommandEnqueueResult
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r clearPasscodeResponse) Error() error { return r.Err }
|
||||
|
||||
func clearPasscodeEndpoint(ctx context.Context, request any, svc fleet.Service) (fleet.Errorer, error) {
|
||||
req := request.(*clearPasscodeRequest)
|
||||
res, err := svc.ClearPasscode(ctx, req.HostID)
|
||||
if err != nil {
|
||||
return clearPasscodeResponse{Err: err}, nil
|
||||
}
|
||||
return clearPasscodeResponse{CommandEnqueueResult: res}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ClearPasscode(ctx context.Context, hostID uint) (*fleet.CommandEnqueueResult, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,12 @@ import (
|
|||
)
|
||||
|
||||
// testSAMLIDPBaseURL is the SAML IDP base URL, read from FLEET_SAML_IDP_HTTP_PORT (defaults to http://localhost:9080).
|
||||
var testSAMLIDPBaseURL = getTestSAMLIDPBaseURL()
|
||||
var testSAMLIDPMetadataURL = testSAMLIDPBaseURL + "/simplesaml/saml2/idp/metadata.php"
|
||||
var testSAMLIDPSSOURL = testSAMLIDPBaseURL + "/simplesaml/saml2/idp/SSOService.php"
|
||||
var testSAMLIDPSLOURL = testSAMLIDPBaseURL + "/simplesaml/saml2/idp/SingleLogoutService.php"
|
||||
var (
|
||||
testSAMLIDPBaseURL = getTestSAMLIDPBaseURL()
|
||||
testSAMLIDPMetadataURL = testSAMLIDPBaseURL + "/simplesaml/saml2/idp/metadata.php"
|
||||
testSAMLIDPSSOURL = testSAMLIDPBaseURL + "/simplesaml/saml2/idp/SSOService.php"
|
||||
testSAMLIDPSLOURL = testSAMLIDPBaseURL + "/simplesaml/saml2/idp/SingleLogoutService.php"
|
||||
)
|
||||
|
||||
func getTestSAMLIDPBaseURL() string {
|
||||
if port := os.Getenv("FLEET_SAML_IDP_HTTP_PORT"); port != "" {
|
||||
|
|
@ -643,6 +645,27 @@ func (ts *withServer) lastActivityMatchesExtended(name, details string, id uint,
|
|||
return act.ID
|
||||
}
|
||||
|
||||
func (ts *withServer) lastHostActivityMatches(hostID uint, name, details string, id uint) uint {
|
||||
t := ts.s.T()
|
||||
var listActivities listActivitiesResponse
|
||||
ts.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", hostID), nil, http.StatusOK, &listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "10")
|
||||
require.NotEmpty(t, listActivities.Activities)
|
||||
|
||||
act := listActivities.Activities[0]
|
||||
|
||||
if name != "" {
|
||||
assert.Equal(t, name, act.Type)
|
||||
}
|
||||
if details != "" {
|
||||
require.NotNil(t, act.Details)
|
||||
assert.JSONEq(t, details, string(*act.Details))
|
||||
}
|
||||
if id > 0 {
|
||||
assert.Equal(t, id, act.ID)
|
||||
}
|
||||
return act.ID
|
||||
}
|
||||
|
||||
// gets the latest activity with the specified type name and checks that it
|
||||
// matches any provided properties. empty string or 0 id means do not check
|
||||
// that property. It returns the ID of that latest activity.
|
||||
|
|
|
|||
Loading…
Reference in a new issue