Merge branch 'main' into feature_19010-ipad-ios-lock-wipe

This commit is contained in:
Lucas Rodriguez 2024-06-14 12:45:47 -03:00
commit 63a6bb8bf3
No known key found for this signature in database
GPG key ID: 07D558B587D9E1FE
87 changed files with 814 additions and 307 deletions

View file

@ -35,6 +35,7 @@ What else should contributors [keep in mind](https://fleetdm.com/handbook/compan
- [ ] UI changes: TODO <!-- Insert the link to the relevant Figma cover page. Remove this checkbox if there are no changes to the user interface. -->
- [ ] CLI usage changes: TODO <!-- Insert the link to the relevant Figma cover page. Remove this checkbox if there are no changes to the CLI. -->
- [ ] REST API changes: TODO <!-- Specify changes as a draft PR to the REST API doc page. Remove this checkbox if there are no changes necessary. Move this item to the engineering list below if engineering will design the API changes. -->
- [ ] Fleet's agent (fleetd) changes: TODO <!-- Specify changes to fleetd. If the change requires a new Fleet (server) version, consider specifying to only enable this change in new Fleet versions. Remove this checkbox if there are no changes necessary. -->
- [ ] Permissions changes: TODO <!-- Specify changes as a draft PR to the Manage access doc page. If doc changes aren't necessary, explicitly mention no changes to the doc page. Remove this checkbox if there are no permissions changes. -->
- [ ] Outdated documentation changes: TODO <!-- Specify required documentation changes (public-facing fleetdm.com/docs or contributors) & redirects to add to /website/config/routes.js. -->
- [ ] Changes to paid features or tiers: TODO <!-- Specify "Fleet Free" or "Fleet Premium". If only certain parts of the user story involve paid features, specify which parts. Implementation of paid features should live in the `ee/` directory. -->

View file

@ -0,0 +1,2 @@
- Endpoint `/api/latest/fleet/users/admin` to return API token when creating API-only (non-SSO) users.
- Added API-token of the created API-only (non-SSO) user to the output of `fleetctl user create --api-only`.

1
changes/18427-cert-names Normal file
View file

@ -0,0 +1 @@
* Use Fleet instead of FleetDM in MDM certificates

View file

@ -0,0 +1 @@
- Fixed bug where MDM migration failed when attempting to renew enrollment profiles on macOS Sonoma devices.

2
changes/19545-unlock-pin Normal file
View file

@ -0,0 +1,2 @@
* /api/latest/fleet/hosts/:id/lock returns `unlock_pin` for Apple hosts
* UI no longer uses unlock pending state for Apple hosts

View file

@ -0,0 +1 @@
* Added a server setting to configure the query repory cap size, `server_settings.query_report_cap` (default is 1000).

View file

@ -0,0 +1 @@
- Improved db usage when sending statistics

View file

@ -418,6 +418,7 @@ func TestFullGlobalGitOps(t *testing.T) {
assert.Equal(t, orgName, savedAppConfig.OrgInfo.OrgName)
assert.Equal(t, fleetServerURL, savedAppConfig.ServerSettings.ServerURL)
assert.Contains(t, string(*savedAppConfig.AgentOptions), "distributed_denylist_duration")
assert.Equal(t, 2000, savedAppConfig.ServerSettings.QueryReportCap)
assert.Len(t, enrolledSecrets, 2)
assert.True(t, policyDeleted)
assert.Len(t, appliedPolicySpecs, 5)
@ -923,7 +924,6 @@ team_settings:
_ = runAppForTest(t, []string{"gitops", "-f", globalFile.Name(), "-f", teamFile.Name(), "--delete-other-teams"})
assert.True(t, ds.ListTeamsFuncInvoked)
assert.True(t, ds.DeleteTeamFuncInvoked)
}
func TestFullGlobalAndTeamGitOps(t *testing.T) {
@ -1059,7 +1059,6 @@ func TestTeamSofwareInstallersGitOps(t *testing.T) {
}
})
}
}
func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, **fleet.Team) {

View file

@ -2,6 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"os"
"slices"
@ -361,13 +362,6 @@ func TestMDMLockCommand(t *testing.T) {
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
}
macEnrolledUP := &fleet.Host{
ID: 9,
UUID: "mac-enrolled-up",
Platform: "darwin",
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
}
winEnrolledLP := &fleet.Host{
ID: 10,
@ -409,7 +403,6 @@ func TestMDMLockCommand(t *testing.T) {
macPending,
winPending,
winEnrolledUP,
macEnrolledUP,
winEnrolledLP,
macEnrolledLP,
winEnrolledWP,
@ -421,7 +414,6 @@ func TestMDMLockCommand(t *testing.T) {
unlockPending := map[uint]*fleet.Host{
winEnrolledUP.ID: winEnrolledUP,
macEnrolledUP.ID: macEnrolledUP,
}
lockPending := map[uint]*fleet.Host{
@ -446,9 +438,7 @@ func TestMDMLockCommand(t *testing.T) {
if _, ok := unlockPending[host.ID]; ok {
if fleetPlatform == "darwin" {
status.UnlockPIN = "1234"
status.UnlockRequestedAt = time.Now()
return &status, nil
return nil, errors.New("apple devices do not have an unlock pending state")
}
status.UnlockScript = &fleet.HostScriptResult{}
@ -542,7 +532,6 @@ fleetctl mdm unlock --host=%s
{appCfgWinMDM, "valid windows but pending ", []string{"--host", winPending.UUID}, `Can't lock the host because it doesn't have MDM turned on.`},
{appCfgMacMDM, "valid macos but pending", []string{"--host", macPending.UUID}, `Can't lock the host because it doesn't have MDM turned on.`},
{appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."},
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, "Host has pending unlock request."},
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},
@ -603,13 +592,6 @@ func TestMDMUnlockCommand(t *testing.T) {
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
}
macEnrolledUP := &fleet.Host{
ID: 9,
UUID: "mac-enrolled-up",
Platform: "darwin",
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
}
winEnrolledLP := &fleet.Host{
ID: 10,
UUID: "win-enrolled-lp",
@ -650,7 +632,6 @@ func TestMDMUnlockCommand(t *testing.T) {
macPending,
winPending,
winEnrolledUP,
macEnrolledUP,
winEnrolledLP,
macEnrolledLP,
winEnrolledWP,
@ -667,7 +648,6 @@ func TestMDMUnlockCommand(t *testing.T) {
unlockPending := map[uint]*fleet.Host{
winEnrolledUP.ID: winEnrolledUP,
macEnrolledUP.ID: macEnrolledUP,
}
lockPending := map[uint]*fleet.Host{
@ -701,9 +681,7 @@ func TestMDMUnlockCommand(t *testing.T) {
if _, ok := unlockPending[host.ID]; ok {
if fleetPlatform == "darwin" {
status.UnlockPIN = "1234"
status.UnlockRequestedAt = time.Now()
return &status, nil
return nil, errors.New("apple devices do not have an unlock pending state")
}
status.UnlockScript = &fleet.HostScriptResult{}
@ -800,7 +778,6 @@ fleetctl get host %s
{appCfgWinMDM, "valid windows but pending mdm enroll", []string{"--host", winPending.UUID}, `Can't unlock the host because it doesn't have MDM turned on.`},
{appCfgMacMDM, "valid macos but pending mdm enroll", []string{"--host", macPending.UUID}, `Can't unlock the host because it doesn't have MDM turned on.`},
{appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."},
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, ""},
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},
@ -856,13 +833,6 @@ func TestMDMWipeCommand(t *testing.T) {
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
}
macEnrolledUP := &fleet.Host{
ID: 9,
UUID: "mac-enrolled-up",
Platform: "darwin",
MDMInfo: &fleet.HostMDM{Enrolled: true, Name: fleet.WellKnownMDMFleet},
MDM: fleet.MDMHostData{Name: fleet.WellKnownMDMFleet, EnrollmentStatus: ptr.String("On (manual)")},
}
winEnrolledLP := &fleet.Host{
ID: 10,
UUID: "win-enrolled-lp",
@ -950,7 +920,6 @@ func TestMDMWipeCommand(t *testing.T) {
macPending,
winPending,
winEnrolledUP,
macEnrolledUP,
winEnrolledLP,
macEnrolledLP,
winEnrolledWP,
@ -971,7 +940,6 @@ func TestMDMWipeCommand(t *testing.T) {
unlockPending := map[uint]*fleet.Host{
winEnrolledUP.ID: winEnrolledUP,
macEnrolledUP.ID: macEnrolledUP,
}
lockPending := map[uint]*fleet.Host{
@ -1010,9 +978,7 @@ func TestMDMWipeCommand(t *testing.T) {
if _, ok := unlockPending[host.ID]; ok {
if fleetPlatform == "darwin" {
status.UnlockPIN = "1234"
status.UnlockRequestedAt = time.Now()
return &status, nil
return nil, errors.New("apple devices do not have an unlock pending state")
}
status.UnlockScript = &fleet.HostScriptResult{}
@ -1129,7 +1095,6 @@ func TestMDMWipeCommand(t *testing.T) {
{appCfgWinMDM, "valid windows but pending mdm enroll", []string{"--host", winPending.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
{appCfgMacMDM, "valid macos but pending mdm enroll", []string{"--host", macPending.UUID}, `Can't wipe the host because it doesn't have MDM turned on.`},
{appCfgAllMDM, "valid windows but pending unlock", []string{"--host", winEnrolledUP.UUID}, "Host has pending unlock request."},
{appCfgAllMDM, "valid macos but pending unlock", []string{"--host", macEnrolledUP.UUID}, "Host has pending unlock request."},
{appCfgAllMDM, "valid windows but pending lock", []string{"--host", winEnrolledLP.UUID}, "Host has pending lock request."},
{appCfgAllMDM, "valid macos but pending lock", []string{"--host", macEnrolledLP.UUID}, "Host has pending lock request."},
{appCfgAllMDM, "valid windows but pending wipe", []string{"--host", winEnrolledWP.UUID}, "Host has pending wipe request."},

View file

@ -11,6 +11,7 @@
"server_settings": {
"server_url": "",
"live_query_disabled": false,
"query_report_cap": 0,
"query_reports_disabled": false,
"enable_analytics": false,
"deferred_save_host": false,

View file

@ -59,6 +59,7 @@ spec:
deferred_save_host: false
enable_analytics: false
live_query_disabled: false
query_report_cap: 0
query_reports_disabled: false
server_url: ""
scripts_disabled: false

View file

@ -11,6 +11,7 @@
"server_settings": {
"server_url": "",
"live_query_disabled": false,
"query_report_cap": 0,
"query_reports_disabled": false,
"enable_analytics": false,
"deferred_save_host": false,

View file

@ -98,6 +98,7 @@ spec:
deferred_save_host: false
enable_analytics: false
live_query_disabled: false
query_report_cap: 0
query_reports_disabled: false
server_url: ""
scripts_disabled: false

View file

@ -101,6 +101,7 @@ org_settings:
deferred_save_host: false
enable_analytics: true
live_query_disabled: false
query_report_cap: 2000
query_reports_disabled: false
scripts_disabled: false
server_url: $FLEET_SERVER_URL

View file

@ -59,6 +59,7 @@ spec:
deferred_save_host: false
enable_analytics: true
live_query_disabled: false
query_report_cap: 0
query_reports_disabled: false
server_url: https://example.org
scripts_disabled: false

View file

@ -59,6 +59,7 @@ spec:
deferred_save_host: false
enable_analytics: true
live_query_disabled: false
query_report_cap: 0
query_reports_disabled: false
server_url: https://example.org
scripts_disabled: false

View file

@ -160,7 +160,7 @@ func createUserCommand() *cli.Command {
force_reset := !sso && !apiOnly
// password requirements are validated as part of `CreateUser`
err = client.CreateUser(fleet.UserPayload{
sessionKey, err := client.CreateUser(fleet.UserPayload{
Password: &password,
Email: &email,
Name: &name,
@ -174,6 +174,10 @@ func createUserCommand() *cli.Command {
return fmt.Errorf("Failed to create user: %w", err)
}
if apiOnly && sessionKey != nil && *sessionKey != "" {
fmt.Fprintf(c.App.Writer, "Success! The API token for your new user is: %s\n", *sessionKey)
}
return nil
},
}
@ -208,7 +212,6 @@ func createBulkUsersCommand() *cli.Command {
}
defer csvFile.Close()
csvLines, err := csv.NewReader(csvFile).ReadAll()
if err != nil {
return err
}
@ -278,7 +281,7 @@ func createBulkUsersCommand() *cli.Command {
}
for _, user := range users {
err = client.CreateUser(user)
_, err = client.CreateUser(user)
if err != nil {
return fmt.Errorf("Failed to create user: %w", err)
}
@ -351,7 +354,6 @@ func deleteBulkUsersCommand() *cli.Command {
}
defer csvFile.Close()
csvLines, err := csv.NewReader(csvFile).ReadAll()
if err != nil {
return err
}
@ -362,10 +364,10 @@ func deleteBulkUsersCommand() *cli.Command {
}
}
return nil
},
}
}
func generateRandomPassword() (string, error) {
password, err := password.Generate(20, 2, 2, false, true)
if err != nil {

View file

@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"encoding/csv"
"fmt"
"math/big"
"os"
"strings"
@ -73,31 +74,57 @@ func TestUserCreateForcePasswordReset(t *testing.T) {
) error {
return nil
}
ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
if email == "bar@example.com" {
apiOnlyUser := &fleet.User{
ID: 1,
Email: email,
}
err := apiOnlyUser.SetPassword(pwd, 24, 10)
require.NoError(t, err)
return apiOnlyUser, nil
}
return nil, &notFoundError{}
}
var apiOnlyUserSessionKey string
ds.NewSessionFunc = func(ctx context.Context, userID uint, sessionKey string) (*fleet.Session, error) {
apiOnlyUserSessionKey = sessionKey
return &fleet.Session{
ID: 2,
UserID: userID,
Key: sessionKey,
}, nil
}
for _, tc := range []struct {
name string
args []string
expectedAdminForcePasswordReset bool
displaysToken bool
}{
{
name: "sso",
args: []string{"--email", "foo@example.com", "--name", "foo", "--sso"},
expectedAdminForcePasswordReset: false,
displaysToken: false,
},
{
name: "api-only",
args: []string{"--email", "bar@example.com", "--password", pwd, "--name", "bar", "--api-only"},
expectedAdminForcePasswordReset: false,
displaysToken: true,
},
{
name: "api-only-sso",
args: []string{"--email", "baz@example.com", "--name", "baz", "--api-only", "--sso"},
expectedAdminForcePasswordReset: false,
displaysToken: false,
},
{
name: "non-sso-non-api-only",
args: []string{"--email", "zoo@example.com", "--password", pwd, "--name", "zoo"},
expectedAdminForcePasswordReset: true,
displaysToken: false,
},
} {
ds.NewUserFuncInvoked = false
@ -106,10 +133,15 @@ func TestUserCreateForcePasswordReset(t *testing.T) {
return user, nil
}
require.Equal(t, "", runAppForTest(t, append(
stdout := runAppForTest(t, append(
[]string{"user", "create"},
tc.args...,
)))
))
if tc.displaysToken {
require.Equal(t, stdout, fmt.Sprintf("Success! The API token for your new user is: %s\n", apiOnlyUserSessionKey))
} else {
require.Empty(t, stdout)
}
require.True(t, ds.NewUserFuncInvoked)
}
}

View file

@ -38,22 +38,22 @@ func (svc *Service) OSVersion(ctx context.Context, osID uint, teamID *uint, incl
return svc.Service.OSVersion(ctx, osID, teamID, true)
}
func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
func (svc *Service) LockHost(ctx context.Context, hostID uint) (unlockPIN string, err error) {
// First ensure the user has access to list hosts, then check the specific
// host once team_id is loaded.
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return err
return "", err
}
host, err := svc.ds.HostLite(ctx, hostID)
if err != nil {
return ctxerr.Wrap(ctx, err, "get host lite")
return "", ctxerr.Wrap(ctx, err, "get host lite")
}
// Authorize again with team loaded now that we have the host's team_id.
// Authorize as "execute mdm_command", which is the correct access
// requirement and is what happens for macOS platforms.
if err := svc.authz.Authorize(ctx, fleet.MDMCommandAuthz{TeamID: host.TeamID}, fleet.ActionWrite); err != nil {
return err
return "", err
}
// locking validations are based on the platform of the host
@ -65,19 +65,23 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
if errors.Is(err, fleet.ErrMDMNotConfigured) {
err = fleet.NewInvalidArgumentError("host_id", fleet.AppleMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
}
return ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
return "", ctxerr.Wrap(ctx, err, "check macOS MDM enabled")
}
// on macOS, the lock command requires the host to be MDM-enrolled in Fleet
hostMDM, err := svc.ds.GetHostMDM(ctx, host.ID)
if err != nil {
if fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."))
return "", ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."),
)
}
return ctxerr.Wrap(ctx, err, "get host MDM information")
return "", ctxerr.Wrap(ctx, err, "get host MDM information")
}
if !hostMDM.IsFleetEnrolled() {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."))
return "", ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock the host because it doesn't have MDM turned on."),
)
}
case "windows", "linux":
@ -86,27 +90,30 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
if errors.Is(err, fleet.ErrMDMNotConfigured) {
err = fleet.NewInvalidArgumentError("host_id", fleet.WindowsMDMNotConfiguredMessage).WithStatus(http.StatusBadRequest)
}
return ctxerr.Wrap(ctx, err, "check windows MDM enabled")
return "", ctxerr.Wrap(ctx, err, "check windows MDM enabled")
}
}
// on windows and linux, a script is used to lock the host so scripts must
// be enabled
appCfg, err := svc.ds.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "get app config")
return "", ctxerr.Wrap(ctx, err, "get app config")
}
if appCfg.ServerSettings.ScriptsDisabled {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings."))
return "", ctxerr.Wrap(
ctx,
fleet.NewInvalidArgumentError("host_id", "Can't lock host because running scripts is disabled in organization settings."),
)
}
hostOrbitInfo, err := svc.ds.GetHostOrbitInfo(ctx, host.ID)
switch {
case err != nil:
// If not found, then do nothing. We do not know if this host has scripts enabled or not
if !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "get host orbit info")
return "", ctxerr.Wrap(ctx, err, "get host orbit info")
}
case hostOrbitInfo.ScriptsEnabled != nil && !*hostOrbitInfo.ScriptsEnabled:
return ctxerr.Wrap(
return "", ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError(
"host_id", "Couldn't lock host. To lock, deploy the fleetd agent with --enable-scripts and refetch host vitals.",
),
@ -114,26 +121,37 @@ func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
}
default:
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", fmt.Sprintf("Unsupported host platform: %s", host.Platform)))
}
// if there's a lock, unlock or wipe action pending, do not accept the lock
// request.
lockWipe, err := svc.ds.GetHostLockWipeStatus(ctx, host)
if err != nil {
return ctxerr.Wrap(ctx, err, "get host lock/wipe status")
return "", ctxerr.Wrap(ctx, err, "get host lock/wipe status")
}
switch {
case lockWipe.IsPendingLock():
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. The host will lock when it comes online."))
return "", ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending lock request. The host will lock when it comes online."),
)
case lockWipe.IsPendingUnlock():
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete."))
return "", ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError(
"host_id", "Host has pending unlock request. Host cannot be locked again until unlock is complete.",
),
)
case lockWipe.IsPendingWipe():
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped."))
return "", ctxerr.Wrap(
ctx,
fleet.NewInvalidArgumentError("host_id", "Host has pending wipe request. Cannot process lock requests once host is wiped."),
)
case lockWipe.IsWiped():
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped."))
return "", ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError("host_id", "Host is wiped. Cannot process lock requests once host is wiped."),
)
case lockWipe.IsLocked():
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict))
return "", ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("host_id", "Host is already locked.").WithStatus(http.StatusConflict))
}
// all good, go ahead with queuing the lock request.
@ -333,19 +351,21 @@ func (svc *Service) WipeHost(ctx context.Context, hostID uint) error {
return svc.enqueueWipeHostRequest(ctx, host, lockWipe)
}
func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) error {
func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) (
unlockPIN string, err error,
) {
vc, ok := viewer.FromContext(ctx)
if !ok {
return fleet.ErrNoContext
return "", fleet.ErrNoContext
}
if lockStatus.HostFleetPlatform == "darwin" {
lockCommandUUID := uuid.NewString()
if err := svc.mdmAppleCommander.DeviceLock(ctx, host, lockCommandUUID); err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing lock request for darwin")
if unlockPIN, err = svc.mdmAppleCommander.DeviceLock(ctx, host, lockCommandUUID); err != nil {
return "", ctxerr.Wrap(ctx, err, "enqueuing lock request for darwin")
}
if err := svc.NewActivity(
if err = svc.NewActivity(
ctx,
vc.User,
fleet.ActivityTypeLockedHost{
@ -353,10 +373,10 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host
HostDisplayName: host.DisplayName(),
},
); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for darwin lock host request")
return "", ctxerr.Wrap(ctx, err, "create activity for darwin lock host request")
}
return nil
return unlockPIN, nil
}
script := windowsLockScript
@ -376,7 +396,7 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host
UserID: &vc.User.ID,
SyncRequest: false,
}, host.FleetPlatform()); err != nil {
return err
return "", err
}
if err := svc.NewActivity(
@ -387,10 +407,10 @@ func (svc *Service) enqueueLockHostRequest(ctx context.Context, host *fleet.Host
HostDisplayName: host.DisplayName(),
},
); err != nil {
return ctxerr.Wrap(ctx, err, "create activity for lock host request")
return "", ctxerr.Wrap(ctx, err, "create activity for lock host request")
}
return nil
return "", nil
}
func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Host, lockStatus *fleet.HostLockWipeStatus) (string, error) {
@ -401,7 +421,9 @@ func (svc *Service) enqueueUnlockHostRequest(ctx context.Context, host *fleet.Ho
var unlockPIN string
if lockStatus.HostFleetPlatform == "darwin" {
// record the unlock request if it was not already recorded
// Record the unlock request time if it was not already recorded.
// It should be always recorded, since the UnlockRequestedAt time is created after the lock command is acknowledged.
// This code is left here to catch potential issues.
if lockStatus.UnlockRequestedAt.IsZero() {
if err := svc.ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC()); err != nil {
return "", err

View file

@ -135,7 +135,7 @@ func (svc *Service) MDMAppleDeviceLock(ctx context.Context, hostID uint) error {
return err
}
err = svc.mdmAppleCommander.DeviceLock(ctx, host, uuid.New().String())
_, err = svc.mdmAppleCommander.DeviceLock(ctx, host, uuid.New().String())
if err != nil {
return err
}

View file

@ -320,6 +320,7 @@ const DEFAULT_QUERY_REPORT_MOCK: IQueryReport = {
},
},
],
report_clipped: false,
};
const createMockQueryReport = (

View file

@ -9,4 +9,5 @@ export interface IQueryReportResultRow {
export interface IQueryReport {
query_id: number;
results: IQueryReportResultRow[];
report_clipped: boolean;
}

View file

@ -2,16 +2,27 @@ import React from "react";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
import { IDeviceUserResponse } from "interfaces/host";
interface IAutoEnrollMdmModalProps {
host: IDeviceUserResponse["host"];
onCancel: () => void;
}
const baseClass = "auto-enroll-mdm-modal";
const AutoEnrollMdmModal = ({
host: { platform, os_version },
onCancel,
}: IAutoEnrollMdmModalProps): JSX.Element => {
let isMacOsSonomaOrLater = false;
if (platform === "darwin" && os_version.startsWith("macOS ")) {
const [major] = os_version
.replace("macOS ", "")
.split(".")
.map((s) => parseInt(s, 10));
isMacOsSonomaOrLater = major >= 14;
}
return (
<Modal
title="Turn on MDM"
@ -25,13 +36,21 @@ const AutoEnrollMdmModal = ({
</p>
<ol>
<li>
Open your Macs notification center by selecting the date and time
in the top right corner of your screen.
From the Apple menu in the top left corner of your screen, select{" "}
<b>System Settings</b> or <b>System Preferences</b>.
</li>
<li>
Select the <b>Device Enrollment</b> notification. This will open{" "}
<b>System Settings</b> or <b>System Preferences</b>. Select{" "}
<b>Allow</b>.
{isMacOsSonomaOrLater ? (
<>
In the sidebar menu, select <b>Enroll in Remote Management</b>,
and select <b>Enroll</b>.
</>
) : (
<>
In the search bar, type Profiles. Select <b>Profiles</b>, find
and select <b>Enrollment Profile</b>, and select <b>Install</b>.
</>
)}
</li>
<li>
Enter your password, and select <b>Enroll</b>.

View file

@ -311,7 +311,7 @@ const DeviceUserPage = ({
const renderEnrollMdmModal = () => {
return host?.dep_assigned_to_fleet ? (
<AutoEnrollMdmModal onCancel={toggleEnrollMdmModal} />
<AutoEnrollMdmModal host={host} onCancel={toggleEnrollMdmModal} />
) : (
<ManualEnrollMdmModal
onCancel={toggleEnrollMdmModal}

View file

@ -42,7 +42,6 @@ import NoResults from "../components/NoResults/NoResults";
import {
DEFAULT_SORT_HEADER,
DEFAULT_SORT_DIRECTION,
QUERY_REPORT_RESULTS_LIMIT,
} from "./QueryDetailsPageConfig";
interface IQueryDetailsPageProps {
@ -199,8 +198,7 @@ const QueryDetailsPage = ({
const isLoading = isStoredQueryLoading || isQueryReportLoading;
const isApiError = storedQueryError || queryReportError;
const isClipped =
(queryReport?.results?.length ?? 0) >= QUERY_REPORT_RESULTS_LIMIT;
const isClipped = queryReport?.report_clipped;
const disabledLiveQuery = config?.server_settings.live_query_disabled;
const renderHeader = () => {

View file

@ -11,5 +11,3 @@ export type QueryDetailsPageQueryParams = Record<
export const DEFAULT_SORT_HEADER = "host_name";
export const DEFAULT_SORT_DIRECTION = "asc";
export const QUERY_REPORT_RESULTS_LIMIT = 1000;

View file

@ -24,6 +24,7 @@ describe("QueryReport", () => {
columns: { col1: "value3", col2: "value4" },
},
],
report_clipped: false,
},
];
render(<QueryReport {...{ isClipped, queryReport }} />);
@ -56,6 +57,7 @@ describe("QueryReport", () => {
},
},
],
report_clipped: false,
},
];
render(<QueryReport {...{ isClipped, queryReport }} />);
@ -83,6 +85,7 @@ describe("QueryReport", () => {
columns: { col1: "value1", col2: "value2" },
},
],
report_clipped: true,
},
];
render(<QueryReport {...{ isClipped, queryReport }} />);

View file

@ -247,6 +247,6 @@ func parseEnrollmentProfileValue(line []byte, key string) (string, bool) {
// showEnrollmentProfileCmd is declared as a variable so it can be overwritten by tests.
var showEnrollmentProfileCmd = func() ([]byte, error) {
cmd := exec.Command("/usr/bin/profiles", "show", "-type", "enrollment")
cmd := exec.Command("sh", "-c", `launchctl asuser $(id -u $(stat -f "%u" /dev/console)) profiles show -type enrollment`)
return cmd.Output()
}

View file

@ -3,5 +3,6 @@
package update
func runRenewEnrollmentProfile() error {
return runCmdCollectErr("/usr/bin/profiles", "renew", "--type", "enrollment")
cmd := `launchctl asuser $(id -u $(stat -f "%u" /dev/console)) profiles renew -type enrollment`
return runCmdCollectErr("sh", "-c", cmd)
}

View file

@ -104,14 +104,15 @@ func (h *renewEnrollmentProfileConfigReceiver) Run(config *fleet.OrbitConfig) er
fn = runRenewEnrollmentProfile
}
if err := fn(); err != nil {
// TODO: Look into whether we should increment lastRun here or implement a
// backoff to avoid unnecessary user notification popups and mitigate rate
// limiting by Apple.
log.Info().Err(err).Msg("calling /usr/bin/profiles to renew enrollment profile failed")
} else {
h.lastRun = time.Now()
log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
// TODO: Design a better way to backoff `profiles show` so that the device doesn't get rate
// limited by Apple. For now, wait at least 2 minutes before retrying.
h.lastRun = time.Now().Add(-h.Frequency).Add(2 * time.Minute)
return nil
}
h.lastRun = time.Now()
log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
} else {
log.Debug().Msg("skipped calling /usr/bin/profiles to renew enrollment profile, last run was too recent")
}

View file

@ -8,6 +8,8 @@ import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"text/template"
"time"
@ -48,7 +50,7 @@ const mdmUnenrollmentTotalWaitTime = 90 * time.Second
// between unenrollment checks.
const defaultUnenrollmentRetryInterval = 5 * time.Second
var mdmMigrationTemplate = template.Must(template.New("mdmMigrationTemplate").Parse(`
var mdmMigrationTemplatePreSonoma = template.Must(template.New("mdmMigrationTemplate").Parse(`
## Migrate to Fleet
Select **Start** and look for this notification in your notification center:` +
@ -56,6 +58,14 @@ Select **Start** and look for this notification in your notification center:` +
"After you start, this window will popup every 15-20 minutes until you finish.",
))
var mdmMigrationTemplate = template.Must(template.New("mdmMigrationTemplate").Parse(`
## Migrate to Fleet
Select **Start** and Remote Management window will appear soon:` +
"\n\n![Image showing MDM migration notification](https://fleetdm.com/images/permanent/mdm-migration-sonoma-1500x938.png)\n\n" +
"After you start, this window will popup every 15-20 minutes until you finish.",
))
var errorTemplate = template.Must(template.New("").Parse(`
### Something's gone wrong.
@ -291,29 +301,9 @@ func (m *swiftDialogMDMMigrator) waitForUnenrollment() error {
}
func (m *swiftDialogMDMMigrator) renderMigration() error {
var message bytes.Buffer
if err := mdmMigrationTemplate.Execute(
&message,
m.props,
); err != nil {
return fmt.Errorf("execute template: %w", err)
}
flags := []string{
// main button
"--button1text", "Start",
// secondary button
"--button2text", "Later",
"--height", "440",
}
if m.props.OrgInfo.ContactURL != "" {
flags = append(flags,
// info button
"--infobuttontext", "Unsure? Contact IT",
"--infobuttonaction", m.props.OrgInfo.ContactURL,
"--quitoninfo",
)
message, flags, err := m.getMessageAndFlags()
if err != nil {
return fmt.Errorf("getting mdm migrator message: %w", err)
}
exitCodeCh, errCh := m.render(message.String(), flags...)
@ -419,3 +409,71 @@ func (m *swiftDialogMDMMigrator) ShowInterval() error {
func (m *swiftDialogMDMMigrator) SetProps(props MDMMigratorProps) {
m.props = props
}
func (m *swiftDialogMDMMigrator) getMessageAndFlags() (*bytes.Buffer, []string, error) {
vers, err := m.getMacOSMajorVersion()
if err != nil {
// log error for debugging and continue with default template
log.Error().Err(err).Msg("getting macOS major version failed: using default migration template")
}
tmpl := mdmMigrationTemplate
height := "669"
if vers != 0 && vers < 14 {
height = "440"
tmpl = mdmMigrationTemplatePreSonoma
}
var message bytes.Buffer
if err := tmpl.Execute(
&message,
m.props,
); err != nil {
return nil, nil, fmt.Errorf("executing migrqation template: %w", err)
}
flags := []string{
// main button
"--button1text", "Start",
// secondary button
"--button2text", "Later",
"--height", height,
}
if m.props.OrgInfo.ContactURL != "" {
flags = append(flags,
// info button
"--infobuttontext", "Unsure? Contact IT",
"--infobuttonaction", m.props.OrgInfo.ContactURL,
"--quitoninfo",
)
}
return &message, flags, nil
}
// TODO: make this a variable for testing
func (m *swiftDialogMDMMigrator) getMacOSMajorVersion() (int, error) {
cmd := exec.Command("sw_vers", "-productVersion")
out, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("getting macOS version: %w", err)
}
parts := strings.SplitN(string(out), ".", 2)
switch len(parts) {
case 0:
// this should never happen
return 0, errors.New("getting macOS version: sw_vers command returned no output")
case 1:
// unexpected, so log for debugging
log.Debug().Msgf("parsing macOS version: expected 2 parts, got 1: %s", out)
default:
// ok
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return 0, fmt.Errorf("parsing macOS major version: %w", err)
}
return major, nil
}

View file

@ -137,6 +137,7 @@ func TestValidGitOpsYaml(t *testing.T) {
serverSettings, ok := gitops.OrgSettings["server_settings"]
assert.True(t, ok, "server_settings not found")
assert.Equal(t, "https://fleet.example.com", serverSettings.(map[string]interface{})["server_url"])
assert.EqualValues(t, 2000, serverSettings.(map[string]interface{})["query_report_cap"])
assert.Contains(t, gitops.OrgSettings, "org_info")
orgInfo, ok := gitops.OrgSettings["org_info"].(map[string]interface{})
assert.True(t, ok)

View file

@ -101,6 +101,7 @@ org_settings:
deferred_save_host: false
enable_analytics: true
live_query_disabled: false
query_report_cap: 2000
query_reports_disabled: false
scripts_disabled: false
server_url: https://fleet.example.com

View file

@ -4,6 +4,7 @@ server_settings:
deferred_save_host: false
enable_analytics: true
live_query_disabled: false
query_report_cap: 2000
query_reports_disabled: false
scripts_disabled: false
server_url: https://fleet.example.com

23
schema/tables/patches.yml Normal file
View file

@ -0,0 +1,23 @@
name: patches
description: |- # (required) string - The description for this table. Note: this field supports Markdown
The `patches` osquery table lists Windows security patch updates.
examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
Basic query:
```
SELECT * FROM patches;
```
This query determines if a specific hotfix patch is installed:
```
SELECT * FROM patches WHERE hotfix_id='kb5037663';
```
notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
Microsoft creates a support page per hotfix patch. Support pages can be discovered by doing a web browser search for the hotfix ID string (e.g., KB5037663).
Microsoft documentation for [KB5037663](https://support.microsoft.com/en-us/topic/may-29-2024-kb5037853-os-builds-22621-3672-and-22631-3672-preview-dcf14fd8-84d6-4234-9d5b-784c319cd7cf)
The `patches` table does not include updates that are applied via Windows Installer / Microsoft Standard Installer packages (.msi) or updates downloaded directly from Windows Update (e.g., Service Packs).
[Windows Installer](https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal)

View file

@ -24,6 +24,7 @@ notes: |- # (optional) string - Notes about this table. Note: This field support
- [Windows Installer](https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal)
- [Chocolatey](https://chocolatey.org/)
- The Fleet `chocolatey_packages`[table](https://fleetdm.com/tables/chocolatey_packages)
- [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/)
- [winget.run](https://winget.run/)
- Windows [cmd](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd)

View file

@ -1,11 +1,25 @@
name: safari_extensions
description: Installed Safari browser extensions (plugins).
description: Safari extensions add functionality to Safari.app, the native web browser in macOS. The `safari_extensions` table collects all Safari extensions installed on a Mac.
columns:
- name: uid
examples: |-
Collect Safari extensions for all Mac users:
```
SELECT * FROM users CROSS JOIN safari_extensions USING (uid);
```
notes: |-
- Querying this table requires joining against the `users` table. [Learn more](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
- Includes installed extensions for all system users.
Because Safari data is intentionally isolated for each macOS user to maintain privacy, this query requires a `JOIN` operation.
Query explanation:
- The `safari_extensions` table has a row for each installed extension
- Each row has a column with the `uid` of the user who installed the extension
- Each `uid` from the `safari_extensions` table is matched in the `users` table to collect Safari extensions in the output data for all user accounts on the Mac by the `JOIN`
Links:
- Apple dcoumentaion on Safari Extensions: https://support.apple.com/en-us/102343
- CROSS JOIN SQLite tutorial: https://www.sqlitetutorial.net/sqlite-cross-join/
- [Fleet documentation on joining against the `users` table](https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table)
- Fleet users table: https://fleetdm.com/tables/users

View file

@ -0,0 +1,13 @@
name: scheduled_tasks
description: |- # (required) string - The description for this table. Note: this field supports Markdown
The Windows Task Scheduler tracks and performs automated tasks on a Windows device. The `scheduled_tasks` table collects the data from the Windows Task Scheduler.
examples: |- # (optional) string - An example query for this table. Note: This field supports Markdown
This query collects all tasks that are enabled but have not run:
```
SELECT * FROM scheduled_tasks WHERE enabled='1' AND last_run_message='The task has not yet run.';
```
notes: |- # (optional) string - Notes about this table. Note: This field supports Markdown.
Many automated tasks are added to the Task Scheduler by Windows itself, however, administrators can also customize the Task Scheduler. Scheduled tasks are analogous to Launch Daemons and Launch Agents used on Linux or macOS. Because automation is a potential vector for malicious activity, monitoring the Windows Task Scheduler may be critical in an enterprise environment.
[Windows Task Scheduler](https://learn.microsoft.com/en-us/windows/win32/taskschd/about-the-task-scheduler)

View file

@ -1,12 +1,29 @@
name: software_update
description: The `software_update` table displays the number of updates available from Apple's Software Update service on a Mac.
platforms:
- darwin
description: Information about available Apple software updates.
examples: |-
Basic query:
```
SELECT * FROM software_update;
```
columns:
- name: software_update_required
type: integer
required: false
description: |-
If true, means one of the Apple softwares installed on this machine has a new available upgrade.
notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
A value of 0 means no updates are available. Any other integer represents the number of updates available.
notes: |-
This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
Available updates on a Mac can be displayed in the macOS Graphical User Interface (GUI) by clicking on the Apple menu and then selecting “System Settings”. In the System Settings.app, click General > Software Update.
Apple Software Updates can also be listed in Terminal with the following command:
```
softwareupdate --list --verbose
```
[Update Your Apple Software](https://support.apple.com/guide/personal-safety/update-your-apple-software-ips4930e3486/web)
evented: false

View file

@ -4603,14 +4603,14 @@ func testLockUnlockWipeMacOS(t *testing.T, ds *Datastore) {
require.NoError(t, err)
checkLockWipeState(t, status, false, true, false, false, false, false)
// request an unlock, to make it pending unlock
// request an unlock. This is a NOOP for Apple MDM.
err = ds.UnlockHostManually(ctx, host.ID, host.FleetPlatform(), time.Now().UTC())
require.NoError(t, err)
// it is now locked pending unlock
// it is still locked
status, err = ds.GetHostLockWipeStatus(ctx, host)
require.NoError(t, err)
checkLockWipeState(t, status, false, true, false, true, false, false)
checkLockWipeState(t, status, false, true, false, false, false, false)
// execute CleanMacOSMDMLock to simulate successful unlock
err = ds.CleanMacOSMDMLock(ctx, host.UUID)

View file

@ -4255,7 +4255,7 @@ func testHostsIncludesScheduledQueriesInPackStats(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage(json.RawMessage(`{"foo": "baz"}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), queryResultRow)
err = ds.OverwriteQueryResultRows(context.Background(), queryResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
hostResult, err = ds.Host(context.Background(), host.ID)
@ -9067,7 +9067,7 @@ func testHostsAddToTeamCleansUpTeamQueryResults(t *testing.T, ds *Datastore) {
h4Global0Results,
h4Query1Results,
} {
err = ds.OverwriteQueryResultRows(ctx, results)
err = ds.OverwriteQueryResultRows(ctx, results, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
}

View file

@ -6122,7 +6122,7 @@ func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) {
cert := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: "FleetDM Identity",
CommonName: "Fleet Identity",
},
NotAfter: notAfter,
// use a random value, just to make sure they're

View file

@ -359,7 +359,8 @@ ON DUPLICATE KEY UPDATE
// if we received a Wipe command result, update the host's status
if wipeCmdUUID != "" {
if err := updateHostLockWipeStatusFromResultAndHostUUID(ctx, tx, enrollment.HostUUID,
"wipe_ref", wipeCmdUUID, strings.HasPrefix(wipeCmdStatus, "2")); err != nil {
"wipe_ref", wipeCmdUUID, strings.HasPrefix(wipeCmdStatus, "2"), false,
); err != nil {
return ctxerr.Wrap(ctx, err, "updating wipe command result in host_mdm_actions")
}
}

View file

@ -13,7 +13,7 @@ import (
// OverwriteQueryResultRows overwrites the query result rows for a given query and host
// in a single transaction, ensuring that the number of rows for the given query
// does not exceed the maximum allowed
func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) (err error) {
func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) (err error) {
if len(rows) == 0 {
return nil
}
@ -31,7 +31,7 @@ func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet
return ctxerr.Wrap(ctx, err, "counting existing query results")
}
if countExisting >= fleet.MaxQueryReportRows {
if countExisting >= maxQueryReportRows {
// do not delete any rows if we are already at the limit
return nil
}
@ -53,7 +53,7 @@ func (ds *Datastore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet
// Calculate how many new rows can be added given the maximum limit
netRowsAfterDeletion := countExisting - int(countDeleted)
allowedNewRows := fleet.MaxQueryReportRows - netRowsAfterDeletion
allowedNewRows := maxQueryReportRows - netRowsAfterDeletion
if allowedNewRows == 0 {
return nil
}

View file

@ -62,7 +62,7 @@ func testGetQueryResultRows(t *testing.T, ds *Datastore) {
}`)),
},
}
err := ds.OverwriteQueryResultRows(context.Background(), query1Rows)
err := ds.OverwriteQueryResultRows(context.Background(), query1Rows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Insert Result Row for different Scheduled Query
@ -76,7 +76,7 @@ func testGetQueryResultRows(t *testing.T, ds *Datastore) {
},
}
err = ds.OverwriteQueryResultRows(context.Background(), query2Rows)
err = ds.OverwriteQueryResultRows(context.Background(), query2Rows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
results, err := ds.QueryResultRows(context.Background(), query.ID, fleet.TeamFilter{User: test.UserAdmin})
@ -125,7 +125,7 @@ func testGetQueryResultRowsForHost(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
}
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows)
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Insert 1 Result Row for Query1 Host2
@ -137,7 +137,7 @@ func testGetQueryResultRowsForHost(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRows)
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that Query1 returns 2 results for Host1
@ -215,7 +215,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
},
}
err = ds.OverwriteQueryResultRows(context.Background(), globalRow)
err = ds.OverwriteQueryResultRows(context.Background(), globalRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
teamRow := []*fleet.ScheduledQueryResultRow{
@ -229,7 +229,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), teamRow)
err = ds.OverwriteQueryResultRows(context.Background(), teamRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
observerTeamRow := []*fleet.ScheduledQueryResultRow{
@ -243,7 +243,7 @@ func testQueryResultRowsTeamFilter(t *testing.T, ds *Datastore) {
}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), observerTeamRow)
err = ds.OverwriteQueryResultRows(context.Background(), observerTeamRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
filter := fleet.TeamFilter{
@ -286,7 +286,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
}`)),
},
}
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRow)
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Insert Nil Result Row for Query1, nil data rows are not counted
@ -298,7 +298,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
Data: nil,
},
}
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow)
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Insert 5 Result Rows for Query2
@ -317,7 +317,7 @@ func testCountResultsForQuery(t *testing.T, ds *Datastore) {
resultRows = append(resultRows, resultRow2)
}
err = ds.OverwriteQueryResultRows(context.Background(), resultRows)
err = ds.OverwriteQueryResultRows(context.Background(), resultRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that ResultCountForQuery returns 1
@ -366,7 +366,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
}`)),
},
}
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows)
err := ds.OverwriteQueryResultRows(context.Background(), host1ResultRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
host1Query2 := []*fleet.ScheduledQueryResultRow{
@ -380,7 +380,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), host1Query2)
err = ds.OverwriteQueryResultRows(context.Background(), host1Query2, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
host2ResultRow := []*fleet.ScheduledQueryResultRow{
@ -394,7 +394,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow)
err = ds.OverwriteQueryResultRows(context.Background(), host2ResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
host3ResultRow := []*fleet.ScheduledQueryResultRow{
@ -405,7 +405,7 @@ func testCountResultsForQueryAndHost(t *testing.T, ds *Datastore) {
Data: nil,
},
}
err = ds.OverwriteQueryResultRows(context.Background(), host3ResultRow)
err = ds.OverwriteQueryResultRows(context.Background(), host3ResultRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that Query1 returns 2
@ -451,7 +451,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
},
}
err := ds.OverwriteQueryResultRows(context.Background(), initialRow)
err := ds.OverwriteQueryResultRows(context.Background(), initialRow, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Overwrite Result Rows with new data
@ -465,7 +465,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
},
}
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that we get the overwritten data (1 result with USB Mouse data)
@ -486,7 +486,7 @@ func testOverwriteQueryResultRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that the data has not changed
@ -511,7 +511,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
mockTime := time.Now().UTC().Truncate(time.Second)
// Generate max rows -1
maxRows := fleet.MaxQueryReportRows - 1
maxRows := fleet.DefaultMaxQueryReportRows - 1
maxMinusOneRows := make([]*fleet.ScheduledQueryResultRow, maxRows)
for i := 0; i < maxRows; i++ {
maxMinusOneRows[i] = &fleet.ScheduledQueryResultRow{
@ -521,7 +521,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
}
}
err := ds.OverwriteQueryResultRows(context.Background(), maxMinusOneRows)
err := ds.OverwriteQueryResultRows(context.Background(), maxMinusOneRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Add an empty data rows which do not count towards the max
@ -532,7 +532,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
LastFetched: mockTime,
Data: nil,
},
})
}, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Confirm that we can still add a row
@ -543,13 +543,13 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
LastFetched: mockTime,
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
})
}, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that we now have max rows
count, err := ds.ResultCountForQuery(context.Background(), query.ID)
require.NoError(t, err)
require.Equal(t, fleet.MaxQueryReportRows, count)
require.Equal(t, fleet.DefaultMaxQueryReportRows, count)
// Attempt to add another row
err = ds.OverwriteQueryResultRows(context.Background(), []*fleet.ScheduledQueryResultRow{
@ -559,7 +559,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
LastFetched: mockTime,
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
})
}, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Assert that the last row was not added
@ -568,7 +568,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
require.Len(t, host4result, 0)
// Generate more than max rows in Query 2
rows := fleet.MaxQueryReportRows + 50
rows := fleet.DefaultMaxQueryReportRows + 50
largeBatchRows := make([]*fleet.ScheduledQueryResultRow, rows)
for i := 0; i < rows; i++ {
largeBatchRows[i] = &fleet.ScheduledQueryResultRow{
@ -578,13 +578,13 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
}
}
err = ds.OverwriteQueryResultRows(context.Background(), largeBatchRows)
err = ds.OverwriteQueryResultRows(context.Background(), largeBatchRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Confirm only max rows are stored for the queryID
allResults, err := ds.QueryResultRowsForHost(context.Background(), query2.ID, host1.ID)
require.NoError(t, err)
require.Len(t, allResults, fleet.MaxQueryReportRows)
require.Len(t, allResults, fleet.DefaultMaxQueryReportRows)
// Confirm that new rows are not added when the max is reached
newMockTime := mockTime.Add(2 * time.Minute)
@ -597,7 +597,7 @@ func testQueryResultRowsDoNotExceedMaxRows(t *testing.T, ds *Datastore) {
},
}
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
host2Results, err := ds.QueryResultRowsForHost(context.Background(), query2.ID, host2.ID)
@ -619,7 +619,7 @@ func testQueryResultRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "USB Mouse", "vendor": "Logitech"}`)),
},
}
err := ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
err := ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
filter := fleet.TeamFilter{User: user, IncludeObserver: true}
@ -655,7 +655,7 @@ func testCleanupQueryResultRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "Keyboard", "vendor": "Microsoft"}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), rows)
err = ds.OverwriteQueryResultRows(context.Background(), rows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Call OverwriteQueryResultRows again with different rows
@ -673,7 +673,7 @@ func testCleanupQueryResultRows(t *testing.T, ds *Datastore) {
Data: ptr.RawMessage([]byte(`{"model": "Speakers", "vendor": "Bose"}`)),
},
}
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows)
err = ds.OverwriteQueryResultRows(context.Background(), overwriteRows, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
// Cleanup query result rows

View file

@ -39,7 +39,7 @@ func TestAppleMDMSCEPSerial(t *testing.T) {
func TestAppleMDMPutAndHasCN(t *testing.T) {
depot := setup(t)
name := "FleetDM Identity"
name := "Fleet Identity"
serial, err := depot.Serial()
require.NoError(t, err)
cert := x509.Certificate{

View file

@ -41,7 +41,7 @@ CREATE TABLE `app_config_json` (
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"macos_setup\": {\"bootstrap_package\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_default_team\": \"\", \"apple_bm_terms_expired\": false, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"google_calendar\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `calendar_events` (

View file

@ -1023,7 +1023,7 @@ func (ds *Datastore) UnlockHostManually(ctx context.Context, hostID uint, hostFl
return ctxerr.Wrap(ctx, err, "record manual unlock host request")
}
func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart string) string {
func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart string, setUnlockRef bool) string {
var alias string
stmt := `UPDATE host_mdm_actions `
@ -1039,7 +1039,14 @@ func buildHostLockWipeStatusUpdateStmt(refCol string, succeeded bool, joinPart s
// Note that this must not clear the unlock_pin, because recording the
// lock request does generate the PIN and store it there to be used by an
// eventual unlock.
stmt += fmt.Sprintf("%sunlock_ref = NULL, %[1]swipe_ref = NULL", alias)
if !setUnlockRef {
stmt += fmt.Sprintf("%sunlock_ref = NULL, %[1]swipe_ref = NULL", alias)
} else {
// Currently only used for Apple MDM devices.
// We set the unlock_ref to current time since the device can be unlocked any time after the lock.
// Apple MDM does not have a concept of unlock pending.
stmt += fmt.Sprintf("%sunlock_ref = '%s', %[1]swipe_ref = NULL", alias, time.Now().Format(time.DateTime))
}
case "unlock_ref":
// a successful unlock clears itself as well as the lock ref, because
// unlock is the default state so we don't need to keep its unlock_ref
@ -1061,26 +1068,30 @@ func (ds *Datastore) UpdateHostLockWipeStatusFromAppleMDMResult(ctx context.Cont
// a bit of MDM protocol leaking in the mysql layer, but it's either that or
// the other way around (MDM protocol would translate to database column)
var refCol string
var setUnlockRef bool
switch requestType {
case "EraseDevice":
refCol = "wipe_ref"
case "DeviceLock":
refCol = "lock_ref"
setUnlockRef = true
default:
return nil
}
return updateHostLockWipeStatusFromResultAndHostUUID(ctx, ds.writer(ctx), hostUUID, refCol, cmdUUID, succeeded)
return updateHostLockWipeStatusFromResultAndHostUUID(ctx, ds.writer(ctx), hostUUID, refCol, cmdUUID, succeeded, setUnlockRef)
}
func updateHostLockWipeStatusFromResultAndHostUUID(ctx context.Context, tx sqlx.ExtContext, hostUUID, refCol, cmdUUID string, succeeded bool) error {
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, `JOIN hosts h ON hma.host_id = h.id`)
func updateHostLockWipeStatusFromResultAndHostUUID(
ctx context.Context, tx sqlx.ExtContext, hostUUID, refCol, cmdUUID string, succeeded bool, setUnlockRef bool,
) error {
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, `JOIN hosts h ON hma.host_id = h.id`, setUnlockRef)
stmt += ` WHERE h.uuid = ? AND hma.` + refCol + ` = ?`
_, err := tx.ExecContext(ctx, stmt, hostUUID, cmdUUID)
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result via host uuid")
}
func updateHostLockWipeStatusFromResult(ctx context.Context, tx sqlx.ExtContext, hostID uint, refCol string, succeeded bool) error {
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, "")
stmt := buildHostLockWipeStatusUpdateStmt(refCol, succeeded, "", false)
stmt += ` WHERE host_id = ?`
_, err := tx.ExecContext(ctx, stmt, hostID)
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result")

View file

@ -15,6 +15,7 @@ import (
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -764,6 +765,7 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Equal(t, "windows", status.HostFleetPlatform)
require.NotNil(t, status.LockScript)
assert.Nil(t, status.UnlockScript)
s := status.LockScript
require.Equal(t, script, s.ScriptContents)

View file

@ -24,47 +24,47 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
lic, _ := license.FromContext(ctx)
computeStats := func(stats *fleet.StatisticsPayload, since time.Time) error {
enrolledHostsByOS, amountEnrolledHosts, err := amountEnrolledHostsByOSDB(ctx, ds.writer(ctx))
enrolledHostsByOS, amountEnrolledHosts, err := amountEnrolledHostsByOSDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount enrolled hosts by os")
}
amountUsers, err := tableRowsCount(ctx, ds.writer(ctx), "users")
amountUsers, err := tableRowsCount(ctx, ds.reader(ctx), "users")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount users")
}
amountSoftwaresVersions, err := tableRowsCount(ctx, ds.writer(ctx), "software")
amountSoftwaresVersions, err := tableRowsCount(ctx, ds.reader(ctx), "software")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software")
}
amountHostSoftwares, err := tableRowsCount(ctx, ds.writer(ctx), "host_software")
amountHostSoftwares, err := tableRowsCount(ctx, ds.reader(ctx), "host_software")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount host_software")
}
amountSoftwareTitles, err := tableRowsCount(ctx, ds.writer(ctx), "software_titles")
amountSoftwareTitles, err := tableRowsCount(ctx, ds.reader(ctx), "software_titles")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_titles")
}
amountHostSoftwareInstalledPaths, err := tableRowsCount(ctx, ds.writer(ctx), "host_software_installed_paths")
amountHostSoftwareInstalledPaths, err := tableRowsCount(ctx, ds.reader(ctx), "host_software_installed_paths")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount host_software_installed_paths")
}
amountSoftwareCpes, err := tableRowsCount(ctx, ds.writer(ctx), "software_cpe")
amountSoftwareCpes, err := tableRowsCount(ctx, ds.reader(ctx), "software_cpe")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_cpe")
}
amountSoftwareCves, err := tableRowsCount(ctx, ds.writer(ctx), "software_cve")
amountSoftwareCves, err := tableRowsCount(ctx, ds.reader(ctx), "software_cve")
if err != nil {
return ctxerr.Wrap(ctx, err, "amount software_cve")
}
amountTeams, err := amountTeamsDB(ctx, ds.writer(ctx))
amountTeams, err := amountTeamsDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount teams")
}
amountPolicies, err := amountPoliciesDB(ctx, ds.writer(ctx))
amountPolicies, err := amountPoliciesDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount policies")
}
amountLabels, err := amountLabelsDB(ctx, ds.writer(ctx))
amountLabels, err := amountLabelsDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount labels")
}
@ -72,11 +72,11 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
if err != nil {
return ctxerr.Wrap(ctx, err, "statistics app config")
}
amountWeeklyUsers, err := amountActiveUsersSinceDB(ctx, ds.writer(ctx), since)
amountWeeklyUsers, err := amountActiveUsersSinceDB(ctx, ds.reader(ctx), since)
if err != nil {
return ctxerr.Wrap(ctx, err, "amount active users")
}
amountPolicyViolationDaysActual, amountPolicyViolationDaysPossible, err := amountPolicyViolationDaysDB(ctx, ds.writer(ctx))
amountPolicyViolationDaysActual, amountPolicyViolationDaysPossible, err := amountPolicyViolationDaysDB(ctx, ds.reader(ctx))
if err == sql.ErrNoRows {
level.Debug(ds.logger).Log("msg", "amount policy violation days", "err", err) //nolint:errcheck
} else if err != nil {
@ -86,15 +86,15 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
if err != nil {
return ctxerr.Wrap(ctx, err, "statistics error store")
}
amountHostsNotResponding, err := countHostsNotRespondingDB(ctx, ds.writer(ctx), ds.logger, config)
amountHostsNotResponding, err := countHostsNotRespondingDB(ctx, ds.reader(ctx), ds.logger, config)
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts not responding")
}
amountHostsByOrbitVersion, err := amountHostsByOrbitVersionDB(ctx, ds.writer(ctx))
amountHostsByOrbitVersion, err := amountHostsByOrbitVersionDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts by orbit version")
}
amountHostsByOsqueryVersion, err := amountHostsByOsqueryVersionDB(ctx, ds.writer(ctx))
amountHostsByOsqueryVersion, err := amountHostsByOsqueryVersionDB(ctx, ds.reader(ctx))
if err != nil {
return ctxerr.Wrap(ctx, err, "amount hosts by osquery version")
}
@ -134,7 +134,7 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
}
dest := statistics{}
err := sqlx.GetContext(ctx, ds.writer(ctx), &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
if err != nil {
if err == sql.ErrNoRows {
anonIdentifier, err := server.GenerateRandomText(64)

View file

@ -888,6 +888,16 @@ type ServerSettings struct {
QueryReportsDisabled bool `json:"query_reports_disabled"`
ScriptsDisabled bool `json:"scripts_disabled"`
AIFeaturesDisabled bool `json:"ai_features_disabled"`
QueryReportCap int `json:"query_report_cap"`
}
const DefaultMaxQueryReportRows int = 1000
func (f *ServerSettings) GetQueryReportCap() int {
if f.QueryReportCap <= 0 {
return DefaultMaxQueryReportRows
}
return f.QueryReportCap
}
// HostExpirySettings contains settings pertaining to automatic host expiry.

View file

@ -18,7 +18,7 @@ import (
type MDMAppleCommandIssuer interface {
InstallProfile(ctx context.Context, hostUUIDs []string, profile mobileconfig.Mobileconfig, uuid string) error
RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error
DeviceLock(ctx context.Context, host *Host, uuid string) error
DeviceLock(ctx context.Context, host *Host, uuid string) (unlockPIN string, err error)
EraseDevice(ctx context.Context, host *Host, uuid string) error
InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error
}

View file

@ -457,7 +457,7 @@ type Datastore interface {
QueryResultRowsForHost(ctx context.Context, queryID, hostID uint) ([]*ScheduledQueryResultRow, error)
ResultCountForQuery(ctx context.Context, queryID uint) (int, error)
ResultCountForQueryAndHost(ctx context.Context, queryID, hostID uint) (int, error)
OverwriteQueryResultRows(ctx context.Context, rows []*ScheduledQueryResultRow) error
OverwriteQueryResultRows(ctx context.Context, rows []*ScheduledQueryResultRow, maxQueryReportRows int) error
// CleanupDiscardedQueryResults deletes all query results for queries with DiscardData enabled.
// Used in cleanups_then_aggregation cron to cleanup rows that were inserted immediately
// after DiscardData was set to true due to query caching.

View file

@ -18,8 +18,7 @@ type Stats struct {
const (
// StatusOK is the success code returned by osquery
StatusOK OsqueryStatus = 0
MaxQueryReportRows int = 1000
StatusOK OsqueryStatus = 0
)
// QueryContent is the format of a query stanza in an osquery configuration.

View file

@ -414,8 +414,8 @@ func (s *HostLockWipeStatus) IsPendingLock() bool {
func (s HostLockWipeStatus) IsPendingUnlock() bool {
if s.HostFleetPlatform == "darwin" || s.HostFleetPlatform == "ios" || s.HostFleetPlatform == "ipados" {
// pending unlock if an unlock was requested
return !s.UnlockRequestedAt.IsZero()
// Apple MDM does not have a concept of pending unlock.
return false
}
// pending unlock if script execution request is queued but no result yet
return s.UnlockScript != nil && s.UnlockScript.ExitCode == nil

View file

@ -106,7 +106,8 @@ type Service interface {
CreateUserFromInvite(ctx context.Context, p UserPayload) (user *User, err error)
// CreateUser allows an admin to create a new user without first creating and validating invite tokens.
CreateUser(ctx context.Context, p UserPayload) (user *User, err error)
// The sessionKey is only returned (not-nil) when creating API-only (non-SSO) users.
CreateUser(ctx context.Context, p UserPayload) (user *User, sessionKey *string, err error)
// CreateInitialUser creates the first user, skipping authorization checks. If a user already exists this method
// should fail.
@ -274,12 +275,13 @@ type Service interface {
// included in the results.
ListQueries(ctx context.Context, opt ListOptions, teamID *uint, scheduled *bool, mergeInherited bool) ([]*Query, error)
GetQuery(ctx context.Context, id uint) (*Query, error)
// GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to
GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, error)
// GetQueryReportResults returns all the stored results of a query for hosts the requestor has access to.
// Returns a boolean indicating whether the report is clipped.
GetQueryReportResults(ctx context.Context, id uint) ([]HostQueryResultRow, bool, error)
// GetHostQueryReportResults returns all stored results of a query for a specific host
GetHostQueryReportResults(ctx context.Context, hid uint, queryID uint) (rows []HostQueryReportResult, lastFetched *time.Time, err error)
// QueryReportIsClipped returns true if the number of query report rows exceeds the maximum
QueryReportIsClipped(ctx context.Context, queryID uint) (bool, error)
QueryReportIsClipped(ctx context.Context, queryID uint, maxQueryReportRows int) (bool, error)
NewQuery(ctx context.Context, p QueryPayload) (*Query, error)
ModifyQuery(ctx context.Context, id uint, p QueryPayload) (*Query, error)
DeleteQuery(ctx context.Context, teamID *uint, name string) error
@ -1040,7 +1042,7 @@ type Service interface {
BatchSetScripts(ctx context.Context, maybeTmID *uint, maybeTmName *string, payloads []ScriptPayload, dryRun bool) error
// Script-based methods (at least for some platforms, MDM-based for others)
LockHost(ctx context.Context, hostID uint) error
LockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
UnlockHost(ctx context.Context, hostID uint) (unlockPIN string, err error)
WipeHost(ctx context.Context, hostID uint) error

View file

@ -91,7 +91,7 @@ type DEPService struct {
// getDefaultProfile returns a godep.Profile with default values set.
func (d *DEPService) getDefaultProfile() *godep.Profile {
return &godep.Profile{
ProfileName: "FleetDM default enrollment profile",
ProfileName: "Fleet default enrollment profile",
AllowPairing: true,
AutoAdvanceSetup: false,
IsSupervised: false,
@ -688,8 +688,8 @@ var enrollmentProfileMobileconfigTemplate = template.Must(template.New("").Parse
<string>{{ .SCEPURL }}</string>
<key>Subject</key>
<array>
<array><array><string>O</string><string>FleetDM</string></array></array>
<array><array><string>CN</string><string>FleetDM Identity</string></array></array>
<array><array><string>O</string><string>Fleet</string></array></array>
<array><array><string>CN</string><string>Fleet Identity</string></array></array>
</array>
</dict>
<key>PayloadIdentifier</key>

View file

@ -20,7 +20,7 @@ import (
const (
defaultFleetDMAPIURL = "https://fleetdm.com"
getSignedAPNSCSRPath = "/api/v1/deliver-apple-csr"
depCertificateCommonName = "FleetDM"
depCertificateCommonName = "Fleet"
depCertificateExpiryDays = 30
)
@ -208,7 +208,7 @@ func NewSCEPCACertKey() (*x509.Certificate, *rsa.PrivateKey, error) {
caCert := depot.NewCACert(
depot.WithYears(10),
depot.WithCommonName("FleetDM"),
depot.WithCommonName("Fleet"),
)
crtBytes, err := caCert.SelfSign(rand.Reader, key.Public(), key)

View file

@ -90,8 +90,8 @@ func (svc *MDMAppleCommander) RemoveProfile(ctx context.Context, hostUUIDs []str
return ctxerr.Wrap(ctx, err, "commander remove profile")
}
func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host, uuid string) error {
pin := GenerateRandomPin(6)
func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host, uuid string) (unlockPIN string, err error) {
unlockPIN = GenerateRandomPin(6)
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">
@ -106,22 +106,23 @@ func (svc *MDMAppleCommander) DeviceLock(ctx context.Context, host *fleet.Host,
<string>%s</string>
</dict>
</dict>
</plist>`, uuid, pin)
</plist>`, uuid, unlockPIN,
)
cmd, err := mdm.DecodeCommand([]byte(raw))
if err != nil {
return ctxerr.Wrap(ctx, err, "decoding command")
return "", ctxerr.Wrap(ctx, err, "decoding command")
}
if err := svc.storage.EnqueueDeviceLockCommand(ctx, host, cmd, pin); err != nil {
return ctxerr.Wrap(ctx, err, "enqueuing for DeviceLock")
if err := svc.storage.EnqueueDeviceLockCommand(ctx, host, cmd, unlockPIN); err != nil {
return "", ctxerr.Wrap(ctx, err, "enqueuing for DeviceLock")
}
if err := svc.sendNotifications(ctx, []string{host.UUID}); err != nil {
return ctxerr.Wrap(ctx, err, "sending notifications for DeviceLock")
return "", ctxerr.Wrap(ctx, err, "sending notifications for DeviceLock")
}
return nil
return unlockPIN, nil
}
func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, host *fleet.Host, uuid string) error {

View file

@ -132,8 +132,9 @@ func TestMDMAppleCommander(t *testing.T) {
require.Len(t, pin, 6)
return nil
}
err = cmdr.DeviceLock(ctx, host, cmdUUID)
pin, err := cmdr.DeviceLock(ctx, host, cmdUUID)
require.NoError(t, err)
require.Len(t, pin, 6)
require.True(t, mdmStorage.EnqueueDeviceLockCommandFuncInvoked)
mdmStorage.EnqueueDeviceLockCommandFuncInvoked = false
require.True(t, mdmStorage.RetrievePushInfoFuncInvoked)

View file

@ -339,7 +339,7 @@ type ResultCountForQueryFunc func(ctx context.Context, queryID uint) (int, error
type ResultCountForQueryAndHostFunc func(ctx context.Context, queryID uint, hostID uint) (int, error)
type OverwriteQueryResultRowsFunc func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error
type OverwriteQueryResultRowsFunc func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error
type CleanupDiscardedQueryResultsFunc func(ctx context.Context) error
@ -3508,11 +3508,11 @@ func (s *DataStore) ResultCountForQueryAndHost(ctx context.Context, queryID uint
return s.ResultCountForQueryAndHostFunc(ctx, queryID, hostID)
}
func (s *DataStore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
func (s *DataStore) OverwriteQueryResultRows(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
s.mu.Lock()
s.OverwriteQueryResultRowsFuncInvoked = true
s.mu.Unlock()
return s.OverwriteQueryResultRowsFunc(ctx, rows)
return s.OverwriteQueryResultRowsFunc(ctx, rows, maxQueryReportRows)
}
func (s *DataStore) CleanupDiscardedQueryResults(ctx context.Context) error {

View file

@ -8,11 +8,16 @@ import (
)
// CreateUser creates a new user, skipping the invitation process.
func (c *Client) CreateUser(p fleet.UserPayload) error {
//
// The session key (aka API token) is returned only when creating
// API only users.
func (c *Client) CreateUser(p fleet.UserPayload) (*string, error) {
verb, path := "POST", "/api/latest/fleet/users/admin"
var responseBody createUserResponse
return c.authenticatedRequest(p, verb, path, &responseBody)
if err := c.authenticatedRequest(p, verb, path, &responseBody); err != nil {
return nil, err
}
return responseBody.Token, nil
}
// ListUsers retrieves the list of users.

View file

@ -1231,7 +1231,12 @@ func getHostQueryReportEndpoint(ctx context.Context, request interface{}, svc fl
return getHostQueryReportResponse{Err: err}, nil
}
isClipped, err := svc.QueryReportIsClipped(ctx, req.QueryID)
appConfig, err := svc.AppConfigObfuscated(ctx)
if err != nil {
return getHostQueryReportResponse{Err: err}, nil
}
isClipped, err := svc.QueryReportIsClipped(ctx, req.QueryID, appConfig.ServerSettings.GetQueryReportCap())
if err != nil {
return getHostQueryReportResponse{Err: err}, nil
}

View file

@ -1648,9 +1648,9 @@ func TestLockUnlockWipeHostAuth(t *testing.T) {
}
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
err := svc.LockHost(ctx, globalHostID)
_, err := svc.LockHost(ctx, globalHostID)
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
err = svc.LockHost(ctx, teamHostID)
_, err = svc.LockHost(ctx, teamHostID)
checkAuthErr(t, tt.shouldFailTeamWrite, err)
// Pretend we locked the host

View file

@ -281,6 +281,44 @@ func (s *integrationTestSuite) TestQueryCreationLogsActivity() {
require.True(t, found)
}
func (s *integrationTestSuite) TestCreatingAPIOnlyUserReturnsAPIToken() {
t := s.T()
defer func() {
s.token = s.getTestAdminToken()
}()
var createResp createUserResponse
params := fleet.UserPayload{
Name: ptr.String("someadmin"),
Email: ptr.String("someadmin@example.com"),
Password: ptr.String(test.GoodPassword),
GlobalRole: ptr.String(fleet.RoleAdmin),
APIOnly: ptr.Bool(false),
}
s.DoJSON("POST", "/api/latest/fleet/users/admin", params, http.StatusOK, &createResp)
assert.NotZero(t, createResp.User.ID)
assert.Nil(t, createResp.Token)
params = fleet.UserPayload{
Name: ptr.String("apionly"),
Email: ptr.String("apionly@example.com"),
Password: ptr.String(test.GoodPassword),
GlobalRole: ptr.String(fleet.RoleObserver),
APIOnly: ptr.Bool(true),
// AdminForcedPasswordReset is set to false when creating api-only users via `fleetctl user create --api-only`.
AdminForcedPasswordReset: ptr.Bool(false),
}
s.DoJSON("POST", "/api/latest/fleet/users/admin", params, http.StatusOK, &createResp)
assert.NotZero(t, createResp.User.ID)
assert.NotNil(t, createResp.Token)
s.token = *createResp.Token
var chr countHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts/count", countHostsRequest{}, http.StatusOK, &chr)
assert.Equal(t, 0, chr.Count)
}
func (s *integrationTestSuite) TestActivityUserEmailPersistsAfterDeletion() {
t := s.T()
@ -10799,12 +10837,14 @@ func (s *integrationTestSuite) TestQueryReports() {
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
require.False(t, gqrr.ReportClipped)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
require.False(t, gqrr.ReportClipped)
// don't change platform or min_osquery_version and results should not be deleted
s.DoJSON("POST", "/api/latest/fleet/spec/queries", applyQuerySpecsRequest{
@ -10812,6 +10852,7 @@ func (s *integrationTestSuite) TestQueryReports() {
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
require.False(t, gqrr.ReportClipped)
// now update the platform and results should be deleted.
osqueryInfoQuerySpec.Platform = "darwin"
@ -10820,30 +10861,35 @@ func (s *integrationTestSuite) TestQueryReports() {
}, http.StatusOK, &applyResp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
require.False(t, gqrr.ReportClipped)
// Update logging type, which should cause results deletion
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", usbDevicesQuery.ID), modifyQueryRequest{ID: usbDevicesQuery.ID, QueryPayload: fleet.QueryPayload{Logging: &fleet.LoggingDifferential}}, http.StatusOK, &modifyQueryResp)
require.Equal(t, fleet.LoggingDifferential, modifyQueryResp.Query.Logging)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", usbDevicesQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
require.False(t, gqrr.ReportClipped)
// Re-add results to our query and check that they're actually there
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 1)
require.False(t, gqrr.ReportClipped)
discardData := true
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/queries/%d", osqueryInfoQuery.ID), modifyQueryRequest{ID: osqueryInfoQuery.ID, QueryPayload: fleet.QueryPayload{DiscardData: &discardData}}, http.StatusOK, &modifyQueryResp)
require.True(t, modifyQueryResp.Query.DiscardData)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
require.False(t, gqrr.ReportClipped)
// check that now that discardData is set, we don't add new results
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, 0)
require.False(t, gqrr.ReportClipped)
// Verify that we can't have more than 1k results
@ -10855,7 +10901,7 @@ func (s *integrationTestSuite) TestQueryReports() {
NodeKey: *host1Global.NodeKey,
LogType: "result",
Data: json.RawMessage(`[{
"snapshot": [` + results(1000, host1Global.UUID) + `
"snapshot": [` + results(fleet.DefaultMaxQueryReportRows, host1Global.UUID) + `
],
"action": "snapshot",
"name": "pack/Global/` + osqueryInfoQuery.Name + `",
@ -10878,13 +10924,14 @@ func (s *integrationTestSuite) TestQueryReports() {
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, fleet.MaxQueryReportRows)
require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
require.True(t, gqrr.ReportClipped)
ghqrr = getHostQueryReportResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/queries/%d", host1Global.ID, osqueryInfoQuery.ID), getHostQueryReportRequest{}, http.StatusOK, &ghqrr)
require.NoError(t, ghqrr.Err)
require.Len(t, ghqrr.Results, fleet.DefaultMaxQueryReportRows)
require.True(t, ghqrr.ReportClipped)
require.Len(t, ghqrr.Results, fleet.MaxQueryReportRows)
slreq.Data = json.RawMessage(`[{
"snapshot": [` + results(1, host1Global.UUID) + `
@ -10906,7 +10953,41 @@ func (s *integrationTestSuite) TestQueryReports() {
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, fleet.MaxQueryReportRows)
require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
require.True(t, gqrr.ReportClipped)
appConfigSpec := map[string]map[string]int{
"server_settings": {"query_report_cap": fleet.DefaultMaxQueryReportRows + 1},
}
s.Do("PATCH", "/api/latest/fleet/config", appConfigSpec, http.StatusOK)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows)
require.False(t, gqrr.ReportClipped)
slreq.Data = json.RawMessage(`[{
"snapshot": [` + results(1002, host1Global.UUID) + `
],
"action": "snapshot",
"name": "pack/Global/` + osqueryInfoQuery.Name + `",
"hostIdentifier": "` + *host1Global.OsqueryHostID + `",
"calendarTime": "Fri Oct 6 18:13:04 2023 UTC",
"unixTime": 1696615984,
"epoch": 0,
"counter": 0,
"numerics": false,
"decorations": {
"host_uuid": "187c4d56-8e45-1a9d-8513-ac17efd2f0fd",
"hostname": "` + host1Global.Hostname + `"
}
}]`)
s.DoJSON("POST", "/api/osquery/log", slreq, http.StatusOK, &slres)
require.NoError(t, slres.Err)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/queries/%d/report", osqueryInfoQuery.ID), getQueryReportRequest{}, http.StatusOK, &gqrr)
require.Len(t, gqrr.Results, fleet.DefaultMaxQueryReportRows+1)
require.True(t, gqrr.ReportClipped)
// TODO: Set global discard flag and verify that all data is gone.
}

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"os"
"path/filepath"
@ -73,12 +74,15 @@ func (s *integrationMDMTestSuite) TestTurnOnLifecycleEventsApple() {
{
"locked host turns on MDM",
func(t *testing.T, host *fleet.Host, device *mdmtest.TestAppleMDMClient) {
s.Do(
var resp lockHostResponse
s.DoJSON(
"POST",
fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID),
nil,
http.StatusNoContent,
http.StatusOK,
&resp,
)
assert.Len(t, resp.UnlockPIN, 6)
cmd, err := device.Idle()
require.NoError(t, err)

View file

@ -531,9 +531,9 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() {
var mdmResp getAppleMDMResponse
s.DoJSON("GET", "/api/latest/fleet/apns", nil, http.StatusOK, &mdmResp)
// returned values are dummy, this is a test certificate
require.Equal(t, "FleetDM", mdmResp.Issuer)
require.Equal(t, "Fleet", mdmResp.Issuer)
require.NotZero(t, mdmResp.SerialNumber)
require.Equal(t, "FleetDM", mdmResp.CommonName)
require.Equal(t, "Fleet", mdmResp.CommonName)
require.NotZero(t, mdmResp.RenewDate)
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -8038,7 +8038,9 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() {
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", host.ID), nil, http.StatusConflict, &unlockResp)
// lock the host
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusNoContent)
var lockResp lockHostResponse
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusOK, &lockResp)
assert.Len(t, lockResp.UnlockPIN, 6)
// refresh the host's status, it is now pending lock
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
@ -8084,12 +8086,12 @@ func (s *integrationMDMTestSuite) TestLockUnlockWipeMacOS() {
unlockActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeUnlockedHost{}.ActivityName(),
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "host_platform": %q}`, host.ID, host.DisplayName(), host.FleetPlatform()), 0)
// refresh the host's status, it is locked pending unlock
// refresh the host's status, it is still locked
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
require.Equal(t, "locked", *getHostResp.Host.MDM.DeviceStatus)
require.NotNil(t, getHostResp.Host.MDM.PendingAction)
require.Equal(t, "unlock", *getHostResp.Host.MDM.PendingAction)
assert.Empty(t, *getHostResp.Host.MDM.PendingAction)
// try unlocking the host again simply returns the PIN again
unlockResp = unlockHostResponse{}
@ -8834,7 +8836,7 @@ func (s *integrationMDMTestSuite) appleCoreCertsSetup() {
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
Subject: pkix.Name{
CommonName: "FleetDM",
CommonName: "Fleet",
ExtraNames: []pkix.AttributeTypeAndValue{
{
Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1},

View file

@ -1799,7 +1799,8 @@ func (svc *Service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage
unmarshaledResults, queriesDBData := svc.preProcessOsqueryResults(ctx, logs, queryReportsDisabled)
if !queryReportsDisabled {
svc.saveResultLogsToQueryReports(ctx, unmarshaledResults, queriesDBData)
maxQueryReportRows := appConfig.ServerSettings.GetQueryReportCap()
svc.saveResultLogsToQueryReports(ctx, unmarshaledResults, queriesDBData, maxQueryReportRows)
}
var filteredLogs []json.RawMessage
@ -1861,7 +1862,12 @@ func (svc *Service) SubmitResultLogs(ctx context.Context, logs []json.RawMessage
// Query Reports
////////////////////////////////////////////////////////////////////////////////
func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshaledResults []*fleet.ScheduledQueryResult, queriesDBData map[string]*fleet.Query) {
func (svc *Service) saveResultLogsToQueryReports(
ctx context.Context,
unmarshaledResults []*fleet.ScheduledQueryResult,
queriesDBData map[string]*fleet.Query,
maxQueryReportRows int,
) {
// skipauth: Authorization is currently for user endpoints only.
svc.authz.SkipAuthorization(ctx)
@ -1903,11 +1909,11 @@ func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshale
level.Error(svc.logger).Log("msg", "get result count for query", "err", err, "query_id", dbQuery.ID)
continue
}
if count >= fleet.MaxQueryReportRows {
if count >= maxQueryReportRows {
continue
}
if err := svc.overwriteResultRows(ctx, result, dbQuery.ID, host.ID); err != nil {
if err := svc.overwriteResultRows(ctx, result, dbQuery.ID, host.ID, maxQueryReportRows); err != nil {
level.Error(svc.logger).Log("msg", "overwrite results", "err", err, "query_id", dbQuery.ID, "host_id", host.ID)
continue
}
@ -1919,7 +1925,7 @@ func (svc *Service) saveResultLogsToQueryReports(ctx context.Context, unmarshale
// The "snapshot" array in a ScheduledQueryResult can contain multiple rows.
// Each row is saved as a separate ScheduledQueryResultRow, i.e. a result could contain
// many USB Devices or a result could contain all user accounts on a host.
func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.ScheduledQueryResult, queryID, hostID uint) error {
func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.ScheduledQueryResult, queryID, hostID uint, maxQueryReportRows int) error {
fetchTime := time.Now()
rows := make([]*fleet.ScheduledQueryResultRow, 0, len(result.Snapshot))
@ -1945,7 +1951,7 @@ func (svc *Service) overwriteResultRows(ctx context.Context, result *fleet.Sched
rows = append(rows, row)
}
if err := svc.ds.OverwriteQueryResultRows(ctx, rows); err != nil {
if err := svc.ds.OverwriteQueryResultRows(ctx, rows, maxQueryReportRows); err != nil {
return ctxerr.Wrap(ctx, err, "overwriting query result rows")
}
return nil

View file

@ -614,7 +614,7 @@ func TestSubmitResultLogsToLogDestination(t *testing.T) {
return 0, nil
}
teamQueryResultsStored := false
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
if len(rows) == 0 {
return nil
}
@ -766,7 +766,7 @@ func TestSaveResultLogsToQueryReports(t *testing.T) {
Logging: fleet.LoggingSnapshot,
},
}
serv.saveResultLogsToQueryReports(ctx, results, discardDataFalse)
serv.saveResultLogsToQueryReports(ctx, results, discardDataFalse, fleet.DefaultMaxQueryReportRows)
assert.False(t, ds.OverwriteQueryResultRowsFuncInvoked)
// Happy Path: Results saved
@ -777,13 +777,13 @@ func TestSaveResultLogsToQueryReports(t *testing.T) {
Logging: fleet.LoggingSnapshot,
},
}
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
return nil
}
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
return 0, nil
}
serv.saveResultLogsToQueryReports(ctx, results, discardDataTrue)
serv.saveResultLogsToQueryReports(ctx, results, discardDataTrue, fleet.DefaultMaxQueryReportRows)
require.True(t, ds.OverwriteQueryResultRowsFuncInvoked)
}
@ -825,7 +825,7 @@ func TestSubmitResultLogsToQueryResultsWithEmptySnapShot(t *testing.T) {
return 0, nil
}
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
require.Len(t, rows, 1)
require.Equal(t, uint(999), rows[0].HostID)
require.NotZero(t, rows[0].LastFetched)
@ -876,7 +876,7 @@ func TestSubmitResultLogsToQueryResultsDoesNotCountNullDataRows(t *testing.T) {
return 0, nil
}
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
require.Len(t, rows, 1)
require.Equal(t, uint(999), rows[0].HostID)
require.NotZero(t, rows[0].LastFetched)
@ -933,7 +933,7 @@ func TestSubmitResultLogsFail(t *testing.T) {
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
return 0, nil
}
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow) error {
ds.OverwriteQueryResultRowsFunc = func(ctx context.Context, rows []*fleet.ScheduledQueryResultRow, maxQueryReportRows int) error {
return nil
}

View file

@ -121,16 +121,17 @@ type getQueryReportRequest struct {
}
type getQueryReportResponse struct {
QueryID uint `json:"query_id"`
Results []fleet.HostQueryResultRow `json:"results"`
Err error `json:"error,omitempty"`
QueryID uint `json:"query_id"`
Results []fleet.HostQueryResultRow `json:"results"`
ReportClipped bool `json:"report_clipped"`
Err error `json:"error,omitempty"`
}
func (r getQueryReportResponse) error() error { return r.Err }
func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*getQueryReportRequest)
queryReportResults, err := svc.GetQueryReportResults(ctx, req.ID)
queryReportResults, reportClipped, err := svc.GetQueryReportResults(ctx, req.ID)
if err != nil {
return listQueriesResponse{Err: err}, nil
}
@ -140,44 +141,53 @@ func getQueryReportEndpoint(ctx context.Context, request interface{}, svc fleet.
results = queryReportResults
}
return getQueryReportResponse{
QueryID: req.ID,
Results: results,
QueryID: req.ID,
Results: results,
ReportClipped: reportClipped,
}, nil
}
func (svc *Service) GetQueryReportResults(ctx context.Context, id uint) ([]fleet.HostQueryResultRow, error) {
func (svc *Service) GetQueryReportResults(ctx context.Context, id uint) ([]fleet.HostQueryResultRow, bool, error) {
// Load query first to get its teamID.
query, err := svc.ds.Query(ctx, id)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return nil, ctxerr.Wrap(ctx, err, "get query from datastore")
return nil, false, ctxerr.Wrap(ctx, err, "get query from datastore")
}
if err := svc.authz.Authorize(ctx, query, fleet.ActionRead); err != nil {
return nil, err
return nil, false, err
}
if query.DiscardData {
return nil, nil
return nil, false, nil
}
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, fleet.ErrNoContext
return nil, false, fleet.ErrNoContext
}
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: true}
queryReportResultRows, err := svc.ds.QueryResultRows(ctx, id, filter)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get query report results")
return nil, false, ctxerr.Wrap(ctx, err, "get query report results")
}
queryReportResults, err := fleet.MapQueryReportResultsToRows(queryReportResultRows)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "map db rows to results")
return nil, false, ctxerr.Wrap(ctx, err, "map db rows to results")
}
return queryReportResults, nil
appConfig, err := svc.ds.AppConfig(ctx)
if err != nil {
return nil, false, ctxerr.Wrap(ctx, err, "get app config")
}
reportClipped, err := svc.QueryReportIsClipped(ctx, id, appConfig.ServerSettings.GetQueryReportCap())
if err != nil {
return nil, false, ctxerr.Wrap(ctx, err, "check query report is clipped")
}
return queryReportResults, reportClipped, nil
}
func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint) (bool, error) {
func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint, maxQueryReportRows int) (bool, error) {
query, err := svc.ds.Query(ctx, queryID)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
@ -191,7 +201,7 @@ func (svc *Service) QueryReportIsClipped(ctx context.Context, queryID uint) (boo
if err != nil {
return false, err
}
return count >= fleet.MaxQueryReportRows, nil
return count >= maxQueryReportRows, nil
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -644,7 +644,7 @@ func TestQueryAuth(t *testing.T) {
_, err = svc.GetQuery(ctx, tt.qid)
checkAuthErr(t, tt.shouldFailRead, err)
_, err = svc.QueryReportIsClipped(ctx, tt.qid)
_, err = svc.QueryReportIsClipped(ctx, tt.qid, fleet.DefaultMaxQueryReportRows)
checkAuthErr(t, tt.shouldFailRead, err)
_, err = svc.ListQueries(ctx, fleet.ListOptions{}, query.TeamID, nil, false)
@ -688,15 +688,15 @@ func TestQueryReportIsClipped(t *testing.T) {
return 0, nil
}
isClipped, err := svc.QueryReportIsClipped(viewerCtx, 1)
isClipped, err := svc.QueryReportIsClipped(viewerCtx, 1, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
require.False(t, isClipped)
ds.ResultCountForQueryFunc = func(ctx context.Context, queryID uint) (int, error) {
return fleet.MaxQueryReportRows, nil
return fleet.DefaultMaxQueryReportRows, nil
}
isClipped, err = svc.QueryReportIsClipped(viewerCtx, 1)
isClipped, err = svc.QueryReportIsClipped(viewerCtx, 1, fleet.DefaultMaxQueryReportRows)
require.NoError(t, err)
require.True(t, isClipped)
}
@ -725,9 +725,10 @@ func TestQueryReportReturnsNilIfDiscardDataIsTrue(t *testing.T) {
}, nil
}
results, err := svc.GetQueryReportResults(viewerCtx, 1)
results, reportClipped, err := svc.GetQueryReportResults(viewerCtx, 1)
require.NoError(t, err)
require.Nil(t, results)
require.False(t, reportClipped)
}
func TestComparePlatforms(t *testing.T) {

View file

@ -915,26 +915,37 @@ type lockHostRequest struct {
}
type lockHostResponse struct {
Err error `json:"error,omitempty"`
Err error `json:"error,omitempty"`
UnlockPIN string `json:"unlock_pin,omitempty"`
StatusCode int `json:"-"`
}
func (r lockHostResponse) Status() int { return http.StatusNoContent }
func (r lockHostResponse) Status() int {
if r.StatusCode != 0 {
return r.StatusCode
}
return http.StatusNoContent
}
func (r lockHostResponse) error() error { return r.Err }
func lockHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*lockHostRequest)
if err := svc.LockHost(ctx, req.HostID); err != nil {
unlockPIN, err := svc.LockHost(ctx, req.HostID)
if err != nil {
return lockHostResponse{Err: err}, nil
}
if unlockPIN != "" {
return lockHostResponse{UnlockPIN: unlockPIN, StatusCode: http.StatusOK}, nil
}
return lockHostResponse{}, nil
}
func (svc *Service) LockHost(ctx context.Context, hostID uint) error {
func (svc *Service) LockHost(ctx context.Context, hostID uint) (string, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
return fleet.ErrMissingLicense
return "", fleet.ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -34,38 +34,43 @@ type createUserRequest struct {
type createUserResponse struct {
User *fleet.User `json:"user,omitempty"`
Err error `json:"error,omitempty"`
// Token is only returned when creating API-only (non-SSO) users.
Token *string `json:"token,omitempty"`
Err error `json:"error,omitempty"`
}
func (r createUserResponse) error() error { return r.Err }
func createUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*createUserRequest)
user, err := svc.CreateUser(ctx, req.UserPayload)
user, sessionKey, err := svc.CreateUser(ctx, req.UserPayload)
if err != nil {
return createUserResponse{Err: err}, nil
}
return createUserResponse{User: user}, nil
return createUserResponse{
User: user,
Token: sessionKey,
}, nil
}
func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, *string, error) {
var teams []fleet.UserTeam
if p.Teams != nil {
teams = *p.Teams
}
if err := svc.authz.Authorize(ctx, &fleet.User{Teams: teams}, fleet.ActionWrite); err != nil {
return nil, err
return nil, nil, err
}
if err := p.VerifyAdminCreate(); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify user payload")
return nil, nil, ctxerr.Wrap(ctx, err, "verify user payload")
}
if teams != nil {
// Validate that the teams exist
teamsSummary, err := svc.ds.TeamsSummary(ctx)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "fetching teams in attempt to verify team exists")
return nil, nil, ctxerr.Wrap(ctx, err, "fetching teams in attempt to verify team exists")
}
teamIDs := map[uint]struct{}{}
for _, team := range teamsSummary {
@ -74,7 +79,7 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
for _, userTeam := range teams {
_, ok := teamIDs[userTeam.Team.ID]
if !ok {
return nil, ctxerr.Wrap(
return nil, nil, ctxerr.Wrap(
ctx, fleet.NewInvalidArgumentError("teams.id", fmt.Sprintf("team with id %d does not exist", userTeam.Team.ID)),
)
}
@ -82,7 +87,7 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
}
if invite, err := svc.ds.InviteByEmail(ctx, *p.Email); err == nil && invite != nil {
return nil, ctxerr.Errorf(ctx, "%s already invited", *p.Email)
return nil, nil, ctxerr.Errorf(ctx, "%s already invited", *p.Email)
}
if p.AdminForcedPasswordReset == nil {
@ -90,7 +95,28 @@ func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet
p.AdminForcedPasswordReset = ptr.Bool(true)
}
return svc.NewUser(ctx, p)
user, err := svc.NewUser(ctx, p)
if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "create user")
}
// The sessionKey is returned for API-only non-SSO users only.
var sessionKey *string
if user.APIOnly && !user.SSOEnabled {
if p.Password == nil {
// Should not happen but let's log just in case.
level.Error(svc.logger).Log("err", err, "msg", "password not set during admin user creation")
} else {
// Create a session for the API-only user by logging in.
_, session, err := svc.Login(ctx, user.Email, *p.Password)
if err != nil {
return nil, nil, ctxerr.Wrap(ctx, err, "create session for api-only user")
}
sessionKey = &session.Key
}
}
return user, sessionKey, nil
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -367,7 +367,7 @@ func TestUserAuth(t *testing.T) {
}
teams := []fleet.UserTeam{{Team: fleet.Team{ID: teamID}, Role: fleet.RoleMaintainer}}
_, err = svc.CreateUser(ctx, fleet.UserPayload{
_, _, err = svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Name"),
Email: ptr.String("some@email.com"),
Password: ptr.String(test.GoodPassword),
@ -375,7 +375,7 @@ func TestUserAuth(t *testing.T) {
})
checkAuthErr(t, tt.shouldFailTeamWrite, err)
_, err = svc.CreateUser(ctx, fleet.UserPayload{
_, _, err = svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Name"),
Email: ptr.String("some@email.com"),
Password: ptr.String(test.GoodPassword),
@ -641,6 +641,7 @@ func TestUsersWithDS(t *testing.T) {
{"CreateUserForcePasswdReset", testUsersCreateUserForcePasswdReset},
{"ChangePassword", testUsersChangePassword},
{"RequirePasswordReset", testUsersRequirePasswordReset},
{"UsersCreateUserWithAPIOnly", testUsersCreateUserWithAPIOnly},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@ -668,13 +669,14 @@ func testUsersCreateUserForcePasswdReset(t *testing.T, ds *mysql.Datastore) {
// As the admin, create a new user.
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
user, err := svc.CreateUser(ctx, fleet.UserPayload{
user, sessionKey, err := svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Observer"),
Email: ptr.String("some-observer@email.com"),
Password: ptr.String(test.GoodPassword),
GlobalRole: ptr.String(fleet.RoleObserver),
})
require.NoError(t, err)
require.Nil(t, sessionKey) // only set when creating API-only users
user, err = ds.UserByID(context.Background(), user.ID)
require.NoError(t, err)
@ -1319,3 +1321,50 @@ func TestTeamAdminAddRoleOtherTeam(t *testing.T) {
require.Equal(t, (&authz.Forbidden{}).Error(), err.Error())
require.False(t, ds.SaveUserFuncInvoked)
}
func testUsersCreateUserWithAPIOnly(t *testing.T, ds *mysql.Datastore) {
svc, ctx := newTestService(t, ds, nil, nil)
host, err := ds.NewHost(ctx, &fleet.Host{
UUID: "uuid-42",
OsqueryHostID: ptr.String("osquery_host_id-42"),
})
require.NoError(t, err)
// Create admin user.
admin := &fleet.User{
Name: "Fleet Admin",
Email: "admin@foo.com",
GlobalRole: ptr.String(fleet.RoleAdmin),
}
err = admin.SetPassword(test.GoodPassword, 10, 10)
require.NoError(t, err)
admin, err = ds.NewUser(ctx, admin)
require.NoError(t, err)
// As the admin, create a new API-only user.
ctx = viewer.NewContext(ctx, viewer.Viewer{User: admin})
apiOnlyUser, sessionKey, err := svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Observer"),
Email: ptr.String("some-observer@email.com"),
Password: ptr.String(test.GoodPassword),
GlobalRole: ptr.String(fleet.RoleObserver),
APIOnly: ptr.Bool(true),
})
require.NoError(t, err)
require.NotNil(t, sessionKey)
require.NotEmpty(t, *sessionKey)
sessions, err := svc.GetInfoAboutSessionsForUser(ctx, apiOnlyUser.ID)
require.NoError(t, err)
require.Len(t, sessions, 1)
session := sessions[0]
require.Equal(t, *sessionKey, session.Key)
refreshCtx(t, ctx, apiOnlyUser, ds, session)
hosts, err := svc.ListHosts(ctx, fleet.HostListOptions{})
require.NoError(t, err)
require.Len(t, hosts, 1)
require.Equal(t, host.ID, hosts[0].ID)
}

View file

@ -72,7 +72,7 @@ func TestMacosSetupAssistant(t *testing.T) {
DEPClient: apple_mdm.NewDEPClient(depStorage, ds, logger),
}
const defaultProfileName = "FleetDM default enrollment profile"
const defaultProfileName = "Fleet default enrollment profile"
// track the profile assigned to each device
serialsToProfile := map[string]string{

View file

@ -12,6 +12,7 @@ github.com/fleetdm/fleet/v4/server/fleet/ServerSettings DeferredSaveHost bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings QueryReportsDisabled bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings ScriptsDisabled bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings AIFeaturesDisabled bool
github.com/fleetdm/fleet/v4/server/fleet/ServerSettings QueryReportCap int
github.com/fleetdm/fleet/v4/server/fleet/AppConfig SMTPSettings *fleet.SMTPSettings
github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPEnabled bool
github.com/fleetdm/fleet/v4/server/fleet/SMTPSettings SMTPConfigured bool

View file

@ -19,6 +19,10 @@ module.exports = {
fleetApiKey: {
type: 'string',
required: true,
},
redirectToExternalPageAfterAuthorization: {
type: 'string',
description: 'If provided, the user will be sent to this URL after they complete the setup of this integration'
}
},
@ -59,7 +63,6 @@ module.exports = {
},
fn: async function (inputs) {
let url = require('url');
// Look for any existing VantaConnection records that use this fleet instance URL.
@ -139,17 +142,26 @@ module.exports = {
fleetApiKey: inputs.fleetApiKey,
});
}
let callbackUrl = `/vanta-authorization`;
if(inputs.redirectToExternalPageAfterAuthorization){
callbackUrl += `?redirectAfterSetup=${inputs.redirectToExternalPageAfterAuthorization}`;
}
// Build the authorization URL for this request.
let vantaAuthorizationRequestURL = `https://app.vanta.com/oauth/authorize?client_id=${encodeURIComponent(sails.config.custom.vantaAuthorizationClientId)}&scope=connectors.self:write-resource connectors.self:read-resource&state=${encodeURIComponent(generatedStateForThisRequest)}&source_id=${encodeURIComponent(sourceIDForThisRequest)}&redirect_uri=${encodeURIComponent(url.resolve(sails.config.custom.baseUrl, '/vanta-authorization'))}&response_type=code`;
let vantaAuthorizationRequestURL = `https://app.vanta.com/oauth/authorize?client_id=${encodeURIComponent(sails.config.custom.vantaAuthorizationClientId)}&scope=connectors.self:write-resource connectors.self:read-resource&state=${encodeURIComponent(generatedStateForThisRequest)}&source_id=${encodeURIComponent(sourceIDForThisRequest)}&redirect_uri=${encodeURIComponent(url.resolve(sails.config.custom.baseUrl, callbackUrl))}&response_type=code`;
// Set a `state` cookie on the user's browser. This value will be checked against a query parameter when the user returns to fleetdm.com.
this.res.cookie('state', generatedStateForThisRequest, {signed: true});
if(inputs.redirectToExternalPageAfterAuthorization){
let internalRedirectUrl = `${sails.config.custom.baseUrl}/redirect-vanta-authorization-request?vantaSourceId=${encodeURIComponent(sourceIDForThisRequest)}&state=${encodeURIComponent(generatedStateForThisRequest)}&vantaAuthorizationRequestURL=${encodeURIComponent(vantaAuthorizationRequestURL)}&redirectAfterSetup=${encodeURIComponent(inputs.redirectToExternalPageAfterAuthorization)}`;
// Set the sourceId to a cookie, we'll use this value to find the database record we created for this request when the user returns to fleetdm.com.
this.res.cookie('vantaSourceId', sourceIDForThisRequest, {signed: true});
return vantaAuthorizationRequestURL;
return internalRedirectUrl;
// If the useInternalRedirect input was provided, we'll return the URL of an internal endpoiint that will set the required cookies for this request.
} else {
// Otherwise, if this request came from a user on the connect-vanta page, we'll set the cookies are redirect them directly to Vanta.
// Set a `state` cookie on the user's browser. This value will be checked against a query parameter when the user returns to fleetdm.com.
this.res.cookie('state', generatedStateForThisRequest, {signed: true});
// Set the sourceId to a cookie, we'll use this value to find the database record we created for this request when the user returns to fleetdm.com.
this.res.cookie('vantaSourceId', sourceIDForThisRequest, {signed: true});
return vantaAuthorizationRequestURL;
}
}

View file

@ -0,0 +1,61 @@
module.exports = {
friendlyName: 'Redirect vanta authorization request',
description: 'Sets provided inputs in the user`s browser as cookies and redirects them to Vanta.',
inputs: {
vantaSourceId: {
type: 'string',
description: 'The generated vanta Source ID for this request.',
required: true,
},
state: {
type: 'string',
description: 'The state provided to Vanta when an authorization request was created',
required: true,
},
vantaAuthorizationRequestURL: {
type: 'string',
description: 'The Vanta authorization url that the user will be directed to after they are sent to this page.',
required: true,
},
redirectAfterSetup: {
type: 'string',
description: 'The URL that the user will be redirected to after they complete setup.',
required: true,
}
},
exits: {
noMatchingVantaConnection: {
description: 'No Vanta connection could be found using the provided vantaSourceId',
responseType: 'badRequest'
},
},
fn: async function ({vantaSourceId, state, vantaAuthorizationRequestURL, redirectAfterSetup}) {
// Find the VantaConnection record that we created when the user created this request.
let recordOfThisAuthorization = await VantaConnection.findOne({vantaSourceId: vantaSourceId});
// If no record of this authorization could be found, return a noMatchingVantaConnection response.
if(!recordOfThisAuthorization){
throw 'noMatchingVantaConnection';
}
// Set a 'state' and 'vantaSourceId' cookie on the users browser.
this.res.cookie('redirectAfterSetup', redirectAfterSetup, {signed: true});
this.res.cookie('state', state, {signed: true});
this.res.cookie('vantaSourceId', vantaSourceId, {signed: true});
// now that the user has the required cookies to complete the vanta integration setup, redirect them to the provided VantaAuthorizationUrl.
return this.res.redirect(vantaAuthorizationRequestURL);
}
};

View file

@ -89,7 +89,9 @@ module.exports = {
if(!updatedRecord){
throw new Error(`When trying to update a VantaConnection record (id: ${recordOfThisAuthorization.id}) with an authorization token from Vanta, the database record associated with this request has gone missing.`);
}
if(this.req.signedCookies.redirectAfterSetup){
return this.res.redirect(this.req.signedCookies.redirectAfterSetup);
}
return {
showSuccessMessage: true
};

View file

@ -48,7 +48,7 @@
"moment": true,
"docsearch": true,
"Chart": true,
// "google": true,
"gtag": true,
// ...etc.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View file

@ -80,13 +80,18 @@ parasails.registerPage('contact', {
methods: {
submittedContactForm: async function() {
if(typeof gtag !== 'undefined'){
gtag('event','website_contact_forms');
}
// Show the success message.
this.cloudSuccess = true;
},
submittedTalkToUsForm: async function() {
this.syncing = true;
if(typeof gtag !== 'undefined'){
gtag('event','website_contact_forms');
}
if(this.formData.numberOfHosts > 700){
this.goto(`https://calendly.com/fleetdm/talk-to-us?email=${encodeURIComponent(this.formData.emailAddress)}&name=${encodeURIComponent(this.formData.firstName+' '+this.formData.lastName)}`);
} else {

View file

@ -222,6 +222,13 @@ parasails.registerPage('basic-documentation', {
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
clickSwagRequestCTA: function () {
if(typeof gtag !== 'undefined') {
gtag('event','website_swag_request');
}
this.goto('https://kqphpqst851.typeform.com/to/ZfA3sOu0');
},
clickCTA: function (slug) {
this.goto(slug);
},

View file

@ -67,6 +67,9 @@ parasails.registerPage('signup', {
// redirect to the /start page.
// > (Note that we re-enable the syncing state here. This is on purpose--
// > to make sure the spinner stays there until the page navigation finishes.)
if(typeof gtag !== 'undefined'){
gtag('event','website_sign_up');
}
this.syncing = true;
this.goto(this.pageToRedirectToAfterRegistration);// « / start if the user came here from the start now button, or customers/new-license if the user came here from the "Get your license" link.
}

View file

@ -55,4 +55,5 @@ module.exports.policies = {
'deliver-talk-to-us-form-submission': true,
'get-human-interpretation-from-osquery-sql': true,
'customers/view-new-license': true,
'redirect-vanta-authorization-request': true,
};

View file

@ -589,7 +589,8 @@ module.exports.routes = {
'POST /api/v1/create-or-update-one-newsletter-subscription': { action: 'create-or-update-one-newsletter-subscription' },
'/api/v1/unsubscribe-from-all-newsletters': { action: 'unsubscribe-from-all-newsletters' },
'POST /api/v1/admin/build-license-key': { action: 'admin/build-license-key' },
'POST /api/v1/create-vanta-authorization-request': { action: 'create-vanta-authorization-request' },
'POST /api/v1/create-vanta-authorization-request': { action: 'create-vanta-authorization-request', csrf: false },
'GET /redirect-vanta-authorization-request': { action: 'redirect-vanta-authorization-request' },
'POST /api/v1/deliver-mdm-beta-signup': { action: 'deliver-mdm-beta-signup' },
'POST /api/v1/get-human-interpretation-from-osquery-sql': { action: 'get-human-interpretation-from-osquery-sql', csrf: false },
'POST /api/v1/deliver-apple-csr ': { action: 'deliver-apple-csr', csrf: false},

View file

@ -157,7 +157,7 @@
<a class="pb-3" target="_blank" href="https://github.com/fleetdm/fleet/releases">Releases</a>
<a class="pb-3" target="_blank" href="/support">Support</a>
<div class="d-none d-lg-block" purpose="swag-cta" v-if="showSwagForm">
<a class="d-flex align-items-center justify-content-center" href="https://kqphpqst851.typeform.com/to/ZfA3sOu0" target="_blank">
<a class="d-flex align-items-center justify-content-center" @click="clickSwagRequestCTA()">
<div class="d-flex flex-column align-items-center">
<img style="height: auto; width: 47px; margin-bottom: 8px;" alt="A very nice Fleet branded shirt" src="/images/fleet-shirt-60x55@2x.png">
<p class="mb-0">Request Fleet swag</p>