mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Merge branch 'main' into feat-byod-enrollment
This commit is contained in:
commit
694598b803
104 changed files with 3693 additions and 631 deletions
1
changes/18897-shoe-zeroes
Normal file
1
changes/18897-shoe-zeroes
Normal file
|
|
@ -0,0 +1 @@
|
|||
Added "0 items" description on empty software tables for UI consistency
|
||||
1
changes/20757-profiles-batch-activity
Normal file
1
changes/20757-profiles-batch-activity
Normal file
|
|
@ -0,0 +1 @@
|
|||
API endpoint `/api/v1/fleet/mdm/profiles/batch` will now not log an activity for profile types that did not change in the database (Apple configuration profiles, Windows configuration profiles, or Apple declarations).
|
||||
1
changes/21412-remove-node-key-from-server-logs
Normal file
1
changes/21412-remove-node-key-from-server-logs
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Removed invalid node keys from server logs.
|
||||
1
changes/21428-policy-automatic-install-software
Normal file
1
changes/21428-policy-automatic-install-software
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added automatic installation of software packages using policy automations.
|
||||
1
changes/21428-prevent-install-when-already-pending
Normal file
1
changes/21428-prevent-install-when-already-pending
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added validation to `POST /api/_version_/fleet/hosts/{host_id}/software/install/{software_title_id}` to prevent installing on a host that already has a pending installation for that software title.
|
||||
2
changes/21683-apns-cert-validation-on-start
Normal file
2
changes/21683-apns-cert-validation-on-start
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Removed validation of APNS certificate from server startup. This was no longer necessary because
|
||||
we now allow for APNS certificates to be renewed in the UI.
|
||||
|
|
@ -22,7 +22,6 @@ import (
|
|||
"github.com/e-dard/netbug"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/licensing"
|
||||
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
|
||||
"github.com/fleetdm/fleet/v4/pkg/certificate"
|
||||
"github.com/fleetdm/fleet/v4/pkg/scripts"
|
||||
"github.com/fleetdm/fleet/v4/server"
|
||||
configpkg "github.com/fleetdm/fleet/v4/server/config"
|
||||
|
|
@ -75,8 +74,10 @@ import (
|
|||
|
||||
var allowedURLPrefixRegexp = regexp.MustCompile("^(?:/[a-zA-Z0-9_.~-]+)+$")
|
||||
|
||||
const softwareInstallerUploadTimeout = 4 * time.Minute
|
||||
const liveQueryMemCacheDuration = 1 * time.Second
|
||||
const (
|
||||
softwareInstallerUploadTimeout = 4 * time.Minute
|
||||
liveQueryMemCacheDuration = 1 * time.Second
|
||||
)
|
||||
|
||||
type initializer interface {
|
||||
// Initialize is used to populate a datastore with
|
||||
|
|
@ -126,6 +127,10 @@ the way that the Fleet server works.
|
|||
|
||||
logger := initLogger(config)
|
||||
|
||||
if dev {
|
||||
createTestBucketForInstallers(&config, logger)
|
||||
}
|
||||
|
||||
// Init tracing
|
||||
if config.Logging.TracingEnabled {
|
||||
ctx := context.Background()
|
||||
|
|
@ -506,7 +511,7 @@ the way that the Fleet server works.
|
|||
initFatal(errors.New("inserting APNs and SCEP assets"), "missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
|
||||
}
|
||||
|
||||
apnsCert, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs()
|
||||
_, apnsCertPEM, apnsKeyPEM, err := config.MDM.AppleAPNs()
|
||||
if err != nil {
|
||||
initFatal(err, "validate Apple APNs certificate and key")
|
||||
}
|
||||
|
|
@ -516,18 +521,6 @@ the way that the Fleet server works.
|
|||
initFatal(err, "validate Apple SCEP certificate and key")
|
||||
}
|
||||
|
||||
const (
|
||||
apnsConnectionTimeout = 10 * time.Second
|
||||
apnsConnectionURL = "https://api.sandbox.push.apple.com"
|
||||
)
|
||||
|
||||
// check that the Apple APNs certificate is valid to connect to the API
|
||||
ctx, cancel := context.WithTimeout(context.Background(), apnsConnectionTimeout)
|
||||
if err := certificate.ValidateClientAuthTLSConnection(ctx, apnsCert, apnsConnectionURL); err != nil {
|
||||
initFatal(err, "validate authentication with Apple APNs certificate")
|
||||
}
|
||||
cancel()
|
||||
|
||||
err = ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{
|
||||
{Name: fleet.MDMAssetAPNSCert, Value: apnsCertPEM},
|
||||
{Name: fleet.MDMAssetAPNSKey, Value: apnsKeyPEM},
|
||||
|
|
@ -583,6 +576,8 @@ the way that the Fleet server works.
|
|||
// backfilled
|
||||
tok := &fleet.ABMToken{
|
||||
EncryptedToken: appleBM.EncryptedToken,
|
||||
// 2000-01-01 is our "zero value" for time
|
||||
RenewAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
_, err = ds.InsertABMToken(context.Background(), tok)
|
||||
if err != nil {
|
||||
|
|
@ -1404,3 +1399,18 @@ var _ push.Pusher = nopPusher{}
|
|||
func (n nopPusher) Push(context.Context, []string) (map[string]*push.Response, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func createTestBucketForInstallers(config *configpkg.FleetConfig, logger log.Logger) {
|
||||
store, err := s3.NewSoftwareInstallerStore(config.S3)
|
||||
if err != nil {
|
||||
initFatal(err, "initializing S3 software installer store")
|
||||
}
|
||||
if err := store.CreateTestBucket(config.S3.SoftwareInstallersBucket); err != nil {
|
||||
// Don't panic, allow devs to run Fleet without minio/S3 dependency.
|
||||
level.Info(logger).Log(
|
||||
"err", err,
|
||||
"msg", "failed to create test bucket",
|
||||
"name", config.S3.SoftwareInstallersBucket,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,12 +167,15 @@ func TestApplyTeamSpecs(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
|
||||
return nil
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
|
||||
winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
ds.NewActivityFunc = func(
|
||||
|
|
@ -627,8 +630,9 @@ func TestApplyAppConfig(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
ds.LabelIDsByNameFunc = func(ctx context.Context, labels []string) (map[string]uint, error) {
|
||||
|
|
@ -1250,11 +1254,14 @@ func TestApplyAsGitOps(t *testing.T) {
|
|||
teamEnrollSecrets = secrets
|
||||
return nil
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
|
||||
return nil
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
|
||||
winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
|
||||
return nil, ¬FoundError{}
|
||||
|
|
|
|||
|
|
@ -2271,11 +2271,14 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
|
|||
}
|
||||
return nil, fmt.Errorf("team not found: %s", name)
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
|
||||
return nil
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
|
||||
winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -49,13 +49,13 @@ func TestBasicGlobalFreeGitOps(t *testing.T) {
|
|||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
||||
macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
return nil
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
||||
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
||||
) error {
|
||||
return nil
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
|
||||
ds.NewActivityFunc = func(
|
||||
|
|
@ -166,13 +166,13 @@ func TestBasicGlobalPremiumGitOps(t *testing.T) {
|
|||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
||||
macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
return nil
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
||||
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
||||
) error {
|
||||
return nil
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
|
||||
ds.NewActivityFunc = func(
|
||||
|
|
@ -277,13 +277,13 @@ func TestBasicTeamGitOps(t *testing.T) {
|
|||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
return nil
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
||||
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
||||
) error {
|
||||
return nil
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
|
|
@ -450,13 +450,14 @@ func TestFullGlobalGitOps(t *testing.T) {
|
|||
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
appliedMacProfiles = macProfiles
|
||||
appliedWinProfiles = winProfiles
|
||||
return nil
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return job, nil
|
||||
|
|
@ -625,13 +626,14 @@ func TestFullTeamGitOps(t *testing.T) {
|
|||
var appliedWinProfiles []*fleet.MDMWindowsConfigProfile
|
||||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
appliedMacProfiles = macProfiles
|
||||
appliedWinProfiles = winProfiles
|
||||
return nil
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return job, nil
|
||||
|
|
@ -927,10 +929,10 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) {
|
|||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
||||
macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
assert.Empty(t, macProfiles)
|
||||
assert.Empty(t, winProfiles)
|
||||
return nil
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error {
|
||||
assert.Empty(t, scripts)
|
||||
|
|
@ -938,9 +940,9 @@ func TestBasicGlobalAndTeamGitOps(t *testing.T) {
|
|||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
||||
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
||||
) error {
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
assert.Empty(t, profileUUIDs)
|
||||
return nil
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
|
||||
return nil
|
||||
|
|
@ -1666,14 +1668,14 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
|
|||
ds.BatchSetMDMProfilesFunc = func(
|
||||
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
|
||||
macDecls []*fleet.MDMAppleDeclaration,
|
||||
) error {
|
||||
return nil
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) error { return nil }
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(
|
||||
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
|
||||
) error {
|
||||
return nil
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -43,8 +43,9 @@ func TestHostsTransferByHosts(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) {
|
||||
|
|
@ -114,8 +115,9 @@ func TestHostsTransferByLabel(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) {
|
||||
|
|
@ -184,8 +186,9 @@ func TestHostsTransferByStatus(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) {
|
||||
|
|
@ -243,8 +246,9 @@ func TestHostsTransferByStatusAndSearchQuery(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hostIDs []uint) ([]string, error) {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ The same is not true if S3 is used as the storage backend. In that scenario, it
|
|||
|
||||
### Alternative carving backends
|
||||
|
||||
#### Minio
|
||||
#### MinIO
|
||||
|
||||
Configure the following:
|
||||
- `FLEET_S3_ENDPOINT_URL=minio_host:port`
|
||||
|
|
@ -87,6 +87,11 @@ Configure the following:
|
|||
- `FLEET_S3_FORCE_S3_PATH_STYLE=true`
|
||||
- `FLEET_S3_REGION=minio` or any non-empty string otherwise Fleet will attempt to derive the region.
|
||||
|
||||
If you're testing file carving locally with the docker-compose environment, the `--dev` flag on Fleet server will
|
||||
automatically point carves to the local MinIO container and write to the `carves-dev` bucket without needing to set
|
||||
additional configuration. Note that this bucket is *not* created automatically when bringing MinIO up; you'll need to
|
||||
log in via `http://localhost:9001` with credentials `minio` / `minio123!` to create the bucket.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Check carve status in osquery
|
||||
|
|
|
|||
|
|
@ -489,7 +489,9 @@ FLEET_SERVER_SANDBOX_ENABLED=1 FLEET_PACKAGING_GLOBAL_ENROLL_SECRET=xyz ./build
|
|||
Be sure to replace the `FLEET_PACKAGING_GLOBAL_ENROLL_SECRET` value above with the global enroll
|
||||
secret from the `fleetctl package` command used to build the installers.
|
||||
|
||||
MinIO also offers a web interface at http://localhost:9001. Credentials are `minio` / `minio123!`.
|
||||
MinIO also offers a web interface at http://localhost:9001. Credentials are `minio` / `minio123!`. When starting the
|
||||
Fleet server up with `--dev` the server will look for installers in the `software-installers-dev` MinIO bucket. You can
|
||||
create this bucket via the MinIO web UI (it is *not* created by default when setting up the docker-compose environment).
|
||||
|
||||
## Telemetry
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ Render is a cloud hosting service that makes it easy to get up and running fast,
|
|||
</a>
|
||||
</div>
|
||||
|
||||
1. Click "Deploy to Render" to open the Fleet Blueprint on Render. You will be prompted to create or log in to your Render account with associated payment information.
|
||||
1. Click "Deploy to Render" to open the Fleet Blueprint on Render. Ensure that the Redis instance is manually set to the same region as your other resources. You will be prompted to create or log in to your Render account with associated payment information.
|
||||
|
||||
2. Give the Blueprint a unique name like `yourcompany-fleet`.
|
||||
|
||||
|
|
|
|||
|
|
@ -1072,7 +1072,7 @@ func (svc *Service) mdmAppleEditedAppleOSUpdates(ctx context.Context, teamID *ui
|
|||
// This only sets profiles that haven't been queued by the cron to 'pending' (both removes and installs, which includes
|
||||
// the OS updates we just deleted). It doesn't have a functional difference because if you don't call this function
|
||||
// the cron will catch up, but it's important for the UX to mark them as pending immediately so it's reflected in the UI.
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{globalOrTeamID}, nil, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{globalOrTeamID}, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
return nil
|
||||
|
|
@ -1105,7 +1105,7 @@ func (svc *Service) mdmAppleEditedAppleOSUpdates(ctx context.Context, teamID *ui
|
|||
return err
|
||||
}
|
||||
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host declarations")
|
||||
}
|
||||
return nil
|
||||
|
|
@ -1271,10 +1271,13 @@ func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOS
|
|||
// validate the team IDs
|
||||
|
||||
token.MacOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam}
|
||||
token.MacOSDefaultTeamID = nil
|
||||
token.IOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam}
|
||||
token.IOSDefaultTeamID = nil
|
||||
token.IPadOSTeam = fleet.ABMTokenTeam{Name: fleet.TeamNameNoTeam}
|
||||
token.IPadOSDefaultTeamID = nil
|
||||
|
||||
if macOSTeamID != nil {
|
||||
if macOSTeamID != nil && *macOSTeamID != 0 {
|
||||
macOSTeam, err := svc.ds.Team(ctx, *macOSTeamID)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
|
|
@ -1288,7 +1291,7 @@ func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOS
|
|||
token.MacOSDefaultTeamID = macOSTeamID
|
||||
}
|
||||
|
||||
if iOSTeamID != nil {
|
||||
if iOSTeamID != nil && *iOSTeamID != 0 {
|
||||
iOSTeam, err := svc.ds.Team(ctx, *iOSTeamID)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
|
|
@ -1301,7 +1304,7 @@ func (svc *Service) UpdateABMTokenTeams(ctx context.Context, tokenID uint, macOS
|
|||
token.IOSDefaultTeamID = iOSTeamID
|
||||
}
|
||||
|
||||
if iPadOSTeamID != nil {
|
||||
if iPadOSTeamID != nil && *iPadOSTeamID != 0 {
|
||||
iPadOSTeam, err := svc.ds.Team(ctx, *iPadOSTeamID)
|
||||
if err != nil {
|
||||
return nil, &fleet.BadRequestError{
|
||||
|
|
|
|||
|
|
@ -200,8 +200,9 @@ func TestGetOrCreatePreassignTeam(t *testing.T) {
|
|||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes()
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet.
|
|||
if !ok {
|
||||
return fleet.ErrNoContext
|
||||
}
|
||||
payload.UserID = vc.UserID()
|
||||
|
||||
// make sure all scripts use unix-style newlines to prevent errors when
|
||||
// running them, browsers use windows-style newlines, which breaks the
|
||||
|
|
@ -384,6 +385,24 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
|
|||
|
||||
// if we found an installer, use that
|
||||
if installer != nil {
|
||||
lastInstallRequest, err := svc.ds.GetHostLastInstallData(ctx, host.ID, installer.InstallerID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "getting last install data for host %d and installer %d", host.ID, installer.InstallerID)
|
||||
}
|
||||
if lastInstallRequest != nil && lastInstallRequest.Status != nil && *lastInstallRequest.Status == fleet.SoftwareInstallerPending {
|
||||
return &fleet.BadRequestError{
|
||||
Message: "Couldn't install software. Host has a pending install request.",
|
||||
InternalErr: ctxerr.WrapWithData(
|
||||
ctx, err, "host already has a pending install for this installer",
|
||||
map[string]any{
|
||||
"host_id": host.ID,
|
||||
"software_installer_id": installer.InstallerID,
|
||||
"team_id": host.TeamID,
|
||||
"title_id": softwareTitleID,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
return svc.installSoftwareTitleUsingInstaller(ctx, host, installer)
|
||||
}
|
||||
}
|
||||
|
|
@ -629,6 +648,14 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
|||
}
|
||||
return "", ctxerr.Wrap(ctx, err, "extracting metadata from installer")
|
||||
}
|
||||
|
||||
if meta.Version == "" {
|
||||
return "", &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("Couldn't add. Fleet couldn't read the version from %s.", payload.Filename),
|
||||
InternalErr: ctxerr.New(ctx, "extracting version from installer metadata"),
|
||||
}
|
||||
}
|
||||
|
||||
payload.Title = meta.Name
|
||||
if payload.Title == "" {
|
||||
// use the filename if no title from metadata
|
||||
|
|
@ -686,6 +713,11 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin
|
|||
return ctxerr.Wrap(ctx, err, "validating authorization")
|
||||
}
|
||||
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return fleet.ErrNoContext
|
||||
}
|
||||
|
||||
g, workerCtx := errgroup.WithContext(ctx)
|
||||
g.SetLimit(3)
|
||||
// critical to avoid data race, the slice is pre-allocated and each
|
||||
|
|
@ -762,6 +794,7 @@ func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName strin
|
|||
PostInstallScript: p.PostInstallScript,
|
||||
InstallerFile: bytes.NewReader(bodyBytes),
|
||||
SelfService: p.SelfService,
|
||||
UserID: vc.UserID(),
|
||||
}
|
||||
|
||||
// set the filename before adding metadata, as it is used as fallback
|
||||
|
|
|
|||
|
|
@ -612,7 +612,7 @@ func (svc *Service) DeleteTeam(ctx context.Context, teamID uint) error {
|
|||
}
|
||||
|
||||
if len(hostIDs) > 0 {
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, hostIDs, nil, nil, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, hostIDs, nil, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import {
|
|||
ISoftwareVersionsResponse,
|
||||
ISoftwareVersionResponse,
|
||||
} from "services/entities/software";
|
||||
import { IOSVersionsResponse } from "../services/entities/operating_systems";
|
||||
import { IOperatingSystemVersion } from "../interfaces/operating_system";
|
||||
|
||||
const DEFAULT_SOFTWARE_MOCK: ISoftware = {
|
||||
hosts_count: 1,
|
||||
|
|
@ -93,12 +95,48 @@ const DEFAULT_SOFTWARE_VERSIONS_RESPONSE_MOCK: ISoftwareVersionsResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const createMockSoftwareVersionsReponse = (
|
||||
export const createMockSoftwareVersionsResponse = (
|
||||
overrides?: Partial<ISoftwareVersionsResponse>
|
||||
): ISoftwareVersionsResponse => {
|
||||
return { ...DEFAULT_SOFTWARE_VERSIONS_RESPONSE_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
const DEFAULT_OS_VERSION_MOCK = {
|
||||
os_version_id: 1,
|
||||
name: "macOS 14.6.1",
|
||||
name_only: "macOS",
|
||||
version: "14.6.1",
|
||||
platform: "darwin",
|
||||
hosts_count: 42,
|
||||
generated_cpes: [],
|
||||
vulnerabilities: [],
|
||||
};
|
||||
|
||||
export const createMockOSVersion = (
|
||||
overrides?: Partial<IOperatingSystemVersion>
|
||||
): IOperatingSystemVersion => {
|
||||
return {
|
||||
...DEFAULT_OS_VERSION_MOCK,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_OS_VERSIONS_RESPONSE_MOCK: IOSVersionsResponse = {
|
||||
counts_updated_at: "2020-01-01T00:00:00.000Z",
|
||||
count: 1,
|
||||
os_versions: [createMockOSVersion()],
|
||||
meta: {
|
||||
has_next_results: false,
|
||||
has_previous_results: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const createMockOSVersionsResponse = (
|
||||
overrides?: Partial<IOSVersionsResponse>
|
||||
): IOSVersionsResponse => {
|
||||
return { ...DEFAULT_OS_VERSIONS_RESPONSE_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
const DEFAULT_APP_STORE_APP_MOCK: IAppStoreApp = {
|
||||
name: "test app",
|
||||
app_store_id: 1,
|
||||
|
|
@ -208,7 +246,7 @@ const DEFAULT_SOFTWARE_TITLES_RESPONSE_MOCK: ISoftwareTitlesResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const createMockSoftwareTitlesReponse = (
|
||||
export const createMockSoftwareTitlesResponse = (
|
||||
overrides?: Partial<ISoftwareTitlesResponse>
|
||||
): ISoftwareTitlesResponse => {
|
||||
return { ...DEFAULT_SOFTWARE_TITLES_RESPONSE_MOCK, ...overrides };
|
||||
|
|
|
|||
|
|
@ -41,16 +41,19 @@ const PREMIUM_ACTIVITIES = new Set([
|
|||
|
||||
const getProfileMessageSuffix = (
|
||||
isPremiumTier: boolean,
|
||||
platform: "apple" | "windows",
|
||||
teamName?: string | null
|
||||
) => {
|
||||
let messageSuffix = <>hosts</>;
|
||||
const platformDisplayName =
|
||||
platform === "apple" ? "macOS, iOS, and iPadOS" : "Windows";
|
||||
let messageSuffix = <>all {platformDisplayName} hosts</>;
|
||||
if (isPremiumTier) {
|
||||
messageSuffix = teamName ? (
|
||||
<>
|
||||
the <b>{teamName}</b> team
|
||||
{platformDisplayName} hosts assigned to the <b>{teamName}</b> team
|
||||
</>
|
||||
) : (
|
||||
<>hosts with no team</>
|
||||
<>{platformDisplayName} hosts with no team</>
|
||||
);
|
||||
}
|
||||
return messageSuffix;
|
||||
|
|
@ -364,7 +367,12 @@ const TAGGED_TEMPLATES = {
|
|||
) : (
|
||||
<>a configuration profile</>
|
||||
)}{" "}
|
||||
to {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}
|
||||
to{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"apple",
|
||||
activity.details?.team_name
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
|
|
@ -383,7 +391,12 @@ const TAGGED_TEMPLATES = {
|
|||
<>a configuration profile</>
|
||||
)}{" "}
|
||||
from{" "}
|
||||
{getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}.
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"apple",
|
||||
activity.details?.team_name
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
@ -394,6 +407,7 @@ const TAGGED_TEMPLATES = {
|
|||
edited configuration profiles for{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"apple",
|
||||
activity.details?.team_name
|
||||
)}{" "}
|
||||
via fleetctl.
|
||||
|
|
@ -413,7 +427,12 @@ const TAGGED_TEMPLATES = {
|
|||
) : (
|
||||
<>a configuration profile</>
|
||||
)}{" "}
|
||||
to {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}
|
||||
to{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"windows",
|
||||
activity.details?.team_name
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
|
|
@ -432,7 +451,12 @@ const TAGGED_TEMPLATES = {
|
|||
<>a configuration profile</>
|
||||
)}{" "}
|
||||
from{" "}
|
||||
{getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}.
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"windows",
|
||||
activity.details?.team_name
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
@ -443,6 +467,7 @@ const TAGGED_TEMPLATES = {
|
|||
edited configuration profiles for{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"windows",
|
||||
activity.details?.team_name
|
||||
)}{" "}
|
||||
via fleetctl.
|
||||
|
|
@ -762,7 +787,12 @@ const TAGGED_TEMPLATES = {
|
|||
added declaration (DDM) profile <b>
|
||||
{activity.details?.profile_name}
|
||||
</b>{" "}
|
||||
to {getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}
|
||||
to{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"apple",
|
||||
activity.details?.team_name
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
|
|
@ -773,7 +803,12 @@ const TAGGED_TEMPLATES = {
|
|||
{" "}
|
||||
removed declaration (DDM) profile{" "}
|
||||
<b>{activity.details?.profile_name}</b> from{" "}
|
||||
{getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}.
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"apple",
|
||||
activity.details?.team_name
|
||||
)}
|
||||
.
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
@ -783,7 +818,11 @@ const TAGGED_TEMPLATES = {
|
|||
{" "}
|
||||
edited declaration (DDM) profiles{" "}
|
||||
<b>{activity.details?.profile_name}</b> for{" "}
|
||||
{getProfileMessageSuffix(isPremiumTier, activity.details?.team_name)}{" "}
|
||||
{getProfileMessageSuffix(
|
||||
isPremiumTier,
|
||||
"apple",
|
||||
activity.details?.team_name
|
||||
)}{" "}
|
||||
via fleetctl.
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react";
|
|||
import OSTable from "./OSTable";
|
||||
|
||||
describe("Dashboard OS table", () => {
|
||||
it("renders data normally when present", async () => {
|
||||
it("renders data normally when present", () => {
|
||||
render(
|
||||
<OSTable
|
||||
currentTeamId={undefined}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import { createMockOSVersionsResponse } from "__mocks__/softwareMock";
|
||||
|
||||
import SoftwareOSTable from "./SoftwareOSTable";
|
||||
|
||||
// TODO: figure out how to mock the router properly.
|
||||
const mockRouter = {
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
goBack: jest.fn(),
|
||||
goForward: jest.fn(),
|
||||
go: jest.fn(),
|
||||
setRouteLeaveHook: jest.fn(),
|
||||
isActive: jest.fn(),
|
||||
createHref: jest.fn(),
|
||||
createPath: jest.fn(),
|
||||
};
|
||||
|
||||
describe("Software operating systems table", () => {
|
||||
it("Renders the page-wide disabled state when software inventory is disabled", async () => {
|
||||
render(
|
||||
<SoftwareOSTable
|
||||
router={mockRouter}
|
||||
isSoftwareEnabled={false} // Set to false
|
||||
data={createMockOSVersionsResponse({
|
||||
count: 0,
|
||||
os_versions: [],
|
||||
})}
|
||||
perPage={20}
|
||||
orderDirection="asc"
|
||||
orderKey="hosts_count"
|
||||
currentPage={0}
|
||||
teamId={1}
|
||||
isLoading={false}
|
||||
resetPageIndex={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Software inventory disabled")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders the page-wide empty state when no software is present", () => {
|
||||
render(
|
||||
<SoftwareOSTable
|
||||
router={mockRouter}
|
||||
isSoftwareEnabled
|
||||
data={createMockOSVersionsResponse({
|
||||
count: 0,
|
||||
os_versions: [],
|
||||
})}
|
||||
perPage={20}
|
||||
orderDirection="asc"
|
||||
orderKey="hosts_count"
|
||||
currentPage={0}
|
||||
teamId={1}
|
||||
isLoading={false}
|
||||
resetPageIndex={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("No operating systems detected")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("0 items")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Search")).toBeNull();
|
||||
expect(screen.queryByText("Updated")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -129,7 +129,7 @@ const SoftwareOSTable = ({
|
|||
};
|
||||
|
||||
const renderSoftwareCount = () => {
|
||||
if (!data?.os_versions || !data?.count) return null;
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { createCustomRenderer } from "test/test-utils";
|
|||
|
||||
import createMockUser from "__mocks__/userMock";
|
||||
import {
|
||||
createMockSoftwareTitlesReponse,
|
||||
createMockSoftwareVersionsReponse,
|
||||
createMockSoftwareTitlesResponse,
|
||||
createMockSoftwareVersionsResponse,
|
||||
} from "__mocks__/softwareMock";
|
||||
import { noop } from "lodash";
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ const mockRouter = {
|
|||
};
|
||||
|
||||
describe("Software table", () => {
|
||||
it("Renders the page-wide disabled state when software inventory is disabled", async () => {
|
||||
it("Renders the page-wide disabled state when software inventory is disabled", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -40,7 +40,7 @@ describe("Software table", () => {
|
|||
router={mockRouter}
|
||||
isSoftwareEnabled={false} // Set to false
|
||||
showVersions={false}
|
||||
data={createMockSoftwareTitlesReponse({
|
||||
data={createMockSoftwareTitlesResponse({
|
||||
counts_updated_at: null,
|
||||
software_titles: [],
|
||||
})}
|
||||
|
|
@ -68,7 +68,7 @@ describe("Software table", () => {
|
|||
expect(screen.queryByText("Vulnerability")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders the page-wide empty state when no software are present", async () => {
|
||||
it("Renders the page-wide empty state when no software are present", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -83,7 +83,8 @@ describe("Software table", () => {
|
|||
router={mockRouter}
|
||||
isSoftwareEnabled
|
||||
showVersions={false}
|
||||
data={createMockSoftwareTitlesReponse({
|
||||
data={createMockSoftwareTitlesResponse({
|
||||
count: 0,
|
||||
counts_updated_at: null,
|
||||
software_titles: [],
|
||||
})}
|
||||
|
|
@ -111,11 +112,12 @@ describe("Software table", () => {
|
|||
expect(
|
||||
screen.getByText("Expecting to see software? Check back later.")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("0 items")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Search")).toBeNull();
|
||||
expect(screen.queryByText("Updated")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders the page-wide empty state when search query does not exist but versions toggle is applied", async () => {
|
||||
it("Renders the page-wide empty state when search query does not exist but versions toggle is applied", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -130,7 +132,7 @@ describe("Software table", () => {
|
|||
router={mockRouter}
|
||||
isSoftwareEnabled
|
||||
showVersions // Versions toggle applied
|
||||
data={createMockSoftwareVersionsReponse({
|
||||
data={createMockSoftwareVersionsResponse({
|
||||
counts_updated_at: null,
|
||||
software: [],
|
||||
})}
|
||||
|
|
@ -160,7 +162,7 @@ describe("Software table", () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders the empty search state when search query does not exist but dropdown is applied", async () => {
|
||||
it("Renders the empty search state when search query does not exist but dropdown is applied", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -175,7 +177,7 @@ describe("Software table", () => {
|
|||
router={mockRouter}
|
||||
isSoftwareEnabled
|
||||
showVersions={false}
|
||||
data={createMockSoftwareTitlesReponse({
|
||||
data={createMockSoftwareTitlesResponse({
|
||||
counts_updated_at: null,
|
||||
software_titles: [],
|
||||
})}
|
||||
|
|
@ -209,7 +211,7 @@ describe("Software table", () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Renders the empty search state when search query does not exist but vulnerability filter is applied", async () => {
|
||||
it("Renders the empty search state when search query does not exist but vulnerability filter is applied", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -224,7 +226,7 @@ describe("Software table", () => {
|
|||
router={mockRouter}
|
||||
isSoftwareEnabled
|
||||
showVersions={false}
|
||||
data={createMockSoftwareTitlesReponse({
|
||||
data={createMockSoftwareTitlesResponse({
|
||||
counts_updated_at: null,
|
||||
software_titles: [],
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ const SoftwareTable = ({
|
|||
};
|
||||
|
||||
const renderSoftwareCount = () => {
|
||||
if (!tableData || !data?.count) return null;
|
||||
if (!tableData || !data) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const mockRouter = {
|
|||
};
|
||||
|
||||
describe("Software Vulnerabilities table", () => {
|
||||
it("Renders the page-wide disabled state when software inventory is disabled", async () => {
|
||||
it("Renders the page-wide disabled state when software inventory is disabled", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -62,7 +62,7 @@ describe("Software Vulnerabilities table", () => {
|
|||
});
|
||||
|
||||
// TODO: Reinstate collecting software view
|
||||
it("Renders the page-wide empty state when no software vulnerabilities are present", async () => {
|
||||
it("Renders the page-wide empty state when no software vulnerabilities are present", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -97,13 +97,14 @@ describe("Software Vulnerabilities table", () => {
|
|||
);
|
||||
|
||||
expect(screen.getByText("No vulnerabilities detected")).toBeInTheDocument();
|
||||
expect(screen.getByText("0 items")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Expecting to see vulnerabilities? Check back later.")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("Vulnerability")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders the empty search state when search query does not exist but exploited vulnerabilities dropdown is applied", async () => {
|
||||
it("Renders the empty search state when search query does not exist but exploited vulnerabilities dropdown is applied", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -145,7 +146,7 @@ describe("Software Vulnerabilities table", () => {
|
|||
expect(screen.queryByText("Vulnerability")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders the invalid CVE empty search state when search query wrapped in quotes is invalid with no results", async () => {
|
||||
it("Renders the invalid CVE empty search state when search query wrapped in quotes is invalid with no results", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -188,7 +189,7 @@ describe("Software Vulnerabilities table", () => {
|
|||
expect(screen.queryByText("Vulnerability")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders the valid known CVE empty search state when search query wrapped in quotes is valid known CVE with no results", async () => {
|
||||
it("Renders the valid known CVE empty search state when search query wrapped in quotes is valid known CVE with no results", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -233,7 +234,7 @@ describe("Software Vulnerabilities table", () => {
|
|||
expect(screen.queryByText("Vulnerability")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders the valid unknown CVE empty search state when search query wrapped in quotes is not a valid known CVE with no results", async () => {
|
||||
it("Renders the valid unknown CVE empty search state when search query wrapped in quotes is not a valid known CVE with no results", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
@ -276,7 +277,7 @@ describe("Software Vulnerabilities table", () => {
|
|||
expect(screen.queryByText("Vulnerability")).toBeNull();
|
||||
});
|
||||
|
||||
it("Renders premium columns", async () => {
|
||||
it("Renders premium columns", () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
|
|
|
|||
|
|
@ -197,9 +197,9 @@ const SoftwareVulnerabilitiesTable = ({
|
|||
};
|
||||
|
||||
const renderVulnerabilityCount = () => {
|
||||
if (!data?.count) return null;
|
||||
if (!data) return null;
|
||||
|
||||
const count = data.count;
|
||||
const count = data?.count;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -38,11 +38,11 @@ const EnableVppCard = () => {
|
|||
<b>Volume Purchasing Program (VPP) isn't enabled</b>
|
||||
</p>
|
||||
<p className={`${baseClass}__enable-vpp-description`}>
|
||||
To add App Store apps, first enable VPP.
|
||||
To add App Store apps, first add VPP.
|
||||
</p>
|
||||
<CustomLink
|
||||
url={PATHS.ADMIN_INTEGRATIONS_VPP}
|
||||
text="Enable VPP"
|
||||
text="Add VPP"
|
||||
className={`${baseClass}__enable-vpp-link`}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ const EditTeamsVppModal = ({
|
|||
showArrow
|
||||
tipContent={
|
||||
<div className={`${baseClass}__tooltip--all-teams`}>
|
||||
You can’t choose teams because you already have a VPP token
|
||||
You can't choose teams because you already have a VPP token
|
||||
assigned to all teams. First, edit teams for that VPP token to
|
||||
choose teams here.
|
||||
</div>
|
||||
|
|
@ -223,6 +223,7 @@ const EditTeamsVppModal = ({
|
|||
placeholder="Search teams"
|
||||
value={selectedValue}
|
||||
label="Teams"
|
||||
className={`${baseClass}__vpp-dropdown`}
|
||||
wrapperClassName={`${baseClass}__form-field--vpp-teams ${
|
||||
isDropdownDisabled ? `${baseClass}__form-field--disabled` : ""
|
||||
}`}
|
||||
|
|
@ -230,7 +231,7 @@ const EditTeamsVppModal = ({
|
|||
isDropdownDisabled ? undefined : (
|
||||
<>
|
||||
Each team can have only one VPP token. Teams that already
|
||||
have a VPP token won’t show up here.
|
||||
have a VPP token won't show up here.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
.component__tooltip-wrapper__element {
|
||||
width: 100%; // default component style was causing the select box not to be full width
|
||||
}
|
||||
|
||||
// this is needed to wrap the selected team names in that are displayed
|
||||
// in the dropdown select box.
|
||||
.dropdown__select {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
// styles needed to make select look like figma design when disabled,
|
||||
// default styles in the Dropdown component were not enough
|
||||
&__form-field--disabled {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const AppleAutomaticEnrollmentCard = ({
|
|||
"Add an Apple Business Manager (ABM) connection to automatically enroll newly " +
|
||||
"purchased Apple hosts when they're first unboxed and set up by your end users.";
|
||||
} else if (isAppleMdmOn && configured) {
|
||||
msg = "Automatic enrollment for Apple (macOS, iOS, iPadOS) hosts enabled.";
|
||||
msg = "Automatic enrollment for Apple (macOS, iOS, iPadOS) is enabled.";
|
||||
icon = "success";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const VppCard = ({ isAppleMdmOn, isVppOn, router }: IVppCardProps) => {
|
|||
<p>
|
||||
<span>
|
||||
<Icon name="success" />
|
||||
Volume Purchasing Program (VPP) enabled.
|
||||
Volume Purchasing Program (VPP) is enabled.
|
||||
</span>
|
||||
</p>
|
||||
<Button onClick={navigateToVppSetup} variant="text-icon">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# 🛩️ Product groups
|
||||
|
||||
This page covers what all contributors (fleeties or not) need to know in order to contribute changes to [the core product](https://fleetdm.com/docs).
|
||||
|
||||
When creating software, handoffs between teams or contributors are one of the most common sources of miscommunication and waste. Like [GitLab](https://docs.google.com/document/d/1RxqS2nR5K0vN6DbgaBw7SEgpPLi0Kr9jXNGzpORT-OY/edit#heading=h.7sfw1n9c1i2t), Fleet uses product groups to minimize handoffs and maximize iteration and efficiency in the way we build the product.
|
||||
|
|
@ -6,10 +7,14 @@ When creating software, handoffs between teams or contributors are one of the mo
|
|||
> - Write down philosophies and show how the pieces of the development process fit together on this "🛩️ Product groups" page.
|
||||
> - Use the dedicated [departmental](https://fleetdm.com/handbook/company#org-chart) handbook pages for [🚀 Engineering](https://fleetdm.com/handbook/engineering) and [🦢 Product Design](https://fleetdm.com/handbook/product) to keep track of specific, rote responsibilities and recurring rituals designed to be read and used only by people within those departments.
|
||||
|
||||
|
||||
## Product roadmap
|
||||
|
||||
Fleet team members can read [Fleet's high-level product goals for the current quarter](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit?usp=sharing) (confidential Google Sheet).
|
||||
|
||||
|
||||
## What are product groups?
|
||||
|
||||
Fleet organizes product development efforts into separate, cross-functional product groups that include product designers, developers, and quality engineers. These product groups are organized by business goal, and designed to operate in parallel.
|
||||
|
||||
Security, performance, stability, scalability, database migrations, release compatibility, usage documentation (such as REST API and configuration reference), contributor experience, and support escalation are the responsibility of every product group.
|
||||
|
|
@ -18,6 +23,7 @@ At Fleet, [anyone can contribute](https://fleetdm.com/handbook/company#openness)
|
|||
|
||||
> Ideas expressed in wireframes, like code contributions, [are welcome from everyone](https://chat.osquery.io/c/fleet), inside or outside the company.
|
||||
|
||||
|
||||
## Current product groups
|
||||
|
||||
| Product group | Goal _(value for customers and/or community)_ | Capacity\* |
|
||||
|
|
@ -27,7 +33,9 @@ At Fleet, [anyone can contribute](https://fleetdm.com/handbook/company#openness)
|
|||
|
||||
\* The number of [estimated story points](https://fleetdm.com/handbook/company/communications#estimation-points) this group can take on per-sprint under ideal circumstances, used as a baseline number for planning and prioritizing user stories for drafting. In reality, capacity will vary as engineers are on-call, out-of-office, filling in for other product groups, etc.
|
||||
|
||||
|
||||
### Endpoint ops group
|
||||
|
||||
The goal of the endpoint ops group is to increase and exceed [Fleet's product maturity goals in the endpoint operations category](https://drive.google.com/file/d/11yQ_2WG7TbRErUpMBKWu_hQ5wRIZyQhr/view?usp=sharing).
|
||||
|
||||
| Responsibility | Human(s) |
|
||||
|
|
@ -40,7 +48,9 @@ The goal of the endpoint ops group is to increase and exceed [Fleet's product ma
|
|||
|
||||
> The [Slack channel](https://fleetdm.slack.com/archives/C01EZVBHFHU), [kanban release board](https://app.zenhub.com/workspaces/-g-endpoint-ops-current-sprint-63bd7e0bf75dba002a2343ac/board), and [GitHub label](https://github.com/fleetdm/fleet/issues?q=is%3Aopen+is%3Aissue+label%3A%23g-endpoint-ops) for this product group is `#g-endpoint-ops`.
|
||||
|
||||
|
||||
### MDM group
|
||||
|
||||
The goal of the MDM group is to increase and exceed [Fleet's product maturity goals](https://drive.google.com/file/d/11yQ_2WG7TbRErUpMBKWu_hQ5wRIZyQhr/view?usp=sharing) in the "MDM" product category.
|
||||
|
||||
| Responsibility | Human(s) |
|
||||
|
|
@ -53,7 +63,9 @@ The goal of the MDM group is to increase and exceed [Fleet's product maturity go
|
|||
|
||||
> The [Slack channel](https://fleetdm.slack.com/archives/C03C41L5YEL), [kanban release board](https://app.zenhub.com/workspaces/-g-mdm-current-sprint-63bc507f6558550011840298/board), and [GitHub label](https://github.com/fleetdm/fleet/issues?q=is%3Aopen+is%3Aissue+label%3A%23g-mdm) for this product group is `#g-mdm`.
|
||||
|
||||
|
||||
## Making changes
|
||||
|
||||
Fleet's highest product ambition is to create experiences that users want.
|
||||
|
||||
To deliver on this mission, we need a clear, repeatable process for turning an idea into a set of cohesively-designed changes in the product. We also need to allow [open source contributions](https://fleetdm.com/handbook/company#open-source) at any point in the process from the wider Fleet community - these won't necessarily follow this process.
|
||||
|
|
@ -65,14 +77,18 @@ To make a change to Fleet:
|
|||
- Then, it will be [drafted](https://fleetdm.com/handbook/company/product-groups#drafting) (planned).
|
||||
- Next, it will be [implemented](https://fleetdm.com/handbook/company/product-groups#implementing) and [released](https://fleetdm.com/handbook/engineering#release-process).
|
||||
|
||||
|
||||
### Planned and unplanned changes
|
||||
|
||||
Most changes to Fleet are planned changes. They are [prioritized](https://fleetdm.com/handbook/product), defined, designed, revised, estimated, and scheduled into a release sprint _prior to starting implementation_. The process of going from a prioritized goal to an estimated, scheduled, committed user story with a target release is called "drafting", or "the drafting phase".
|
||||
|
||||
Occasionally, changes are unplanned. Like a patch for an unexpected bug, or a hotfix for a security issue. Or if an open source contributor suggests an unplanned change in the form of a pull request. These unplanned changes are sometimes OK to merge as-is. But if they change the user interface, the CLI usage, or the REST API, then they need to go through drafting and reconsideration before merging.
|
||||
|
||||
> But wait, [isn't this "waterfall"?](https://about.gitlab.com/handbook/product-development-flow/#but-wait-isnt-this-waterfall) Waterfall is something else. Between 2015-2023, GitLab and The Sails Company independently developed and coevolved similar delivery processes. (What we call "drafting" and "implementation" at Fleet, is called "the validation phase" and "the build phase" at GitLab.)
|
||||
|
||||
|
||||
### Experimental features
|
||||
|
||||
When a new feature is introduced it may be labeled as experimental. Experimental features are undergoing a rapid [incremental improvement and iteration process](https://fleetdm.com/handbook/company/why-this-way#why-lean-software-development) where new learnings may requires breaking changes. When we introduce experimental features, it is important that any API endpoints or configuration surface that may change in the future be clearly labeled as experimental.
|
||||
|
||||
1. Apply the `~experimental` label to all associated user stories.
|
||||
|
|
@ -81,7 +97,9 @@ When a new feature is introduced it may be labeled as experimental. Experimental
|
|||
|
||||
> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
|
||||
|
||||
|
||||
### Breaking changes
|
||||
|
||||
For product changes that cause breaking API or configuration changes or major impact for users (or even just the _impression_ of major impact!), the company plans migration thoughtfully. If the feature was released as stable (not experimental), the product group and E-group:
|
||||
|
||||
1. **Written:** Write a migration guide.
|
||||
|
|
@ -92,12 +110,16 @@ For product changes that cause breaking API or configuration changes or major im
|
|||
|
||||
All of the steps above happen prior to any breaking changes to stable features being prioritized for implementation.
|
||||
|
||||
|
||||
#### API changes
|
||||
|
||||
To maintain consistency, ensure perspective, and provide a single pair of eyes in the design of Fleet's REST API and API documentation, there is a single Directly Responsible Individual (DRI). The API design DRI will review and approve any alterations at the pull request stage, instead of making it a prerequisite during drafting of the story. You may tag the DRI in a GitHub issue with draft API specs in place to receive a review and feedback prior to implementation. Receiving a pre-review from the DRI is encouraged if the API changes introduce new endpoints, or substantially change existing endpoints.
|
||||
|
||||
No API changes are merged without accompanying API documentation and approval from the DRI. The DRI is responsible for ensuring that the API design remains consistent and adequately addresses both standard and edge-case scenarios. The DRI is also the code owner of the API documentation Markdown file. The DRI is committed to reviewing PRs within one business day. In instances where the DRI is unavailable, the Head of Product will act as the substitute code owner and reviewer.
|
||||
|
||||
|
||||
#### Changes to tables' schema
|
||||
|
||||
Whenever a PR is proposed for making changes to our [tables' schema](https://fleetdm.com/tables/screenlock)(e.g. to schema/tables/screenlock.yml), it also has to be reflected in our osquery_fleet_schema.json file.
|
||||
|
||||
The website team will [periodically](https://fleetdm.com/handbook/marketing/website-handbook#rituals) update the json file with the latest changes. If the changes should be deployed sooner, you can generate the new json file yourself by running these commands:
|
||||
|
|
@ -110,14 +132,18 @@ cd website
|
|||
|
||||
> If a table is added to our ChromeOS extension but it does not exist in osquery or if it is a table added by fleetd, add a note that mentions it, as in this [example](https://github.com/fleetdm/fleet/blob/e95e075e77b683167e86d50960e3dc17045e3c44/schema/tables/mdm.yml#L2).
|
||||
|
||||
|
||||
### Drafting
|
||||
|
||||
"Drafting" is the art of defining a change, designing and shepherding it through the drafting process until it is ready for implementation.
|
||||
|
||||
The goal of drafting is to deliver software that works every time with less total effort and investment, without making contribution any less fun. By researching and iterating [prior to development](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-a-wireframe-first-approach), we design better product features, crystallize fewer bad, preemptive naming decisions, and achieve better throughput: getting more done in less time.
|
||||
|
||||
> Fleet's drafting process is focused first and foremost on product development, but it can be used for any kind of change that benefits from planning or a "dry run". For example, imagine you work for a business who has decided to swap out one of your payroll or device management vendors. You will probably need to plan and execute changes to a number of complicated onboarding/offboarding processes.
|
||||
|
||||
|
||||
#### Drafting process
|
||||
|
||||
The DRI for defining and drafting issues for a product group is the product manager, with close involvement from the designer and engineering manager. But drafting is a team effort, and all contributors participate.
|
||||
|
||||
A user story is considered ready for implementation once:
|
||||
|
|
@ -130,19 +156,25 @@ A user story is considered ready for implementation once:
|
|||
|
||||
> All user stories intended for the next sprint are estimated by the last estimation session before the sprint begins. This makes sure contributors have adequate time to complete the current sprint and provide accurate estimates for the next sprint.
|
||||
|
||||
|
||||
#### Writing a good user story
|
||||
|
||||
Good user stories are short, with clear, unambiguous language.
|
||||
- What screen are they looking at? (`As an observer on the host details page…`)
|
||||
- What do they want to do? (`As an observer on the host details page, I want to run a permitted query.`)
|
||||
- Don't get hung up on the "so that I can ________" clause. It is helpful, but optional.
|
||||
- Example: "As an admin I would like to be asked for confirmation before deleting a user so that I do not accidentally delete a user."
|
||||
|
||||
|
||||
#### Is it actually a story?
|
||||
|
||||
User stories are small and independently valuable.
|
||||
- Is it small enough? Will this task be likely to fit in 1 sprint when estimated?
|
||||
- Is it valuable enough? Will this task drive business value when released, independent of other tasks?
|
||||
|
||||
|
||||
#### Defining "done"
|
||||
|
||||
To successfully deliver a user story, the people working on it need to know what "done" means.
|
||||
|
||||
Since the goal of a user story is to implement certain changes to the product, the "definition of done" is written and maintained by the product manager. But ultimately, this "definition of done" involves everyone in the product group. We all collectively rely on accuracy of estimations, astuteness of designs, and cohesiveness of changes envisioned in order to deliver on time and without fuss.
|
||||
|
|
@ -163,7 +195,9 @@ Things to consider when writing the "definition of done" for a user story:
|
|||
- **QA:** Changes are tested by hand prior to submitting pull requests. In addition, quality assurance will do an extra QA check prior to considering this story "done". Any special QA notes?
|
||||
- **Follow-through:** Is there anything in particular that we should inform others (people who aren't in this product group) about after this user story is released? For example: communication to specific customers, tips on how best to highlight this in a release post, gotchas, etc.
|
||||
|
||||
|
||||
#### Providing context
|
||||
|
||||
User story issues contain an optional section called "Context".
|
||||
|
||||
This section is optional and hidden by default. It can be included or omitted, as time allows. As Fleet grows as an all-remote company with more asynchronous processes across timezones, we will rely on this section more and more.
|
||||
|
|
@ -181,7 +215,9 @@ Here are some examples of questions that might be helpful to answer:
|
|||
|
||||
These questions are helpful for the product team when considering what to prioritize. (The act of writing the answers is a lot of the value!) But these answers can also be helpful when users or contributors (including our future selves) have questions about how best to estimate, iterate, or refine.
|
||||
|
||||
|
||||
#### Initiate an air guitar session
|
||||
|
||||
Anyone in the product group can initiate an air guitar session.
|
||||
|
||||
1. Initiate: Create a user story and add the `~air-guitar` label to indicate that it is going through the air guitar process. Air guitar issues are always intended to be designed right away. If they can't be, the requestor is notified via at-mention in the issue (that person is either the CSM or AE).
|
||||
|
|
@ -205,9 +241,12 @@ Anyone in the product group can initiate an air guitar session.
|
|||
|
||||
Air guitar sessions are timeboxed to ensure they are fast and focused. Documentation from this process may inform future user stories and can be invaluable when revisiting the idea at a later stage. While the air guitar process is exploratory in nature, it should be thorough enough to provide meaningful insights and data for future decision-making.
|
||||
|
||||
|
||||
### Implementing
|
||||
|
||||
|
||||
#### Developing from wireframes
|
||||
|
||||
Please read carefully and [pay special attention](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-a-wireframe-first-approach) to UI wireframes.
|
||||
|
||||
Designs have usually gone through multiple rounds of revisions, but they could easily still be overlooking complexities or edge cases! When you think you've discovered a blocker, here's how to proceed:
|
||||
|
|
@ -226,7 +265,9 @@ At Fleet, we prioritize [iteration](https://fleetdm.com/handbook/company#results
|
|||
|
||||
After these considerations, if you still think you've found a blocker, alert the [appropriate PM](https://fleetdm.com/handbook/company/product-groups#current-product-groups) so that the user story can be brought back for [expedited drafting](https://fleetdm.com/handbook/product#expedited-drafting). Otherwise, make a [feature request](https://fleetdm.com/handbook/product#intake).
|
||||
|
||||
|
||||
#### Sub-tasks
|
||||
|
||||
The simplest way to manage work is to use a single user story issue, then pass it around between contributors/asignees as seldom as possible. But on a case-by-case basis, for particular user stories and teams, it can sometimes be worthwhile to invest additional overhead in creating separate **unestimated sub-task** issues ("sub-tasks").
|
||||
|
||||
A user story is estimated to fit within 1 sprint and drives business value when released, independent of other stories. Sub-tasks are not.
|
||||
|
|
@ -241,20 +282,28 @@ Sub-tasks:
|
|||
- are NOT the best place to post GitHub comments (instead, concentrate conversation in the top-level "user story" issue)
|
||||
- will NOT be looked at or QA'd by quality assurance
|
||||
|
||||
|
||||
## Outages
|
||||
|
||||
At Fleet, we consider an outage to be a situation where new features or previously stable features are broken or unusable.
|
||||
|
||||
- Occurences of outages are tracked in the [Outages](https://docs.google.com/spreadsheets/d/1a8rUk0pGlCPpPHAV60kCEUBLvavHHXbk_L3BI0ybME4/edit#gid=0) spreadsheet.
|
||||
- Fleet encourages embracing the inevitability of mistakes and discourages blame games.
|
||||
- Fleet stresses the critical importance of avoiding outages because they make customers' lives worse instead of better.
|
||||
|
||||
|
||||
## Scaling Fleet
|
||||
|
||||
Fleet, as a Go server, scales horizontally very well. It’s not very CPU or memory intensive. However, there are some specific gotchas to be aware of when implementing new features. Visit our [scaling Fleet page](https://fleetdm.com/handbook/engineering/scaling-fleet) for tips on scaling Fleet as efficiently and effectively as possible.
|
||||
|
||||
|
||||
## Load testing
|
||||
|
||||
The [load testing page](https://fleetdm.com/handbook/engineering/load-testing) outlines the process we use to load test Fleet, and contains the results of our semi-annual load test.
|
||||
|
||||
|
||||
## Version support
|
||||
|
||||
To provide the most accurate and efficient support, Fleet will only target fixes based on the latest released version. In the current version fixes, Fleet will not backport to older releases.
|
||||
|
||||
Community version supported for bug fixes: **Latest version only**
|
||||
|
|
@ -265,7 +314,9 @@ Premium version supported for bug fixes: **Latest version only**
|
|||
|
||||
Premium support for support/troubleshooting: **All versions**
|
||||
|
||||
|
||||
## Release testing
|
||||
|
||||
When a release is in testing, QA should use the Slack channel #help-qa to keep everyone aware of issues found. All bugs found should be reported in the channel after creating the bug first.
|
||||
|
||||
When a critical bug is found, the Fleetie who labels the bug as critical is responsible for following the [critical bug notification process](https://fleetdm.com/handbook/engineering#notify-community-members-about-a-critical-bug) below.
|
||||
|
|
@ -280,7 +331,9 @@ All unreleased bugs are addressed before publishing a release. Released bugs tha
|
|||
- Causes irreversible damage, such as data loss
|
||||
- Introduces a security vulnerability
|
||||
|
||||
|
||||
### Notify the community about a critical bug
|
||||
|
||||
We inform customers and the community about critical bugs immediately so they don’t trigger it themselves. When a bug meeting the definition of critical is found, the bug finder is responsible for raising an alarm. Raising an alarm means pinging @here in the `#g-mdm` or `#g-endpoint-ops` channel with the filed bug.
|
||||
|
||||
If the bug finder is not a Fleetie (e.g., a member of the community), then whoever sees the critical bug should raise the alarm. Note that the bug finder here is NOT necessarily the **first** person who sees the bug. If you come across a bug you think is critical, but it has not been escalated, raise the alarm!
|
||||
|
|
@ -295,7 +348,9 @@ When a critical bug is identified, we will then follow the patch release process
|
|||
|
||||
> After a critical bug is fixed, [an incident postmortem](https://fleetdm.com/handbook/engineering#preform-an-incident-postmortem) is scheduled by the EM of the product group that fixed the bug.
|
||||
|
||||
|
||||
## Feature fest
|
||||
|
||||
To stay in-sync with our customers' needs, Fleet accepts feature requests from customers and community members on a sprint-by-sprint basis in the regular 🎁🗣 Feature Fest meeting. Anyone in the company is invited to submit requests or simply listen in on the 🎁🗣 Feature Fest meeting. Folks from the wider community can also [request an invite](https://fleetdm.com/contact).
|
||||
|
||||
### Making a request
|
||||
|
|
@ -303,7 +358,9 @@ To make a feature request or advocate for a feature request from a customer or c
|
|||
|
||||
Requests are weighed from top to bottom while prioritizing attendee requests. This means that if the individual that added a feature request is not in attendance, the feature request will be discussed towards the end of the call if there's time.
|
||||
|
||||
|
||||
### How feature requests are evaluated
|
||||
|
||||
Digestion of these new product ideas (requests) happens at the **🎁🗣 Feature Fest** meeting.
|
||||
|
||||
Before the **🎁🗣 Feature Fest** meeting, the [Customer renewals DRI](https://fleetdm.com/handbook/company/communications#directly-responsible-individuals-dris) goes through the "Inbox" column and removes customer requests that are not a high priority for the business. Stakeholders will be notified by the Customer renewals DRI.
|
||||
|
|
@ -326,7 +383,9 @@ Requests are weighed by:
|
|||
- How well the request fits within Fleet's product vision and roadmap
|
||||
- Whether the feature seems like it can be designed, estimated, and developed in 6 weeks, given its individual complexity and when combined with other work already accepted
|
||||
|
||||
|
||||
### After the feature is accepted
|
||||
|
||||
After the 🎁🗣 Feature Fest meeting, the Feature prioritization DRI will clear the Feature Fest board as follows:
|
||||
**Prioritized features:** Remove `feature fest` label, add `:product` label, and move the issue to the "Ready" column in the drafting board. The request will then be assigned to a [Product Designer](https://fleetdm.com/handbook/company/product-groups#current-product-groups) during the "Design sprint kick-off" ritual.
|
||||
**Put to the side features:** Remove `feature fest` label and notify the requestor.
|
||||
|
|
@ -363,14 +422,18 @@ You can read our guide to diagnosing issues in Fleet on the [debugging page](htt
|
|||
- [In engineering](https://fleetdm.com/handbook/company/product-groups#in-engineering)
|
||||
- [Awaiting QA](https://fleetdm.com/handbook/company/product-groups#awaiting-qa)
|
||||
|
||||
|
||||
### All bugs
|
||||
|
||||
- [See on GitHub](https://github.com/fleetdm/fleet/issues?q=is%3Aissue+is%3Aopen+label%3Abug).
|
||||
|
||||
- **Bugs opened this week:** This filter returns all "bug" issues opened after the specified date. Simply replace the date with a YYYY-MM-DD equal to one week ago. [See on GitHub](https://github.com/fleetdm/fleet/issues?q=is%3Aissue+archived%3Afalse+label%3Abug+created%3A%3E%3DREPLACE_ME_YYYY-MM-DD).
|
||||
|
||||
- **Bugs closed this week:** This filter returns all "bug" issues closed after the specified date. Simply replace the date with a YYYY-MM-DD equal to one week ago. [See on Github](https://github.com/fleetdm/fleet/issues?q=is%3Aissue+archived%3Afalse+is%3Aclosed+label%3Abug+closed%3A%3E%3DREPLACE_ME_YYYY-MM-DD).
|
||||
|
||||
|
||||
#### Inbox
|
||||
|
||||
Quickly reproducing bug reports is a [priority for Fleet](https://fleetdm.com/handbook/company/why-this-way#why-make-it-obvious-when-stuff-breaks). When a new bug is created using the [bug report form](https://github.com/fleetdm/fleet/issues/new?assignees=&labels=bug%2C%3Areproduce&template=bug-report.md&title=), it is in the "inbox" state.
|
||||
|
||||
At this state, the bug review DRI (QA) is responsible for going through the inbox and documenting reproduction steps, asking for more reproduction details from the reporter, or asking the product team for more guidance. QA has **1 business day** to move the bug to the next step (reproduced).
|
||||
|
|
@ -379,7 +442,9 @@ For community-reported bugs, this may require QA to gather more information from
|
|||
|
||||
Once reproduced, QA documents the reproduction steps in the description and moves it to the reproduced state. If QA or the engineering manager feels the bug report may be expected behavior, or if clarity is required on the intended behavior, it is assigned to the group's product manager. [See on GitHub](https://github.com/fleetdm/fleet/issues?q=archived%3Afalse+org%3Afleetdm+is%3Aissue+is%3Aopen+label%3Abug+label%3A%3Areproduce+sort%3Acreated-asc+).
|
||||
|
||||
|
||||
#### Reproduced
|
||||
|
||||
QA has reproduced the issue successfully. It should now be transferred to engineering.
|
||||
|
||||
Remove the “reproduce” label, add the following labels:
|
||||
|
|
@ -393,10 +458,14 @@ Once the bug is properly labeled, assign it to the [relevant engineering manager
|
|||
|
||||
> **Fast track for Fleeties:** Fleeties do not have to wait for QA to reproduce the bug. If you're confident it's reproducible, it's a bug, and the reproduction steps are well-documented, it can be moved directly to the reproduced state.
|
||||
|
||||
|
||||
#### In product drafting (as needed)
|
||||
|
||||
If a bug requires input from product the `:product` label is added, the `:release` label is removed, and the PM is assigned to the issue. It will stay in this state until product closes the bug, or removes the `:product` label and assigns to an EM.
|
||||
|
||||
|
||||
#### In engineering
|
||||
|
||||
A bug is in engineering after it has been reproduced and assigned to an EM. If a bug meets the criteria for a [critical bug](https://fleetdm.com/handbook/engineering#critical-bugs), the `~critical bug` label is added, and the EM follows the [critical bug notification process](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Releasing-Fleet.md#critical-bug-notification-process).
|
||||
|
||||
During daily standup, the EM will filter the board to only `:incoming` bugs and review with the team. The EM will remove the `:incoming` label, prioritize the bug in the "Ready" coulmn, unassign themselves, and assign an engineer or leave it unassigned for the first available engineer.
|
||||
|
|
@ -415,13 +484,19 @@ For Endpoint ops support on MDM bugs:
|
|||
|
||||
Fleet [always prioritizes bugs](https://fleetdm.com/handbook/product#prioritizing-improvements).
|
||||
|
||||
|
||||
#### Awaiting QA
|
||||
|
||||
Bugs will be verified as fixed by QA when they are placed in the "Awaiting QA" column of the relevant product group's sprint board. If the bug is verified as fixed, it is moved to the "Ready for release" column of the sprint board. Otherwise, the remaining issues are noted in a comment, and it is moved back to the "In progress" column of the sprint board.
|
||||
|
||||
|
||||
## How to reach the developer on-call
|
||||
|
||||
Oncall engineers do not need to actively monitor Slack channels, except when called in by the Community or Customer teams. Members of those teams are instructed to `@oncall` in `#help-engineering` to get the attention of the on-call engineer to continue discussing any issues that come up. In some cases, the Community or Customer representative will continue to communicate with the requestor. In others, the on-call engineer will communicate directly (team members should use their judgment and discuss on a case-by-case basis how to best communicate with community members and customers).
|
||||
|
||||
|
||||
### The developer on-call rotation
|
||||
|
||||
See [the internal Google Doc](https://docs.google.com/document/d/1FNQdu23wc1S9Yo6x5k04uxT2RwT77CIMzLLeEI2U7JA/edit#) for the engineers in the rotation.
|
||||
|
||||
Fleet team members can also subscribe to the [shared calendar](https://calendar.google.com/calendar/u/0?cid=Y181MzVkYThiNzMxMGQwN2QzOWEwMzU0MWRkYzc5ZmVhYjk4MmU0NzQ1ZTFjNzkzNmIwMTAxOTllOWRmOTUxZWJhQGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20) for calendar events.
|
||||
|
|
@ -430,7 +505,9 @@ New developers are added to the on-call rotation by their manager after they hav
|
|||
|
||||
> The on-call rotation may be adjusted with approval from the EMs of any product groups affected. Any changes should be made before the start of the sprint so that capacity can be planned accordingly.
|
||||
|
||||
|
||||
### Developer on-call responsibilities
|
||||
|
||||
- **Second-line response**
|
||||
The on-call developer is a second-line responder to questions raised by customers and community members.
|
||||
|
||||
|
|
@ -459,7 +536,9 @@ Fleet's documentation for contributors can be found in the [Fleet GitHub repo](h
|
|||
|
||||
The on-call developer is asked to read, understand, test, correct, and improve at least one doc page per week. Our goal is to 1, ensure accuracy and verify that our deployment guides and tutorials are up to date and work as expected. And 2, improve the readability, consistency, and simplicity of our documentation – with empathy towards first-time users. See [Writing documentation](https://fleetdm.com/handbook/marketing#writing-documentation) for writing guidelines, and don't hesitate to reach out to [#g-digital-experience](https://fleetdm.slack.com/archives/C01GQUZ91TN) on Slack for writing support. A backlog of documentation improvement needs is kept [here](https://github.com/fleetdm/fleet/issues?q=is%3Aopen+is%3Aissue+label%3A%22%3Aimprove+documentation%22).
|
||||
|
||||
|
||||
### Escalations
|
||||
|
||||
When the on-call developer is unsure of the answer, they should follow this process for escalation.
|
||||
|
||||
To achieve quick "first-response" times, you are encouraged to say something like "I don't know the answer and I'm taking it back to the team," or "I think X, but I'm confirming that with the team (or by looking in the code)."
|
||||
|
|
@ -470,7 +549,9 @@ How to escalate:
|
|||
|
||||
2. Create a new thread in the [#help-engineering channel](https://fleetdm.slack.com/archives/C019WG4GH0A), tagging `@lukeheath` and provide the information turned up in your research. Please include possibly relevant links (even if you didn't find what you were looking for there). Luke will work with you to craft an appropriate answer or find another team member who can help.
|
||||
|
||||
|
||||
### Changing of the guard
|
||||
|
||||
The on-call developer changes each week on Wednesday.
|
||||
|
||||
A Slack reminder should notify the on-call of the handoff. Please do the following:
|
||||
|
|
@ -487,7 +568,9 @@ In the Slack reminder thread, the on-call developer includes their retrospective
|
|||
|
||||
3. How did you spend the rest of your on-call week? This is a chance to demo or share what you learned.
|
||||
|
||||
|
||||
## Wireframes
|
||||
|
||||
- Showing these principles and ideas, to help remember the pros and cons and conceptualize the above visually.
|
||||
- Figma: [⚗️ Fleet product project](https://www.figma.com/files/project/17318630/%E2%9A%97%EF%B8%8F-Fleet-product?fuid=1234929285759903870)
|
||||
|
||||
|
|
@ -560,9 +643,12 @@ OPTIONS
|
|||
--host Host specified by hostname, uuid, osquery_host_id or node_key that you want to target.
|
||||
```
|
||||
|
||||
|
||||
## Meetings
|
||||
|
||||
|
||||
### User story discovery
|
||||
|
||||
User story discovery meetings are scheduled as needed to align on large or complicated user stories. Before a discovery meeting is scheduled, the user story must be prioritized for product drafting and go through the design and specification process. When the user story is ready to be estimated, a user story discovery meeting may be scheduled to provide more dedicated, synchronous time for the team to discuss the user story than is available during weekly estimation sessions.
|
||||
|
||||
All participants are expected to review the user story and associated designs and specifications before the discovery meeting.
|
||||
|
|
@ -582,7 +668,9 @@ All participants are expected to review the user story and associated designs an
|
|||
- Software Engineers: Clarifying questions and implementation details
|
||||
- Product Quality Specialist: Testing plan
|
||||
|
||||
|
||||
### Design consultation
|
||||
|
||||
Design consultations are scheduled as needed with the relevant participants, typically product designers and frontend engineers. It is an opportunity to collaborate and discuss design, implementation, and story requirements. The meeting is scheduled as needed by the product designer or frontend engineer when a user story is in the "Prioritized" column on the [drafting board](https://app.zenhub.com/workspaces/-drafting-ships-in-6-weeks-6192dd66ea2562000faea25c/board).
|
||||
|
||||
**Participants:**
|
||||
|
|
@ -595,7 +683,9 @@ Design consultations are scheduled as needed with the relevant participants, typ
|
|||
- Discuss design input
|
||||
- Discuss implementation details
|
||||
|
||||
|
||||
### Design reviews
|
||||
|
||||
Design reviews are conducted daily between the [Head of Product Design](https://fleetdm.com/handbook/product-design#team) and contributors proposing changes to Fleet's interfaces, such as the graphical user interface (GUI) or REST API. This fast cadence shortens the feedback loop, makes progress visible, and encourages early feedback. This helps Fleet stay intentional about how the product is designed and minimize common issues like UI inconsistencies or accidental breaking changes to the API.
|
||||
|
||||
Product designers or other contributors come prepared to this meeting with their proposed changes in a GitHub issue. Usually these are in the form of Figma wireframes, a pull request to the API docs showing changes, or a demo of a prototype. The Head of Product Design and other participants review the changes quickly and give feedback, and then the contributor applies revisions and attends again the next day or as soon as possible for another go-round. The Head of Product Design is responsible for looping in the right engineers, community members, and other subject-matter experts to iterate on and refine upcoming product changes in the best interest of the business.
|
||||
|
|
@ -610,12 +700,16 @@ Here are some tips for making this meeting effective:
|
|||
|
||||
> To allow for asynchronous participation, instead of attending, contributors can alternatively choose to add an agenda item to the "Product design review" meeting with a GitHub link. Then, the Head of Product Design will review during the meeting and provide feedback. Every "Product design review" is recorded and automatically transcribed to a Google Doc so that it is searchable by every Fleet team member.
|
||||
|
||||
|
||||
### Weekly bug review
|
||||
|
||||
QA has weekly check-in with product to go over the inbox items. QA is responsible for proposing “not a bug”, closing due to lack of response (with a nice message), or raising other relevant questions. All requires product agreement
|
||||
|
||||
QA may also propose that a reported bug is not actually a bug. A bug is defined as “behavior that is not according to spec or implied by spec.” If agreed that it is not a bug, then it's assigned to the relevant product manager to determine its priority.
|
||||
|
||||
|
||||
### Group weeklies
|
||||
|
||||
A chance for deeper, synchronous discussion on topics relevant across product groups like “Frontend weekly”, “Backend weekly”, etc.
|
||||
|
||||
**Participants:** Anyone who wishes to participate.
|
||||
|
|
@ -625,7 +719,9 @@ A chance for deeper, synchronous discussion on topics relevant across product gr
|
|||
- Review difficult frontend bugs
|
||||
- Write engineering-initiated stories
|
||||
|
||||
|
||||
### Eng Together
|
||||
|
||||
This meeting is to disseminate engineering-wide announcements, promote cohesion across groups within the engineering team, and connect with engineers (and the "engineering-curious") in other departments. Held monthly for one hour.
|
||||
|
||||
**Participants:** Everyone at the company is welcome to attend. All engineers are asked to attend. The subject matter is focused on engineering.
|
||||
|
|
@ -639,14 +735,18 @@ This meeting is to disseminate engineering-wide announcements, promote cohesion
|
|||
- Social
|
||||
- Structured and/or unstructured social activities
|
||||
|
||||
|
||||
## Development best practices
|
||||
|
||||
- Remember the user. What would you do if you saw that error message? [🔴](https://fleetdm.com/handbook/company#empathy)
|
||||
- Communicate any blockers ASAP in your group Slack channel or standup. [🟠](https://fleetdm.com/handbook/company#ownership)
|
||||
- Think fast and iterate. [🟢](https://fleetdm.com/handbook/company#results)
|
||||
- If it probably works, assume it's still broken. Assume it's your fault. [🔵](https://fleetdm.com/handbook/company#objectivity)
|
||||
- Speak up and have short toes. Write things down to make them complete. [🟣](https://fleetdm.com/handbook/company#openness)
|
||||
|
||||
|
||||
## Product design conventions
|
||||
|
||||
Behind every [wireframe at Fleet](https://fleetdm.com/handbook/company/why-this-way#why-do-we-use-a-wireframe-first-approach), there are 3 foundational design principles:
|
||||
|
||||
- **Use-case first.** Taking advantage of top-level features vs. per-platform options allows us to take advantage of similarities and avoid having two different ways to configure the same thing.
|
||||
|
|
@ -657,13 +757,17 @@ Start off cross-platform for every option, setting, and feature. If we **prove**
|
|||
|
||||
- **Control the noise.** Bring the needs surface level, tuck away things you don't need by default (when possible, given time). For example, hide Windows controls if there are no Windows devices (based on number of Windows hosts).
|
||||
|
||||
|
||||
## Scrum at Fleet
|
||||
|
||||
Fleet product groups employ scrum, an agile methodology, as a core practice in software development. This process is designed around sprints, which last three weeks to align with our release cadence.
|
||||
|
||||
New tickets are estimated, specified, and prioritized on the roadmap:
|
||||
- [Roadmap](https://app.zenhub.com/workspaces/-roadmap-ships-in-6-weeks-6192dd66ea2562000faea25c/board)
|
||||
|
||||
|
||||
### Scrum items
|
||||
|
||||
Our scrum boards are exclusively composed of four types of scrum items:
|
||||
|
||||
1. **User stories**: These are simple and concise descriptions of features or requirements from the user's perspective, marked with the `story` label. They keep our focus on delivering value to our customers. Occasionally, due to ZenHub's ticket sub-task structure, the term "epic" may be seen. However, we treat these as regular user stories.
|
||||
|
|
@ -676,17 +780,23 @@ Our scrum boards are exclusively composed of four types of scrum items:
|
|||
|
||||
> Our sprint boards do not accommodate any other type of ticket. By strictly adhering to these four types of scrum items, we maintain an organized and focused workflow that consistently adds value for our users.
|
||||
|
||||
|
||||
## Sprints
|
||||
|
||||
Sprints align with Fleet's [3-week release cycle](https://fleetdm.com/handbook/company/why-this-way#why-a-three-week-cadence).
|
||||
|
||||
On the first day of each release, all estimated issues are moved into the relevant section of the new "Release" board, which has a kanban view per group.
|
||||
|
||||
Sprints are managed in [Zenhub](https://fleetdm.com/handbook/company/why-this-way#why-make-work-visible). To plan capacity for a sprint, [create a "Sprint" issue](https://github.com/fleetdm/confidential/issues/new/choose), replace the fake constants with real numbers, and attach the appropriate labels for your product group.
|
||||
|
||||
|
||||
### Sprint numbering
|
||||
|
||||
Sprints are numbered according to the release version. For example, for the sprint ending on June 30th, 2023, on which date we expect to release Fleet v4.34, the sprint is called the 4.34 sprint.
|
||||
|
||||
|
||||
### Sprint ceremonies
|
||||
|
||||
Each sprint is marked by five essential ceremonies:
|
||||
|
||||
1. **Sprint kickoff**: On the first day of the sprint, the team, along with stakeholders, select items from the backlog to work on. The team then commits to completing these items within the sprint.
|
||||
|
|
@ -695,7 +805,9 @@ Each sprint is marked by five essential ceremonies:
|
|||
4. **Sprint demo**: On the last day of each sprint, all engineering teams and stakeholders come together to review the next release. Engineers are allotted 3-10 minutes to showcase features, improvements, and bug fixes they have contributed to the upcoming release. We focus on changes that can be demoed live and avoid overly technical details so the presentation is accessible to everyone. Features should show what is capable and bugs should identify how this might have impacted existing customers and how this resolution fixed that. (These meetings are recorded and posted publicly to YouTube or other platforms, so participants should avoid mentioning customer names. For example, instead of "Fastly", you can say "a publicly-traded hosting company", or use the [customer's codename](https://fleetdm.com/handbook/customers#customer-codenames).)
|
||||
5. **Sprint retrospective**: Also held on the last day of the sprint, this meeting encourages discussions among the team and stakeholders around three key areas: what went well, what could have been better, and what the team learned during the sprint.
|
||||
|
||||
|
||||
## Outside contributions
|
||||
|
||||
[Anyone can contribute](https://fleetdm.com/handbook/company#openness) at Fleet, from inside or outside the company. Since contributors from the wider community don't receive a paycheck from Fleet, they work on whatever they want.
|
||||
|
||||
Many open source contributions that start as a small, seemingly innocuous pull request come with lots of additional [unplanned work](https://fleetdm.com/handbook/company/development-groups#planned-and-unplanned-changes) down the road: unforseen side effects, documentation, testing, potential breaking changes, database migrations, [and more](https://fleetdm.com/handbook/company/development-groups#defining-done).
|
||||
|
|
|
|||
|
|
@ -141,15 +141,6 @@
|
|||
quoteAuthorProfileImageFilename: testimonial-author-dhruv-majumdar-48x48@2x.png
|
||||
quoteAuthorJobTitle: Director Of Cyber Risk & Advisory
|
||||
productCategories: [Vulnerability management, Endpoint operations]
|
||||
-
|
||||
quote: When we look at vendors, we look for ones that are very receptive to feedback, where you’re just part of the family, I guess. Fleet’s really good at that.
|
||||
quoteImageFilename: logo-deputy-118x28@2x.png
|
||||
quoteAuthorName: Harrison Ravazzolo
|
||||
quoteAuthorProfileImageFilename: testimonial-author-harrison-ravazzolo-48x48@2x.png
|
||||
quoteLinkUrl: https://www.linkedin.com/in/harrison-ravazzolo/
|
||||
quoteAuthorJobTitle: Lead platform and identity engineer
|
||||
youtubeVideoUrl: https://www.youtube.com/watch?v=5W0q5yQE3R0
|
||||
productCategories: [Endpoint operations]
|
||||
-
|
||||
quote: Fleet has such a huge amount of use cases. My goal was to get telemetry on endpoints, but then our IR team, our TBM team, and multiple other folks in security started heavily utilizing the system in ways I didn’t expect. It spread so naturally, even our corporate and infrastructure teams want to run it.
|
||||
quoteAuthorName: Charles Zaffery
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
# https://github.com/fleetdm/fleet/pull/13084
|
||||
|
||||
-
|
||||
task: "Complete Digital Experience KPIs"
|
||||
startedOn: "2024-08-30"
|
||||
frequency: "Weekly"
|
||||
description: "Complete Digital Experience KPIs for this week"
|
||||
moreInfoUrl: "https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit?gid=0#gid=0&range=DB1"
|
||||
dri: "SFriendLee"
|
||||
autoIssue:
|
||||
labels: [ "#g-digital-experience" ]
|
||||
repo: "fleet"
|
||||
-
|
||||
task: "Prep 1:1s for OKR planning"
|
||||
startedOn: "2024-09-09"
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ Following are the currently deployed versions of fleetd components on the `stabl
|
|||
|
||||
| Component\OS | macOS | Linux | Windows | Linux (arm64) |
|
||||
|--------------|--------|--------|---------|---------------|
|
||||
| orbit | 1.31.0 | 1.31.0 | 1.31.0 | 1.31.0 |
|
||||
| desktop | 1.31.0 | 1.31.0 | 1.31.0 | 1.31.0 |
|
||||
| orbit | 1.32.0 | 1.32.0 | 1.32.0 | 1.32.0 |
|
||||
| desktop | 1.32.0 | 1.32.0 | 1.32.0 | 1.32.0 |
|
||||
| osqueryd | 5.13.1 | 5.13.1 | 5.13.1 | 5.13.1 |
|
||||
| nudge | - | - | - | - |
|
||||
| swiftDialog | - | - | - | - |
|
||||
|
|
|
|||
|
|
@ -31,7 +31,12 @@ func (ds *Datastore) NewActivity(
|
|||
var userName *string
|
||||
var userEmail *string
|
||||
if user != nil {
|
||||
userID = &user.ID
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
// the installation.
|
||||
if user.ID != 0 {
|
||||
userID = &user.ID
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
}
|
||||
|
|
@ -311,10 +316,12 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
|||
// list pending software installs
|
||||
fmt.Sprintf(`SELECT
|
||||
hsi.execution_id as uuid,
|
||||
u.name as name,
|
||||
u.id as user_id,
|
||||
u.gravatar_url as gravatar_url,
|
||||
u.email as user_email,
|
||||
-- policies with automatic installers generate a host_software_installs with (user_id=NULL,self_service=0),
|
||||
-- thus the user_id for the upcoming activity needs to be the user that uploaded the software installer.
|
||||
IF(hsi.user_id IS NULL AND NOT hsi.self_service, u2.name, u.name) AS name,
|
||||
IF(hsi.user_id IS NULL AND NOT hsi.self_service, u2.id, u.id) as user_id,
|
||||
IF(hsi.user_id IS NULL AND NOT hsi.self_service, u2.gravatar_url, u.gravatar_url) as gravatar_url,
|
||||
IF(hsi.user_id IS NULL AND NOT hsi.self_service, u2.email, u.email) AS user_email,
|
||||
:installed_software_type as activity_type,
|
||||
hsi.created_at as created_at,
|
||||
JSON_OBJECT(
|
||||
|
|
@ -334,6 +341,8 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
|||
software_titles st ON st.id = si.title_id
|
||||
LEFT OUTER JOIN
|
||||
users u ON u.id = hsi.user_id
|
||||
LEFT OUTER JOIN
|
||||
users u2 ON u2.id = si.user_id
|
||||
LEFT OUTER JOIN
|
||||
host_display_names hdn ON hdn.host_id = hsi.host_id
|
||||
WHERE
|
||||
|
|
|
|||
|
|
@ -401,6 +401,7 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
Title: "foo",
|
||||
Source: "apps",
|
||||
Version: "0.0.1",
|
||||
UserID: u.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sw2, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
|
|
@ -411,6 +412,7 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
Title: "bar",
|
||||
Source: "apps",
|
||||
Version: "0.0.2",
|
||||
UserID: u.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sw1Meta, err := ds.GetSoftwareInstallerMetadataByID(ctx, sw1)
|
||||
|
|
@ -492,7 +494,10 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
InstallScriptExitCode: ptr.Int(0),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
h1Foo, err := ds.InsertSoftwareInstallRequest(noUserCtx, h1.ID, sw1Meta.InstallerID, false) // no user for this one
|
||||
|
||||
// No user for this one and not Self-service, means it was installed by Fleet thus the author was decided to be the admin
|
||||
// that uploaded the installer.
|
||||
h1Foo, err := ds.InsertSoftwareInstallRequest(noUserCtx, h1.ID, sw1Meta.InstallerID, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a single pending request for h2, as well as a non-pending one
|
||||
|
|
@ -507,6 +512,9 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
// add a pending software install request for h2
|
||||
h2Bar, err := ds.InsertSoftwareInstallRequest(ctx, h2.ID, sw2Meta.InstallerID, false)
|
||||
require.NoError(t, err)
|
||||
// No user for this one and Self-service, means it was installed by the end user, so the user_id should be null/nil.
|
||||
h2Foo, err := ds.InsertSoftwareInstallRequest(noUserCtx, h2.ID, sw1Meta.InstallerID, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// nothing for h3
|
||||
|
||||
|
|
@ -515,6 +523,8 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h1FooFailed, h1Bar)
|
||||
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h1C, h1D, h1E)
|
||||
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h1FooInstalled, h1Foo)
|
||||
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h1Foo)
|
||||
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h2Foo)
|
||||
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h2Bar)
|
||||
endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h2A, h2F)
|
||||
SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_vpp_software_installs", "command_uuid", vppCommand1, vppCommand2)
|
||||
|
|
@ -527,7 +537,8 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
h1E: false,
|
||||
h2A: true,
|
||||
h2F: true,
|
||||
h1Foo: false,
|
||||
h1Foo: true,
|
||||
h2Foo: false,
|
||||
h1Bar: true,
|
||||
h2Bar: true,
|
||||
vppCommand1: true,
|
||||
|
|
@ -542,6 +553,10 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
h1Foo: "foo",
|
||||
h1Bar: "bar",
|
||||
h2Bar: "bar",
|
||||
h2Foo: "foo",
|
||||
}
|
||||
execIDsWithUserAdminID := map[string]struct{}{
|
||||
h1Foo: {},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
|
|
@ -593,10 +608,10 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 8},
|
||||
},
|
||||
{
|
||||
opts: fleet.ListOptions{PerPage: 3},
|
||||
opts: fleet.ListOptions{PerPage: 4},
|
||||
hostID: h2.ID,
|
||||
wantExecs: []string{h2Bar, h2A, vppCommand2},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 3},
|
||||
wantExecs: []string{h2Foo, h2Bar, h2A, vppCommand2},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 4},
|
||||
},
|
||||
{
|
||||
opts: fleet.ListOptions{},
|
||||
|
|
@ -637,7 +652,11 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
case fleet.ActivityTypeInstalledSoftware{}.ActivityName():
|
||||
require.Equal(t, wantExec, details["install_uuid"], "result %d", i)
|
||||
require.Equal(t, execIDsSoftwareTitle[wantExec], details["software_title"], "result %d", i)
|
||||
wantUser = u2
|
||||
if _, ok := execIDsWithUserAdminID[details["install_uuid"].(string)]; ok {
|
||||
wantUser = u
|
||||
} else {
|
||||
wantUser = u2
|
||||
}
|
||||
|
||||
case fleet.ActivityInstalledAppStoreApp{}.ActivityName():
|
||||
require.Equal(t, wantExec, details["command_uuid"], "result %d", i)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
|
@ -88,7 +89,7 @@ INSERT INTO
|
|||
cp.LabelsExcludeAny[i].Exclude = true
|
||||
labels = append(labels, cp.LabelsExcludeAny[i])
|
||||
}
|
||||
if err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, "darwin"); err != nil {
|
||||
if _, err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, "darwin"); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting darwin profile label associations")
|
||||
}
|
||||
|
||||
|
|
@ -1135,7 +1136,7 @@ func upsertMDMAppleHostMDMInfoDB(ctx context.Context, tx sqlx.ExtContext, server
|
|||
}
|
||||
|
||||
var mdmID int64
|
||||
if insertOnDuplicateDidInsert(result) {
|
||||
if insertOnDuplicateDidInsertOrUpdate(result) {
|
||||
mdmID, _ = result.LastInsertId()
|
||||
} else {
|
||||
stmt := `SELECT id FROM mobile_device_management_solutions WHERE name = ? AND server_url = ?`
|
||||
|
|
@ -1444,7 +1445,8 @@ func (ds *Datastore) GetNanoMDMEnrollment(ctx context.Context, id string) (*flee
|
|||
|
||||
func (ds *Datastore) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, profiles []*fleet.MDMAppleConfigProfile) error {
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return ds.batchSetMDMAppleProfilesDB(ctx, tx, tmID, profiles)
|
||||
_, err := ds.batchSetMDMAppleProfilesDB(ctx, tx, tmID, profiles)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1454,7 +1456,7 @@ func (ds *Datastore) batchSetMDMAppleProfilesDB(
|
|||
tx sqlx.ExtContext,
|
||||
tmID *uint,
|
||||
profiles []*fleet.MDMAppleConfigProfile,
|
||||
) error {
|
||||
) (updatedDB bool, err error) {
|
||||
const loadExistingProfiles = `
|
||||
SELECT
|
||||
identifier,
|
||||
|
|
@ -1516,13 +1518,13 @@ ON DUPLICATE KEY UPDATE
|
|||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "build query to load existing profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "build query to load existing profiles")
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, tx, &existingProfiles, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "select") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "load existing profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "load existing profiles")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1543,31 +1545,37 @@ ON DUPLICATE KEY UPDATE
|
|||
var (
|
||||
stmt string
|
||||
args []interface{}
|
||||
err error
|
||||
)
|
||||
// delete the obsolete profiles (all those that are not in keepIdents or delivered by Fleet)
|
||||
var result sql.Result
|
||||
stmt, args, err = sqlx.In(deleteProfilesNotInList, profTeamID, append(keepIdents, fleetIdents...))
|
||||
if err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "indelete") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "build statement to delete obsolete profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "build statement to delete obsolete profiles")
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "delete") {
|
||||
if result, err = tx.ExecContext(ctx, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "delete") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "delete obsolete profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "delete obsolete profiles")
|
||||
}
|
||||
if result != nil {
|
||||
rows, _ := result.RowsAffected()
|
||||
updatedDB = rows > 0
|
||||
}
|
||||
|
||||
// insert the new profiles and the ones that have changed
|
||||
for _, p := range incomingProfs {
|
||||
if _, err := tx.ExecContext(ctx, insertNewOrEditedProfile, profTeamID, p.Identifier, p.Name, p.Mobileconfig); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "insert") {
|
||||
if result, err = tx.ExecContext(ctx, insertNewOrEditedProfile, profTeamID, p.Identifier, p.Name,
|
||||
p.Mobileconfig); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "insert") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrapf(ctx, err, "insert new/edited profile with identifier %q", p.Identifier)
|
||||
return false, ctxerr.Wrapf(ctx, err, "insert new/edited profile with identifier %q", p.Identifier)
|
||||
}
|
||||
updatedDB = updatedDB || insertOnDuplicateDidInsertOrUpdate(result)
|
||||
}
|
||||
|
||||
// build a list of labels so the associations can be batch-set all at once
|
||||
|
|
@ -1583,19 +1591,19 @@ ON DUPLICATE KEY UPDATE
|
|||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "build query to load newly inserted profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "build query to load newly inserted profiles")
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, tx, &newlyInsertedProfs, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "reselect") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "load newly inserted profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "load newly inserted profiles")
|
||||
}
|
||||
|
||||
for _, newlyInsertedProf := range newlyInsertedProfs {
|
||||
incomingProf, ok := incomingProfs[newlyInsertedProf.Identifier]
|
||||
if !ok {
|
||||
return ctxerr.Wrapf(ctx, err, "profile %q is in the database but was not incoming", newlyInsertedProf.Identifier)
|
||||
return false, ctxerr.Wrapf(ctx, err, "profile %q is in the database but was not incoming", newlyInsertedProf.Identifier)
|
||||
}
|
||||
|
||||
for _, label := range incomingProf.LabelsIncludeAll {
|
||||
|
|
@ -1611,13 +1619,15 @@ ON DUPLICATE KEY UPDATE
|
|||
}
|
||||
|
||||
// insert label associations
|
||||
if err := batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels, "darwin"); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "labels") {
|
||||
var updatedLabels bool
|
||||
if updatedLabels, err = batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels,
|
||||
"darwin"); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "labels") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "inserting apple profile label associations")
|
||||
return false, ctxerr.Wrap(ctx, err, "inserting apple profile label associations")
|
||||
}
|
||||
return nil
|
||||
return updatedDB || updatedLabels, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) BulkDeleteMDMAppleHostsConfigProfiles(ctx context.Context, profs []*fleet.MDMAppleProfilePayload) error {
|
||||
|
|
@ -1682,9 +1692,9 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
ctx context.Context,
|
||||
tx sqlx.ExtContext,
|
||||
uuids []string,
|
||||
) error {
|
||||
) (updatedDB bool, err error) {
|
||||
if len(uuids) == 0 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
appleMDMProfilesDesiredStateQuery := generateDesiredStateQuery("profile")
|
||||
|
|
@ -1752,13 +1762,14 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
stmt, args, err := sqlx.In(toInstallStmt, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "building statement to select profiles to install, batch %d of %d", i, selectProfilesTotalBatches)
|
||||
return false, ctxerr.Wrapf(ctx, err, "building statement to select profiles to install, batch %d of %d", i,
|
||||
selectProfilesTotalBatches)
|
||||
}
|
||||
|
||||
var partialResult []*fleet.MDMAppleProfilePayload
|
||||
err = sqlx.SelectContext(ctx, tx, &partialResult, stmt, args...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "selecting profiles to install, batch %d of %d", i, selectProfilesTotalBatches)
|
||||
return false, ctxerr.Wrapf(ctx, err, "selecting profiles to install, batch %d of %d", i, selectProfilesTotalBatches)
|
||||
}
|
||||
|
||||
wantedProfiles = append(wantedProfiles, partialResult...)
|
||||
|
|
@ -1810,19 +1821,19 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
stmt, args, err := sqlx.In(toRemoveStmt, batchUUIDs, batchUUIDs, batchUUIDs, batchUUIDs, fleet.MDMOperationTypeRemove)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "building profiles to remove statement")
|
||||
return false, ctxerr.Wrap(ctx, err, "building profiles to remove statement")
|
||||
}
|
||||
var partialResult []*fleet.MDMAppleProfilePayload
|
||||
err = sqlx.SelectContext(ctx, tx, &partialResult, stmt, args...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "fetching profiles to remove")
|
||||
return false, ctxerr.Wrap(ctx, err, "fetching profiles to remove")
|
||||
}
|
||||
|
||||
currentProfiles = append(currentProfiles, partialResult...)
|
||||
}
|
||||
|
||||
if len(wantedProfiles) == 0 && len(currentProfiles) == 0 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// delete all host profiles to start from a clean slate, new entries will be added next
|
||||
|
|
@ -1831,8 +1842,11 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
//
|
||||
// TODO part II(roberto): we found this call to be a major bottleneck during load testing
|
||||
// https://github.com/fleetdm/fleet/issues/21338
|
||||
if err := ds.bulkDeleteMDMAppleHostsConfigProfilesDB(ctx, tx, wantedProfiles); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk delete all profiles")
|
||||
if len(wantedProfiles) > 0 {
|
||||
if err := ds.bulkDeleteMDMAppleHostsConfigProfilesDB(ctx, tx, wantedProfiles); err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "bulk delete all profiles")
|
||||
}
|
||||
updatedDB = true
|
||||
}
|
||||
|
||||
// profileIntersection tracks profilesToAdd ∩ profilesToRemove, this is used to avoid:
|
||||
|
|
@ -1853,11 +1867,57 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
hostProfilesToClean = append(hostProfilesToClean, p)
|
||||
}
|
||||
}
|
||||
if err := ds.bulkDeleteMDMAppleHostsConfigProfilesDB(ctx, tx, hostProfilesToClean); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk delete profiles to clean")
|
||||
if len(hostProfilesToClean) > 0 {
|
||||
if err := ds.bulkDeleteMDMAppleHostsConfigProfilesDB(ctx, tx, hostProfilesToClean); err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "bulk delete profiles to clean")
|
||||
}
|
||||
updatedDB = true
|
||||
}
|
||||
|
||||
profilesToInsert := make(map[string]*fleet.MDMAppleProfilePayload)
|
||||
|
||||
executeUpsertBatch := func(valuePart string, args []any) error {
|
||||
// Check if the update needs to be done at all.
|
||||
selectStmt := fmt.Sprintf(`
|
||||
SELECT
|
||||
host_uuid,
|
||||
profile_uuid,
|
||||
profile_identifier,
|
||||
status,
|
||||
COALESCE(operation_type, '') AS operation_type,
|
||||
COALESCE(detail, '') AS detail,
|
||||
command_uuid,
|
||||
profile_name,
|
||||
checksum,
|
||||
profile_uuid
|
||||
FROM host_mdm_apple_profiles WHERE (host_uuid, profile_uuid) IN (%s)`,
|
||||
strings.TrimSuffix(strings.Repeat("(?,?),", len(profilesToInsert)), ","))
|
||||
var selectArgs []any
|
||||
for _, p := range profilesToInsert {
|
||||
selectArgs = append(selectArgs, p.HostUUID, p.ProfileUUID)
|
||||
}
|
||||
var existingProfiles []fleet.MDMAppleProfilePayload
|
||||
if err := sqlx.SelectContext(ctx, tx, &existingProfiles, selectStmt, selectArgs...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending profile status select existing")
|
||||
}
|
||||
var updateNeeded bool
|
||||
if len(existingProfiles) == len(profilesToInsert) {
|
||||
for _, exist := range existingProfiles {
|
||||
insert, ok := profilesToInsert[fmt.Sprintf("%s\n%s", exist.HostUUID, exist.ProfileUUID)]
|
||||
if !ok || !exist.Equal(*insert) {
|
||||
updateNeeded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateNeeded = true
|
||||
}
|
||||
if !updateNeeded {
|
||||
// All profiles are already in the database, no need to update.
|
||||
return nil
|
||||
}
|
||||
|
||||
updatedDB = true
|
||||
baseStmt := fmt.Sprintf(`
|
||||
INSERT INTO host_mdm_apple_profiles (
|
||||
profile_uuid,
|
||||
|
|
@ -1897,6 +1957,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
resetBatch := func() {
|
||||
batchCount = 0
|
||||
clear(profilesToInsert)
|
||||
pargs = pargs[:0]
|
||||
psb.Reset()
|
||||
}
|
||||
|
|
@ -1904,6 +1965,18 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
for _, p := range wantedProfiles {
|
||||
if pp, ok := profileIntersection.GetMatchingProfileInCurrentState(p); ok {
|
||||
if pp.Status != &fleet.MDMDeliveryFailed && bytes.Equal(pp.Checksum, p.Checksum) {
|
||||
profilesToInsert[fmt.Sprintf("%s\n%s", p.HostUUID, p.ProfileUUID)] = &fleet.MDMAppleProfilePayload{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
ProfileIdentifier: p.ProfileIdentifier,
|
||||
ProfileName: p.ProfileName,
|
||||
HostUUID: p.HostUUID,
|
||||
HostPlatform: p.HostPlatform,
|
||||
Checksum: p.Checksum,
|
||||
Status: pp.Status,
|
||||
OperationType: pp.OperationType,
|
||||
Detail: pp.Detail,
|
||||
CommandUUID: pp.CommandUUID,
|
||||
}
|
||||
pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum,
|
||||
pp.OperationType, pp.Status, pp.CommandUUID, pp.Detail)
|
||||
psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),")
|
||||
|
|
@ -1911,7 +1984,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
if batchCount >= batchSize {
|
||||
if err := executeUpsertBatch(psb.String(), pargs); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
resetBatch()
|
||||
}
|
||||
|
|
@ -1919,6 +1992,18 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
}
|
||||
}
|
||||
|
||||
profilesToInsert[fmt.Sprintf("%s\n%s", p.HostUUID, p.ProfileUUID)] = &fleet.MDMAppleProfilePayload{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
ProfileIdentifier: p.ProfileIdentifier,
|
||||
ProfileName: p.ProfileName,
|
||||
HostUUID: p.HostUUID,
|
||||
HostPlatform: p.HostPlatform,
|
||||
Checksum: p.Checksum,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Status: nil,
|
||||
CommandUUID: "",
|
||||
Detail: "",
|
||||
}
|
||||
pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum,
|
||||
fleet.MDMOperationTypeInstall, nil, "", "")
|
||||
psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),")
|
||||
|
|
@ -1926,7 +2011,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
if batchCount >= batchSize {
|
||||
if err := executeUpsertBatch(psb.String(), pargs); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
resetBatch()
|
||||
}
|
||||
|
|
@ -1943,6 +2028,18 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
if p.FailedToInstallOnHost() {
|
||||
continue
|
||||
}
|
||||
profilesToInsert[fmt.Sprintf("%s\n%s", p.HostUUID, p.ProfileUUID)] = &fleet.MDMAppleProfilePayload{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
ProfileIdentifier: p.ProfileIdentifier,
|
||||
ProfileName: p.ProfileName,
|
||||
HostUUID: p.HostUUID,
|
||||
HostPlatform: p.HostPlatform,
|
||||
Checksum: p.Checksum,
|
||||
OperationType: fleet.MDMOperationTypeRemove,
|
||||
Status: nil,
|
||||
CommandUUID: "",
|
||||
Detail: "",
|
||||
}
|
||||
pargs = append(pargs, p.ProfileUUID, p.HostUUID, p.ProfileIdentifier, p.ProfileName, p.Checksum,
|
||||
fleet.MDMOperationTypeRemove, nil, "", "")
|
||||
psb.WriteString("(?, ?, ?, ?, ?, ?, ?, ?, ?),")
|
||||
|
|
@ -1950,7 +2047,7 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
if batchCount >= batchSize {
|
||||
if err := executeUpsertBatch(psb.String(), pargs); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
resetBatch()
|
||||
}
|
||||
|
|
@ -1958,10 +2055,10 @@ func (ds *Datastore) bulkSetPendingMDMAppleHostProfilesDB(
|
|||
|
||||
if batchCount > 0 {
|
||||
if err := executeUpsertBatch(psb.String(), pargs); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return updatedDB, nil
|
||||
}
|
||||
|
||||
// mdmEntityTypeToDynamicNames tracks what names should be used in the
|
||||
|
|
@ -3405,7 +3502,7 @@ func (ds *Datastore) SetOrUpdateMDMAppleSetupAssistant(ctx context.Context, asst
|
|||
// because the updated_at update condition is too complex?), so at the moment
|
||||
// this clears the profile uuids at all times, even if the profile did not
|
||||
// change.
|
||||
if insertOnDuplicateDidUpdate(res) {
|
||||
if insertOnDuplicateDidInsertOrUpdate(res) {
|
||||
// profile was updated, need to clear the profile uuids
|
||||
if err := ds.SetMDMAppleSetupAssistantProfileUUID(ctx, asst.TeamID, "", ""); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "clear mdm apple setup assistant profiles")
|
||||
|
|
@ -3954,7 +4051,9 @@ WHERE h.uuid = ?
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) batchSetMDMAppleDeclarations(ctx context.Context, tx sqlx.ExtContext, tmID *uint, incomingDeclarations []*fleet.MDMAppleDeclaration) ([]*fleet.MDMAppleDeclaration, error) {
|
||||
func (ds *Datastore) batchSetMDMAppleDeclarations(ctx context.Context, tx sqlx.ExtContext, tmID *uint,
|
||||
incomingDeclarations []*fleet.MDMAppleDeclaration,
|
||||
) (declarations []*fleet.MDMAppleDeclaration, updatedDB bool, err error) {
|
||||
const insertStmt = `
|
||||
INSERT INTO mdm_apple_declarations (
|
||||
declaration_uuid,
|
||||
|
|
@ -4021,13 +4120,13 @@ WHERE
|
|||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "build query to load existing declarations")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "build query to load existing declarations")
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, tx, &existingDecls, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "select") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "load existing declarations")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "load existing declarations")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4050,23 +4149,29 @@ WHERE
|
|||
// delete the obsolete declarations (all those that are not in keepNames)
|
||||
stmt, args, err := sqlx.In(fmt.Sprintf(fmtDeleteStmt, andNameNotInList), declTeamID, keepNames)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "build query to delete obsolete profiles")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "build query to delete obsolete profiles")
|
||||
}
|
||||
delStmt = stmt
|
||||
delArgs = args
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, delStmt, delArgs...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "delete") {
|
||||
var result sql.Result
|
||||
if result, err = tx.ExecContext(ctx, delStmt, delArgs...); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr,
|
||||
"delete") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "delete obsolete declarations")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "delete obsolete declarations")
|
||||
}
|
||||
if result != nil {
|
||||
rows, _ := result.RowsAffected()
|
||||
updatedDB = rows > 0
|
||||
}
|
||||
|
||||
for _, d := range incomingDeclarations {
|
||||
checksum := md5ChecksumScriptContent(string(d.RawJSON))
|
||||
declUUID := fleet.MDMAppleDeclarationUUIDPrefix + uuid.NewString()
|
||||
if _, err := tx.ExecContext(ctx, insertStmt,
|
||||
if result, err = tx.ExecContext(ctx, insertStmt,
|
||||
declUUID,
|
||||
d.Identifier,
|
||||
d.Name,
|
||||
|
|
@ -4076,8 +4181,9 @@ WHERE
|
|||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return nil, ctxerr.Wrapf(ctx, err, "insert new/edited declaration with identifier %q", d.Identifier)
|
||||
return nil, false, ctxerr.Wrapf(ctx, err, "insert new/edited declaration with identifier %q", d.Identifier)
|
||||
}
|
||||
updatedDB = updatedDB || insertOnDuplicateDidInsertOrUpdate(result)
|
||||
}
|
||||
|
||||
incomingLabels := []fleet.ConfigurationProfileLabel{}
|
||||
|
|
@ -4092,16 +4198,16 @@ WHERE
|
|||
// optimization for a later iteration.
|
||||
stmt, args, err := sqlx.In(loadExistingDecls, declTeamID, incomingNames)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "build query to load newly inserted declarations")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "build query to load newly inserted declarations")
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, tx, &newlyInsertedDecls, stmt, args...); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "load newly inserted declarations")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "load newly inserted declarations")
|
||||
}
|
||||
|
||||
for _, newlyInsertedDecl := range newlyInsertedDecls {
|
||||
incomingDecl, ok := incomingDecls[newlyInsertedDecl.Name]
|
||||
if !ok {
|
||||
return nil, ctxerr.Wrapf(ctx, err, "declaration %q is in the database but was not incoming", newlyInsertedDecl.Name)
|
||||
return nil, false, ctxerr.Wrapf(ctx, err, "declaration %q is in the database but was not incoming", newlyInsertedDecl.Name)
|
||||
}
|
||||
|
||||
for _, label := range incomingDecl.LabelsIncludeAll {
|
||||
|
|
@ -4116,14 +4222,16 @@ WHERE
|
|||
}
|
||||
}
|
||||
|
||||
if err := batchSetDeclarationLabelAssociationsDB(ctx, tx, incomingLabels); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "labels") {
|
||||
var updatedLabels bool
|
||||
if updatedLabels, err = batchSetDeclarationLabelAssociationsDB(ctx, tx,
|
||||
incomingLabels); err != nil || strings.HasPrefix(ds.testBatchSetMDMAppleProfilesErr, "labels") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMAppleProfilesErr)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "inserting apple declaration label associations")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "inserting apple declaration label associations")
|
||||
}
|
||||
|
||||
return incomingDeclarations, nil
|
||||
return incomingDeclarations, updatedDB || updatedLabels, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) NewMDMAppleDeclaration(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error) {
|
||||
|
|
@ -4220,7 +4328,7 @@ func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insO
|
|||
declaration.LabelsExcludeAny[i].Exclude = true
|
||||
labels = append(labels, declaration.LabelsExcludeAny[i])
|
||||
}
|
||||
if err := batchSetDeclarationLabelAssociationsDB(ctx, tx, labels); err != nil {
|
||||
if _, err := batchSetDeclarationLabelAssociationsDB(ctx, tx, labels); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting mdm declaration label associations")
|
||||
}
|
||||
|
||||
|
|
@ -4234,9 +4342,10 @@ func (ds *Datastore) insertOrUpsertMDMAppleDeclaration(ctx context.Context, insO
|
|||
return declaration, nil
|
||||
}
|
||||
|
||||
func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtContext, declarationLabels []fleet.ConfigurationProfileLabel) error {
|
||||
func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtContext,
|
||||
declarationLabels []fleet.ConfigurationProfileLabel) (updatedDB bool, err error) {
|
||||
if len(declarationLabels) == 0 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// delete any profile+label tuple that is NOT in the list of provided tuples
|
||||
|
|
@ -4258,38 +4367,72 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont
|
|||
exclude = VALUES(exclude)
|
||||
`
|
||||
|
||||
selectStmt := `
|
||||
SELECT apple_declaration_uuid as profile_uuid, label_name, label_id, exclude FROM mdm_declaration_labels
|
||||
WHERE (apple_declaration_uuid, label_name) IN (%s)
|
||||
`
|
||||
|
||||
var (
|
||||
insertBuilder strings.Builder
|
||||
deleteBuilder strings.Builder
|
||||
insertParams []any
|
||||
deleteParams []any
|
||||
insertBuilder strings.Builder
|
||||
selectOrDeleteBuilder strings.Builder
|
||||
selectParams []any
|
||||
insertParams []any
|
||||
deleteParams []any
|
||||
|
||||
setProfileUUIDs = make(map[string]struct{})
|
||||
labelsToInsert = make(map[string]*fleet.ConfigurationProfileLabel, len(declarationLabels))
|
||||
)
|
||||
for i, pl := range declarationLabels {
|
||||
labelsToInsert[fmt.Sprintf("%s\n%s", pl.ProfileUUID, pl.LabelName)] = &declarationLabels[i]
|
||||
if i > 0 {
|
||||
insertBuilder.WriteString(",")
|
||||
deleteBuilder.WriteString(",")
|
||||
selectOrDeleteBuilder.WriteString(",")
|
||||
}
|
||||
insertBuilder.WriteString("(?, ?, ?, ?)")
|
||||
deleteBuilder.WriteString("(?, ?)")
|
||||
selectOrDeleteBuilder.WriteString("(?, ?)")
|
||||
selectParams = append(selectParams, pl.ProfileUUID, pl.LabelName)
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude)
|
||||
deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID)
|
||||
|
||||
setProfileUUIDs[pl.ProfileUUID] = struct{}{}
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, fmt.Sprintf(upsertStmt, insertBuilder.String()), insertParams...)
|
||||
// Determine if we need to update the database
|
||||
var existingProfileLabels []fleet.ConfigurationProfileLabel
|
||||
err = sqlx.SelectContext(ctx, tx, &existingProfileLabels,
|
||||
fmt.Sprintf(selectStmt, selectOrDeleteBuilder.String()), selectParams...)
|
||||
if err != nil {
|
||||
if isChildForeignKeyError(err) {
|
||||
// one of the provided labels doesn't exist
|
||||
return foreignKey("mdm_declaration_labels", fmt.Sprintf("(declaration, label)=(%v)", insertParams))
|
||||
}
|
||||
|
||||
return ctxerr.Wrap(ctx, err, "setting label associations for declarations")
|
||||
return false, ctxerr.Wrap(ctx, err, "selecting existing profile labels")
|
||||
}
|
||||
|
||||
deleteStmt = fmt.Sprintf(deleteStmt, deleteBuilder.String())
|
||||
updateNeeded := false
|
||||
if len(existingProfileLabels) == len(labelsToInsert) {
|
||||
for _, existing := range existingProfileLabels {
|
||||
toInsert, ok := labelsToInsert[fmt.Sprintf("%s\n%s", existing.ProfileUUID, existing.LabelName)]
|
||||
// The fleet.ConfigurationProfileLabel struct has no pointers, so we can use standard cmp.Equal
|
||||
if !ok || !cmp.Equal(existing, *toInsert) {
|
||||
updateNeeded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateNeeded = true
|
||||
}
|
||||
|
||||
if updateNeeded {
|
||||
_, err = tx.ExecContext(ctx, fmt.Sprintf(upsertStmt, insertBuilder.String()), insertParams...)
|
||||
if err != nil {
|
||||
if isChildForeignKeyError(err) {
|
||||
// one of the provided labels doesn't exist
|
||||
return false, foreignKey("mdm_declaration_labels", fmt.Sprintf("(declaration, label)=(%v)", insertParams))
|
||||
}
|
||||
|
||||
return false, ctxerr.Wrap(ctx, err, "setting label associations for declarations")
|
||||
}
|
||||
updatedDB = true
|
||||
}
|
||||
|
||||
deleteStmt = fmt.Sprintf(deleteStmt, selectOrDeleteBuilder.String())
|
||||
|
||||
profUUIDs := make([]string, 0, len(setProfileUUIDs))
|
||||
for k := range setProfileUUIDs {
|
||||
|
|
@ -4299,13 +4442,21 @@ func batchSetDeclarationLabelAssociationsDB(ctx context.Context, tx sqlx.ExtCont
|
|||
|
||||
deleteStmt, args, err := sqlx.In(deleteStmt, deleteArgs...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "sqlx.In delete labels for declarations")
|
||||
return false, ctxerr.Wrap(ctx, err, "sqlx.In delete labels for declarations")
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, deleteStmt, args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "deleting labels for declarations")
|
||||
var result sql.Result
|
||||
if result, err = tx.ExecContext(ctx, deleteStmt, args...); err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "deleting labels for declarations")
|
||||
}
|
||||
if result != nil {
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "count rows affected by insert")
|
||||
}
|
||||
updatedDB = updatedDB || rows > 0
|
||||
}
|
||||
|
||||
return nil
|
||||
return updatedDB, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) MDMAppleDDMDeclarationsToken(ctx context.Context, hostUUID string) (*fleet.MDMAppleDDMDeclarationsToken, error) {
|
||||
|
|
@ -4392,23 +4543,24 @@ func (ds *Datastore) MDMAppleBatchSetHostDeclarationState(ctx context.Context) (
|
|||
|
||||
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
var err error
|
||||
uuids, err = mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, &fleet.MDMDeliveryPending)
|
||||
uuids, _, err = mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, &fleet.MDMDeliveryPending)
|
||||
return err
|
||||
})
|
||||
|
||||
return uuids, ctxerr.Wrap(ctx, err, "upserting host declaration state")
|
||||
}
|
||||
|
||||
func mdmAppleBatchSetHostDeclarationStateDB(ctx context.Context, tx sqlx.ExtContext, batchSize int, status *fleet.MDMDeliveryStatus) ([]string, error) {
|
||||
func mdmAppleBatchSetHostDeclarationStateDB(ctx context.Context, tx sqlx.ExtContext, batchSize int,
|
||||
status *fleet.MDMDeliveryStatus) ([]string, bool, error) {
|
||||
// once all the declarations are in place, compute the desired state
|
||||
// and find which hosts need a DDM sync.
|
||||
changedDeclarations, err := mdmAppleGetHostsWithChangedDeclarationsDB(ctx, tx)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "find hosts with changed declarations")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "find hosts with changed declarations")
|
||||
}
|
||||
|
||||
if len(changedDeclarations) == 0 {
|
||||
return []string{}, nil
|
||||
return []string{}, false, nil
|
||||
}
|
||||
|
||||
// a host might have more than one declaration to sync, we do this to
|
||||
|
|
@ -4430,11 +4582,12 @@ func mdmAppleBatchSetHostDeclarationStateDB(ctx context.Context, tx sqlx.ExtCont
|
|||
// - support the DDM endpoints, which use data from the
|
||||
// `host_mdm_apple_declarations` table to compute which declarations to
|
||||
// serve
|
||||
if err := mdmAppleBatchSetPendingHostDeclarationsDB(ctx, tx, batchSize, changedDeclarations, status); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "batch insert mdm apple host declarations")
|
||||
var updatedDB bool
|
||||
if updatedDB, err = mdmAppleBatchSetPendingHostDeclarationsDB(ctx, tx, batchSize, changedDeclarations, status); err != nil {
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "batch insert mdm apple host declarations")
|
||||
}
|
||||
|
||||
return uuids, nil
|
||||
return uuids, updatedDB, nil
|
||||
}
|
||||
|
||||
// mdmAppleBatchSetPendingHostDeclarationsDB tracks the current status of all
|
||||
|
|
@ -4445,7 +4598,7 @@ func mdmAppleBatchSetPendingHostDeclarationsDB(
|
|||
batchSize int,
|
||||
changedDeclarations []*fleet.MDMAppleHostDeclaration,
|
||||
status *fleet.MDMDeliveryStatus,
|
||||
) error {
|
||||
) (updatedDB bool, err error) {
|
||||
baseStmt := `
|
||||
INSERT INTO host_mdm_apple_declarations
|
||||
(host_uuid, status, operation_type, checksum, declaration_uuid, declaration_identifier, declaration_name)
|
||||
|
|
@ -4457,7 +4610,50 @@ func mdmAppleBatchSetPendingHostDeclarationsDB(
|
|||
checksum = VALUES(checksum)
|
||||
`
|
||||
|
||||
profilesToInsert := make(map[string]*fleet.MDMAppleHostDeclaration)
|
||||
|
||||
executeUpsertBatch := func(valuePart string, args []any) error {
|
||||
// Check if the update needs to be done at all.
|
||||
selectStmt := fmt.Sprintf(`
|
||||
SELECT
|
||||
host_uuid,
|
||||
declaration_uuid,
|
||||
status,
|
||||
COALESCE(operation_type, '') AS operation_type,
|
||||
COALESCE(detail, '') AS detail,
|
||||
checksum,
|
||||
declaration_uuid,
|
||||
declaration_identifier,
|
||||
declaration_name
|
||||
FROM host_mdm_apple_declarations WHERE (host_uuid, declaration_uuid) IN (%s)`,
|
||||
strings.TrimSuffix(strings.Repeat("(?,?),", len(profilesToInsert)), ","))
|
||||
var selectArgs []any
|
||||
for _, p := range profilesToInsert {
|
||||
selectArgs = append(selectArgs, p.HostUUID, p.DeclarationUUID)
|
||||
}
|
||||
var existingProfiles []fleet.MDMAppleHostDeclaration
|
||||
if err := sqlx.SelectContext(ctx, tx, &existingProfiles, selectStmt, selectArgs...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending declarations select existing")
|
||||
}
|
||||
var updateNeeded bool
|
||||
if len(existingProfiles) == len(profilesToInsert) {
|
||||
for _, exist := range existingProfiles {
|
||||
insert, ok := profilesToInsert[fmt.Sprintf("%s\n%s", exist.HostUUID, exist.DeclarationUUID)]
|
||||
if !ok || !exist.Equal(*insert) {
|
||||
updateNeeded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateNeeded = true
|
||||
}
|
||||
clear(profilesToInsert)
|
||||
if !updateNeeded {
|
||||
// All profiles are already in the database, no need to update.
|
||||
return nil
|
||||
}
|
||||
|
||||
updatedDB = true
|
||||
_, err := tx.ExecContext(
|
||||
ctx,
|
||||
fmt.Sprintf(baseStmt, strings.TrimSuffix(valuePart, ",")),
|
||||
|
|
@ -4467,13 +4663,23 @@ func mdmAppleBatchSetPendingHostDeclarationsDB(
|
|||
}
|
||||
|
||||
generateValueArgs := func(d *fleet.MDMAppleHostDeclaration) (string, []any) {
|
||||
profilesToInsert[fmt.Sprintf("%s\n%s", d.HostUUID, d.DeclarationUUID)] = &fleet.MDMAppleHostDeclaration{
|
||||
HostUUID: d.HostUUID,
|
||||
DeclarationUUID: d.DeclarationUUID,
|
||||
Name: d.Name,
|
||||
Identifier: d.Identifier,
|
||||
Status: status,
|
||||
OperationType: d.OperationType,
|
||||
Detail: d.Detail,
|
||||
Checksum: d.Checksum,
|
||||
}
|
||||
valuePart := "(?, ?, ?, ?, ?, ?, ?),"
|
||||
args := []any{d.HostUUID, status, d.OperationType, d.Checksum, d.DeclarationUUID, d.Identifier, d.Name}
|
||||
return valuePart, args
|
||||
}
|
||||
|
||||
err := batchProcessDB(changedDeclarations, batchSize, generateValueArgs, executeUpsertBatch)
|
||||
return ctxerr.Wrap(ctx, err, "inserting changed host declaration state")
|
||||
err = batchProcessDB(changedDeclarations, batchSize, generateValueArgs, executeUpsertBatch)
|
||||
return updatedDB, ctxerr.Wrap(ctx, err, "inserting changed host declaration state")
|
||||
}
|
||||
|
||||
// mdmAppleGetHostsWithChangedDeclarationsDB returns a
|
||||
|
|
|
|||
|
|
@ -1058,7 +1058,9 @@ func expectAppleDeclarations(
|
|||
var got []*fleet.MDMAppleDeclaration
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
ctx := context.Background()
|
||||
return sqlx.SelectContext(ctx, q, &got, `SELECT * FROM mdm_apple_declarations WHERE team_id = ?`, tmID)
|
||||
return sqlx.SelectContext(ctx, q, &got,
|
||||
`SELECT declaration_uuid, team_id, identifier, name, raw_json, checksum, created_at, uploaded_at FROM mdm_apple_declarations WHERE team_id = ?`,
|
||||
tmID)
|
||||
})
|
||||
|
||||
// create map of expected declarations keyed by identifier
|
||||
|
|
@ -4862,8 +4864,11 @@ func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) {
|
|||
Name: "decl-1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
|
||||
updates, err := ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
|
||||
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, "not-exists")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -4880,8 +4885,11 @@ func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
nanoEnroll(t, ds, host1, true)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
|
||||
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -4894,8 +4902,11 @@ func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) {
|
|||
Name: "decl-2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
|
||||
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -4906,8 +4917,11 @@ func testMDMAppleDDMDeclarationsToken(t *testing.T, ds *Datastore) {
|
|||
|
||||
err = ds.DeleteMDMAppleConfigProfile(ctx, decl.DeclarationUUID)
|
||||
require.NoError(t, err)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl2.DeclarationUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
|
||||
toks, err = ds.MDMAppleDDMDeclarationsToken(ctx, host1.UUID)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -6085,8 +6099,11 @@ func testMDMAppleProfilesOnIOSIPadOS(t *testing.T, ds *Datastore) {
|
|||
someProfile, err := ds.NewMDMAppleConfigProfile(ctx, *generateCP("a", "a", 0))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil)
|
||||
updates, err := ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
|
||||
profiles, err := ds.GetHostMDMAppleProfiles(ctx, "iOS0_UUID")
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func (ds *Datastore) CreateOrUpdateCalendarEvent(
|
|||
return ctxerr.Wrap(ctx, err, "insert calendar event")
|
||||
}
|
||||
|
||||
if insertOnDuplicateDidInsert(result) {
|
||||
if insertOnDuplicateDidInsertOrUpdate(result) {
|
||||
id, _ = result.LastInsertId()
|
||||
} else {
|
||||
stmt := `SELECT id FROM calendar_events WHERE email = ?`
|
||||
|
|
|
|||
|
|
@ -6751,6 +6751,7 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
|
|||
InstallScript: "",
|
||||
PreInstallQuery: "",
|
||||
Title: "ChocolateRain",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertSoftwareInstallRequest(context.Background(), host.ID, softwareInstaller, false)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
|
|
@ -121,22 +122,26 @@ func (ds *Datastore) getMDMCommand(ctx context.Context, q sqlx.QueryerContext, c
|
|||
return &cmd, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) error {
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
if err := ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, winProfiles); err != nil {
|
||||
func (ds *Datastore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
|
||||
winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) (updates fleet.MDMProfilesUpdates,
|
||||
err error) {
|
||||
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
var err error
|
||||
if updates.WindowsConfigProfile, err = ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, winProfiles); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "batch set windows profiles")
|
||||
}
|
||||
|
||||
if err := ds.batchSetMDMAppleProfilesDB(ctx, tx, tmID, macProfiles); err != nil {
|
||||
if updates.AppleConfigProfile, err = ds.batchSetMDMAppleProfilesDB(ctx, tx, tmID, macProfiles); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "batch set apple profiles")
|
||||
}
|
||||
|
||||
if _, err := ds.batchSetMDMAppleDeclarations(ctx, tx, tmID, macDeclarations); err != nil {
|
||||
if _, updates.AppleDeclaration, err = ds.batchSetMDMAppleDeclarations(ctx, tx, tmID, macDeclarations); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "batch set apple declarations")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return updates, err
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListMDMConfigProfiles(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
|
||||
|
|
@ -335,10 +340,12 @@ func (ds *Datastore) BulkSetPendingMDMHostProfiles(
|
|||
ctx context.Context,
|
||||
hostIDs, teamIDs []uint,
|
||||
profileUUIDs, hostUUIDs []string,
|
||||
) error {
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return ds.bulkSetPendingMDMHostProfilesDB(ctx, tx, hostIDs, teamIDs, profileUUIDs, hostUUIDs)
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
updates, err = ds.bulkSetPendingMDMHostProfilesDB(ctx, tx, hostIDs, teamIDs, profileUUIDs, hostUUIDs)
|
||||
return err
|
||||
})
|
||||
return updates, err
|
||||
}
|
||||
|
||||
// Note that team ID 0 is used for profiles that apply to hosts in no team
|
||||
|
|
@ -349,7 +356,7 @@ func (ds *Datastore) bulkSetPendingMDMHostProfilesDB(
|
|||
tx sqlx.ExtContext,
|
||||
hostIDs, teamIDs []uint,
|
||||
profileUUIDs, hostUUIDs []string,
|
||||
) error {
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
var (
|
||||
countArgs int
|
||||
macProfUUIDs []string
|
||||
|
|
@ -384,10 +391,10 @@ func (ds *Datastore) bulkSetPendingMDMHostProfilesDB(
|
|||
countArgs++
|
||||
}
|
||||
if countArgs > 1 {
|
||||
return errors.New("only one of hostIDs, teamIDs, profileUUIDs or hostUUIDs can be provided")
|
||||
return updates, errors.New("only one of hostIDs, teamIDs, profileUUIDs or hostUUIDs can be provided")
|
||||
}
|
||||
if countArgs == 0 {
|
||||
return nil
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
var countProfUUIDs int
|
||||
|
|
@ -401,7 +408,7 @@ func (ds *Datastore) bulkSetPendingMDMHostProfilesDB(
|
|||
countProfUUIDs++
|
||||
}
|
||||
if countProfUUIDs > 1 {
|
||||
return errors.New("profile uuids must be all Apple profiles, all Apple declarations, or all Windows profiles")
|
||||
return updates, errors.New("profile uuids must be all Apple profiles, all Apple declarations, or all Windows profiles")
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -471,10 +478,10 @@ WHERE
|
|||
if len(hosts) == 0 && !hasAppleDecls {
|
||||
uuidStmt, args, err := sqlx.In(uuidStmt, args...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "prepare query to load host UUIDs")
|
||||
return updates, ctxerr.Wrap(ctx, err, "prepare query to load host UUIDs")
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, tx, &hosts, uuidStmt, args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "execute query to load host UUIDs")
|
||||
return updates, ctxerr.Wrap(ctx, err, "execute query to load host UUIDs")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -495,12 +502,14 @@ WHERE
|
|||
}
|
||||
}
|
||||
|
||||
if err := ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles")
|
||||
updates.AppleConfigProfile, err = ds.bulkSetPendingMDMAppleHostProfilesDB(ctx, tx, appleHosts)
|
||||
if err != nil {
|
||||
return updates, ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles")
|
||||
}
|
||||
|
||||
if err := ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles")
|
||||
updates.WindowsConfigProfile, err = ds.bulkSetPendingMDMWindowsHostProfilesDB(ctx, tx, winHosts)
|
||||
if err != nil {
|
||||
return updates, ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles")
|
||||
}
|
||||
|
||||
const defaultBatchSize = 1000
|
||||
|
|
@ -513,11 +522,12 @@ WHERE
|
|||
// (and my hunch is that we could even do the same for
|
||||
// profiles) but this could be optimized to use only a provided
|
||||
// set of host uuids.
|
||||
if _, err := mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending apple declarations")
|
||||
_, updates.AppleDeclaration, err = mdmAppleBatchSetHostDeclarationStateDB(ctx, tx, batchSize, nil)
|
||||
if err != nil {
|
||||
return updates, ctxerr.Wrap(ctx, err, "bulk set pending apple declarations")
|
||||
}
|
||||
|
||||
return nil
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify, toFail, toRetry []string) error {
|
||||
|
|
@ -984,9 +994,9 @@ func batchSetProfileLabelAssociationsDB(
|
|||
tx sqlx.ExtContext,
|
||||
profileLabels []fleet.ConfigurationProfileLabel,
|
||||
platform string,
|
||||
) error {
|
||||
) (updatedDB bool, err error) {
|
||||
if len(profileLabels) == 0 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var platformPrefix string
|
||||
|
|
@ -1001,7 +1011,7 @@ func batchSetProfileLabelAssociationsDB(
|
|||
case "windows":
|
||||
platformPrefix = "windows"
|
||||
default:
|
||||
return fmt.Errorf("unsupported platform %s", platform)
|
||||
return false, fmt.Errorf("unsupported platform %s", platform)
|
||||
}
|
||||
|
||||
// delete any profile+label tuple that is NOT in the list of provided tuples
|
||||
|
|
@ -1023,38 +1033,72 @@ func batchSetProfileLabelAssociationsDB(
|
|||
exclude = VALUES(exclude)
|
||||
`
|
||||
|
||||
selectStmt := `
|
||||
SELECT %s_profile_uuid as profile_uuid, label_id, label_name, exclude FROM mdm_configuration_profile_labels
|
||||
WHERE (%s_profile_uuid, label_name) IN (%s)
|
||||
`
|
||||
|
||||
var (
|
||||
insertBuilder strings.Builder
|
||||
deleteBuilder strings.Builder
|
||||
insertParams []any
|
||||
deleteParams []any
|
||||
insertBuilder strings.Builder
|
||||
selectOrDeleteBuilder strings.Builder
|
||||
selectParams []any
|
||||
insertParams []any
|
||||
deleteParams []any
|
||||
|
||||
setProfileUUIDs = make(map[string]struct{})
|
||||
)
|
||||
labelsToInsert := make(map[string]*fleet.ConfigurationProfileLabel, len(profileLabels))
|
||||
for i, pl := range profileLabels {
|
||||
labelsToInsert[fmt.Sprintf("%s\n%s", pl.ProfileUUID, pl.LabelName)] = &profileLabels[i]
|
||||
if i > 0 {
|
||||
insertBuilder.WriteString(",")
|
||||
deleteBuilder.WriteString(",")
|
||||
selectOrDeleteBuilder.WriteString(",")
|
||||
}
|
||||
insertBuilder.WriteString("(?, ?, ?, ?)")
|
||||
deleteBuilder.WriteString("(?, ?)")
|
||||
selectOrDeleteBuilder.WriteString("(?, ?)")
|
||||
selectParams = append(selectParams, pl.ProfileUUID, pl.LabelName)
|
||||
insertParams = append(insertParams, pl.ProfileUUID, pl.LabelID, pl.LabelName, pl.Exclude)
|
||||
deleteParams = append(deleteParams, pl.ProfileUUID, pl.LabelID)
|
||||
|
||||
setProfileUUIDs[pl.ProfileUUID] = struct{}{}
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, fmt.Sprintf(upsertStmt, platformPrefix, insertBuilder.String()), insertParams...)
|
||||
// Determine if we need to update the database
|
||||
var existingProfileLabels []fleet.ConfigurationProfileLabel
|
||||
err = sqlx.SelectContext(ctx, tx, &existingProfileLabels,
|
||||
fmt.Sprintf(selectStmt, platformPrefix, platformPrefix, selectOrDeleteBuilder.String()), selectParams...)
|
||||
if err != nil {
|
||||
if isChildForeignKeyError(err) {
|
||||
// one of the provided labels doesn't exist
|
||||
return foreignKey("mdm_configuration_profile_labels", fmt.Sprintf("(profile, label)=(%v)", insertParams))
|
||||
}
|
||||
|
||||
return ctxerr.Wrap(ctx, err, "setting label associations for profile")
|
||||
return false, ctxerr.Wrap(ctx, err, "selecting existing profile labels")
|
||||
}
|
||||
|
||||
deleteStmt = fmt.Sprintf(deleteStmt, platformPrefix, deleteBuilder.String(), platformPrefix)
|
||||
updateNeeded := false
|
||||
if len(existingProfileLabels) == len(labelsToInsert) {
|
||||
for _, existing := range existingProfileLabels {
|
||||
toInsert, ok := labelsToInsert[fmt.Sprintf("%s\n%s", existing.ProfileUUID, existing.LabelName)]
|
||||
// The fleet.ConfigurationProfileLabel struct has no pointers, so we can use standard cmp.Equal
|
||||
if !ok || !cmp.Equal(existing, *toInsert) {
|
||||
updateNeeded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateNeeded = true
|
||||
}
|
||||
|
||||
if updateNeeded {
|
||||
_, err := tx.ExecContext(ctx, fmt.Sprintf(upsertStmt, platformPrefix, insertBuilder.String()), insertParams...)
|
||||
if err != nil {
|
||||
if isChildForeignKeyError(err) {
|
||||
// one of the provided labels doesn't exist
|
||||
return false, foreignKey("mdm_configuration_profile_labels", fmt.Sprintf("(profile, label)=(%v)", insertParams))
|
||||
}
|
||||
|
||||
return false, ctxerr.Wrap(ctx, err, "setting label associations for profile")
|
||||
}
|
||||
updatedDB = true
|
||||
}
|
||||
|
||||
deleteStmt = fmt.Sprintf(deleteStmt, platformPrefix, selectOrDeleteBuilder.String(), platformPrefix)
|
||||
|
||||
profUUIDs := make([]string, 0, len(setProfileUUIDs))
|
||||
for k := range setProfileUUIDs {
|
||||
|
|
@ -1064,13 +1108,18 @@ func batchSetProfileLabelAssociationsDB(
|
|||
|
||||
deleteStmt, args, err := sqlx.In(deleteStmt, deleteArgs...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "sqlx.In delete labels for profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "sqlx.In delete labels for profiles")
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, deleteStmt, args...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "deleting labels for profiles")
|
||||
var result sql.Result
|
||||
if result, err = tx.ExecContext(ctx, deleteStmt, args...); err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "deleting labels for profiles")
|
||||
}
|
||||
if result != nil {
|
||||
rows, _ := result.RowsAffected()
|
||||
updatedDB = updatedDB || rows > 0
|
||||
}
|
||||
|
||||
return nil
|
||||
return updatedDB, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) MDMGetEULAMetadata(ctx context.Context) (*fleet.MDMEULA, error) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,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"
|
||||
)
|
||||
|
||||
|
|
@ -358,13 +359,15 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
|||
wantApple []*fleet.MDMAppleConfigProfile,
|
||||
wantWindows []*fleet.MDMWindowsConfigProfile,
|
||||
wantAppleDecl []*fleet.MDMAppleDeclaration,
|
||||
wantUpdates fleet.MDMProfilesUpdates,
|
||||
) {
|
||||
ctx := context.Background()
|
||||
err := ds.BatchSetMDMProfiles(ctx, tmID, newAppleSet, newWindowsSet, newAppleDeclSet)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, tmID, newAppleSet, newWindowsSet, newAppleDeclSet)
|
||||
require.NoError(t, err)
|
||||
expectAppleProfiles(t, ds, tmID, wantApple)
|
||||
expectWindowsProfiles(t, ds, tmID, wantWindows)
|
||||
expectAppleDeclarations(t, ds, tmID, wantAppleDecl)
|
||||
assert.Equal(t, wantUpdates, updates)
|
||||
}
|
||||
|
||||
withTeamIDApple := func(p *fleet.MDMAppleConfigProfile, tmID uint) *fleet.MDMAppleConfigProfile {
|
||||
|
|
@ -383,7 +386,7 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
// empty set for no team (both Apple and Windows)
|
||||
applyAndExpect(nil, nil, nil, nil, nil, nil, nil)
|
||||
applyAndExpect(nil, nil, nil, nil, nil, nil, nil, fleet.MDMProfilesUpdates{})
|
||||
|
||||
// single Apple and Windows profile set for a specific team
|
||||
applyAndExpect(
|
||||
|
|
@ -398,6 +401,7 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
|||
withTeamIDWindows(windowsConfigProfileForTest(t, "W1", "l1"), 1),
|
||||
},
|
||||
[]*fleet.MDMAppleDeclaration{withTeamIDDecl(declForTest("D1", "D1", "foo"), 1)},
|
||||
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true},
|
||||
)
|
||||
|
||||
// single Apple and Windows profile set for no team
|
||||
|
|
@ -409,6 +413,7 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
|||
[]*fleet.MDMAppleConfigProfile{configProfileForTest(t, "N1", "I1", "a")},
|
||||
[]*fleet.MDMWindowsConfigProfile{windowsConfigProfileForTest(t, "W1", "l1")},
|
||||
[]*fleet.MDMAppleDeclaration{declForTest("D1", "D1", "foo")},
|
||||
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true},
|
||||
)
|
||||
|
||||
// new Apple and Windows profile sets for a specific team
|
||||
|
|
@ -438,6 +443,7 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
|||
withTeamIDDecl(declForTest("D1", "D1", "foo"), 1),
|
||||
withTeamIDDecl(declForTest("D2", "D2", "foo"), 1),
|
||||
},
|
||||
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true},
|
||||
)
|
||||
|
||||
// edited profiles, unchanged profiles, and new profiles for a specific team
|
||||
|
|
@ -473,6 +479,7 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
|||
withTeamIDDecl(declForTest("D2", "D2", "foo"), 1),
|
||||
withTeamIDDecl(declForTest("D3", "D3", "bar"), 1),
|
||||
},
|
||||
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true},
|
||||
)
|
||||
|
||||
// new Apple and Windows profiles to no team
|
||||
|
|
@ -502,10 +509,43 @@ func testBatchSetMDMProfiles(t *testing.T, ds *Datastore) {
|
|||
declForTest("D5", "D4", "foo"),
|
||||
declForTest("D4", "D5", "foo"),
|
||||
},
|
||||
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true},
|
||||
)
|
||||
|
||||
// Apply the same profiles again -- no update should be detected
|
||||
applyAndExpect(
|
||||
[]*fleet.MDMAppleConfigProfile{
|
||||
configProfileForTest(t, "N4", "I4", "d"),
|
||||
configProfileForTest(t, "N5", "I5", "e"),
|
||||
},
|
||||
[]*fleet.MDMWindowsConfigProfile{
|
||||
windowsConfigProfileForTest(t, "W4", "l4"),
|
||||
windowsConfigProfileForTest(t, "W5", "l5"),
|
||||
},
|
||||
[]*fleet.MDMAppleDeclaration{
|
||||
declForTest("D5", "D4", "foo"),
|
||||
declForTest("D4", "D5", "foo"),
|
||||
},
|
||||
nil,
|
||||
[]*fleet.MDMAppleConfigProfile{
|
||||
configProfileForTest(t, "N4", "I4", "d"),
|
||||
configProfileForTest(t, "N5", "I5", "e"),
|
||||
},
|
||||
[]*fleet.MDMWindowsConfigProfile{
|
||||
windowsConfigProfileForTest(t, "W4", "l4"),
|
||||
windowsConfigProfileForTest(t, "W5", "l5"),
|
||||
},
|
||||
[]*fleet.MDMAppleDeclaration{
|
||||
declForTest("D5", "D4", "foo"),
|
||||
declForTest("D4", "D5", "foo"),
|
||||
},
|
||||
fleet.MDMProfilesUpdates{AppleConfigProfile: false, WindowsConfigProfile: false, AppleDeclaration: false},
|
||||
)
|
||||
|
||||
// Test Case 8: Clear profiles for a specific team
|
||||
applyAndExpect(nil, nil, nil, ptr.Uint(1), nil, nil, nil)
|
||||
applyAndExpect(nil, nil, nil, ptr.Uint(1), nil, nil, nil,
|
||||
fleet.MDMProfilesUpdates{AppleConfigProfile: true, WindowsConfigProfile: true, AppleDeclaration: true},
|
||||
)
|
||||
}
|
||||
|
||||
func testListMDMConfigProfiles(t *testing.T, ds *Datastore) {
|
||||
|
|
@ -1063,17 +1103,24 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
// bulk set for no target ids, does nothing
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, nil, nil)
|
||||
updates, err := ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
|
||||
// bulk set for combination of target ids, not allowed
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{1}, []uint{2}, nil, nil)
|
||||
_, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{1}, []uint{2}, nil, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
// bulk set for all created hosts, no profiles yet so nothing changed
|
||||
allHosts := append(darwinHosts, unenrolledHost, linuxHost)
|
||||
allHosts = append(allHosts, windowsHosts...)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(allHosts...), nil, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(allHosts...), nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {},
|
||||
darwinHosts[1]: {},
|
||||
|
|
@ -1100,7 +1147,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "G2w", "L2"),
|
||||
windowsConfigProfileForTest(t, "G3w", "L3"),
|
||||
}
|
||||
err = ds.BatchSetMDMProfiles(
|
||||
updates, err = ds.BatchSetMDMProfiles(
|
||||
ctx,
|
||||
nil,
|
||||
macGlobalProfiles,
|
||||
|
|
@ -1113,6 +1160,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, macGlobalProfiles, 3)
|
||||
globalProfiles := getProfs(nil)
|
||||
require.Len(t, globalProfiles, 8)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
|
||||
// list profiles to install, should result in the global profiles for all
|
||||
// enrolled hosts
|
||||
|
|
@ -1132,8 +1182,11 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, toRemoveWindows, 0)
|
||||
|
||||
// bulk set for all created hosts, enrolled hosts get the no-team profiles
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(allHosts...), nil, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, hostIDsFromHosts(allHosts...), nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
|
|
@ -1311,7 +1364,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, toRemoveWindows, 3)
|
||||
|
||||
// update status of the moved host (team has no profiles)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
hostIDsFromHosts(darwinHosts[0], windowsHosts[0]),
|
||||
nil,
|
||||
|
|
@ -1319,6 +1372,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
|
|
@ -1482,7 +1538,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, toRemoveWindows, 3)
|
||||
|
||||
// update status of the moved host via its uuid (team has no profiles)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
nil,
|
||||
nil,
|
||||
|
|
@ -1490,6 +1546,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
[]string{darwinHosts[1].UUID, windowsHosts[1].UUID},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
|
|
@ -1620,8 +1679,11 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "T1.1w", "T1.1"),
|
||||
windowsConfigProfileForTest(t, "T1.2w", "T1.2"),
|
||||
}
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team1.ID, tm1DarwinProfiles, tm1WindowsProfiles, nil)
|
||||
updates, err = ds.BatchSetMDMProfiles(ctx, &team1.ID, tm1DarwinProfiles, tm1WindowsProfiles, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
|
||||
tm1Profiles := getProfs(&team1.ID)
|
||||
require.Len(t, tm1Profiles, 4)
|
||||
|
|
@ -1644,8 +1706,11 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, toRemoveWindows, 0)
|
||||
|
||||
// update status of the affected team
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
{
|
||||
|
|
@ -1827,15 +1892,21 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "T1.3w", "T1.3"),
|
||||
}
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team1.ID, newTm1DarwinProfiles, newTm1WindowsProfiles, nil)
|
||||
updates, err = ds.BatchSetMDMProfiles(ctx, &team1.ID, newTm1DarwinProfiles, newTm1WindowsProfiles, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
newTm1Profiles := getProfs(&team1.ID)
|
||||
require.Len(t, newTm1Profiles, 4)
|
||||
|
||||
// update status of the affected team
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -1974,6 +2045,13 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
})
|
||||
|
||||
// update again -- nothing should change
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// re-add tm1Profiles[0] to list of team1 profiles (T1.1 on Apple, T1.2 on Windows)
|
||||
// NOTE: even though it is the same profile, it's unique DB ID is different because
|
||||
// it got deleted and re-inserted from the team's profiles, so this is reflected in
|
||||
|
|
@ -1989,14 +2067,20 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "T1.3w", "T1.3"),
|
||||
}
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team1.ID, newTm1DarwinProfiles, newTm1WindowsProfiles, nil)
|
||||
updates, err = ds.BatchSetMDMProfiles(ctx, &team1.ID, newTm1DarwinProfiles, newTm1WindowsProfiles, nil)
|
||||
require.NoError(t, err)
|
||||
newTm1Profiles = getProfs(&team1.ID)
|
||||
require.Len(t, newTm1Profiles, 6)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// update status of the affected team
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team1.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -2154,15 +2238,21 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
// TODO(roberto): add new darwin declarations for this and all subsequent assertions
|
||||
err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil)
|
||||
updates, err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
|
||||
newGlobalProfiles := getProfs(nil)
|
||||
require.Len(t, newGlobalProfiles, 6)
|
||||
|
||||
// update status of the affected "no-team"
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{0}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
|
||||
require.NoError(t, ds.MDMAppleStoreDDMStatusReport(ctx, darwinHosts[0].UUID, nil))
|
||||
require.NoError(t, ds.MDMAppleStoreDDMStatusReport(ctx, darwinHosts[1].UUID, nil))
|
||||
|
|
@ -2289,15 +2379,21 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "G5w", "G5"),
|
||||
}
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil)
|
||||
updates, err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil)
|
||||
require.NoError(t, err)
|
||||
newGlobalProfiles = getProfs(nil)
|
||||
require.Len(t, newGlobalProfiles, 8)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// bulk-set only those affected by the new Apple global profile
|
||||
newDarwinProfileUUID := newGlobalProfiles[3].ProfileUUID
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newDarwinProfileUUID}, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newDarwinProfileUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -2407,8 +2503,11 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
|
||||
// bulk-set only those affected by the new Apple global profile
|
||||
newWindowsProfileUUID := newGlobalProfiles[7].ProfileUUID
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newWindowsProfileUUID}, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newWindowsProfileUUID}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -2531,14 +2630,20 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "T2.1w", "T2.1"),
|
||||
}
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team2.ID, tm2DarwinProfiles, tm2WindowsProfiles, nil)
|
||||
updates, err = ds.BatchSetMDMProfiles(ctx, &team2.ID, tm2DarwinProfiles, tm2WindowsProfiles, nil)
|
||||
require.NoError(t, err)
|
||||
tm2Profiles := getProfs(&team2.ID)
|
||||
require.Len(t, tm2Profiles, 2)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// update status via tm2 id and the global 0 id to test that custom sql statement
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID, 0}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID, 0}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -2714,7 +2819,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "G7w", "G7", labels[5]),
|
||||
}
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil)
|
||||
updates, err = ds.BatchSetMDMProfiles(ctx, nil, newDarwinGlobalProfiles, newWindowsGlobalProfiles, nil)
|
||||
require.NoError(t, err)
|
||||
newGlobalProfiles = getProfs(nil)
|
||||
require.Len(t, newGlobalProfiles, 12)
|
||||
|
|
@ -2723,6 +2828,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
setProfileLabels(t, newGlobalProfiles[5], labels[2])
|
||||
setProfileLabels(t, newGlobalProfiles[10], labels[3], labels[4])
|
||||
setProfileLabels(t, newGlobalProfiles[11], labels[5])
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// simulate an entry with some values set to NULL
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
|
|
@ -2737,7 +2845,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
// do a sync of all hosts, should not change anything as no host is a member
|
||||
// of the new label-based profiles (indices change due to new Apple and
|
||||
// Windows profiles)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
hostIDsFromHosts(
|
||||
append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...),
|
||||
|
|
@ -2746,6 +2854,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -2912,7 +3023,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
|
||||
// do a full sync, the new global hosts get the standard global profiles and
|
||||
// also the label-based profile that they are a member of
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
hostIDsFromHosts(
|
||||
append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...),
|
||||
|
|
@ -2921,6 +3032,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -3117,7 +3231,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// do a sync of those hosts, they will get the two label-based profiles of their platform
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
hostIDsFromHosts(darwinHosts[2], windowsHosts[2]),
|
||||
nil,
|
||||
|
|
@ -3125,6 +3239,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -3327,7 +3444,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, ds.DeleteLabel(ctx, labels[3].Name))
|
||||
|
||||
// sync the affected profiles
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
nil,
|
||||
nil,
|
||||
|
|
@ -3335,7 +3452,10 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
nil,
|
||||
nil,
|
||||
|
|
@ -3343,6 +3463,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// nothing changes - broken label-based profiles are simply ignored
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
|
|
@ -3551,7 +3674,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
hostIDsFromHosts(darwinHosts[2], windowsHosts[2]),
|
||||
nil,
|
||||
|
|
@ -3559,6 +3682,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -3756,7 +3882,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
setProfileLabels(t, newGlobalProfiles[4], labels[1])
|
||||
setProfileLabels(t, newGlobalProfiles[10], labels[4])
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
nil,
|
||||
nil,
|
||||
|
|
@ -3764,7 +3890,10 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
nil,
|
||||
nil,
|
||||
|
|
@ -3772,6 +3901,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -3969,18 +4101,24 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "T2.2w", "T2.2", labels[4], labels[5]),
|
||||
}
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team2.ID, tm2DarwinProfiles, tm2WindowsProfiles, nil)
|
||||
updates, err = ds.BatchSetMDMProfiles(ctx, &team2.ID, tm2DarwinProfiles, tm2WindowsProfiles, nil)
|
||||
require.NoError(t, err)
|
||||
tm2Profiles = getProfs(&team2.ID)
|
||||
require.Len(t, tm2Profiles, 4)
|
||||
// TODO(mna): temporary until BatchSetMDMProfiles supports labels
|
||||
setProfileLabels(t, tm2Profiles[1], labels[1], labels[2])
|
||||
setProfileLabels(t, tm2Profiles[3], labels[4], labels[5])
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// sync team 2, no changes because no host is a member of the labels (except
|
||||
// index change due to new profiles)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -4178,8 +4316,11 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// sync team 2, the label-based profile of team2 is now pending install
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -4388,8 +4529,11 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
|
||||
// sync team 2, the label-based profile of team2 is left untouched (broken
|
||||
// profiles are ignored)
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -4603,8 +4747,11 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
|
||||
// sync team 2, the label-based profile of team2 is still left untouched
|
||||
// because even if the hosts are not members anymore, the profile is broken
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -4808,8 +4955,11 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
|
||||
// sync team 2, now it sees that the hosts are not members and the profile
|
||||
// gets removed
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{team2.ID}, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -5003,7 +5153,7 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
|
||||
// sanity-check, a full sync does not change anything
|
||||
err = ds.BulkSetPendingMDMHostProfiles(
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(
|
||||
ctx,
|
||||
hostIDsFromHosts(
|
||||
append(darwinHosts, append(windowsHosts, unenrolledHost, linuxHost)...)...),
|
||||
|
|
@ -5012,6 +5162,9 @@ func testBulkSetPendingMDMHostProfiles(t *testing.T, ds *Datastore) {
|
|||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
darwinHosts[0]: {
|
||||
|
|
@ -5237,8 +5390,11 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
|||
configProfileForTest(t, "T1.2", "T1.2", "e"),
|
||||
}
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -5275,8 +5431,11 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
|||
label, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
var uid string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
|
|
@ -5354,8 +5513,11 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
|||
testLabel3, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_3"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
var uid string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
|
|
@ -5444,8 +5606,11 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
|||
testLabel4, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_4"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, profiles, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
var uid string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
|
|
@ -5524,8 +5689,11 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
|||
windowsConfigProfileForTest(t, "T5.2", "T5.2"),
|
||||
}
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
profs, _, err := ds.ListMDMConfigProfiles(ctx, &team.ID, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -5562,8 +5730,11 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
|||
label, err := ds.NewLabel(ctx, &fleet.Label{Name: "test_label_6"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
var uid string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
|
|
@ -5641,8 +5812,11 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
|||
testLabel3, err := ds.NewLabel(ctx, &fleet.Label{Name: uuid.NewString()})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
var uid string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
|
|
@ -5731,8 +5905,11 @@ func testGetHostMDMProfilesExpectedForVerification(t *testing.T, ds *Datastore)
|
|||
label, err := ds.NewLabel(ctx, &fleet.Label{Name: uuid.NewString()})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, &team.ID, nil, profiles, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
var uid string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
|
|
@ -5947,18 +6124,16 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
wantOtherWin := []fleet.ConfigurationProfileLabel{
|
||||
{ProfileUUID: otherWinProfile.ProfileUUID, LabelName: label.Name, LabelID: label.ID},
|
||||
}
|
||||
require.NoError(
|
||||
t,
|
||||
batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherWin, "windows"),
|
||||
)
|
||||
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherWin, "windows")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updatedDB)
|
||||
// make it an "exclude" label on the other macos profile
|
||||
wantOtherMac := []fleet.ConfigurationProfileLabel{
|
||||
{ProfileUUID: otherMacProfile.ProfileUUID, LabelName: label.Name, LabelID: label.ID, Exclude: true},
|
||||
}
|
||||
require.NoError(
|
||||
t,
|
||||
batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherMac, "darwin"),
|
||||
)
|
||||
updatedDB, err = batchSetProfileLabelAssociationsDB(ctx, ds.writer(ctx), wantOtherMac, "darwin")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updatedDB)
|
||||
|
||||
platforms := map[string]string{
|
||||
"darwin": macOSProfile.ProfileUUID,
|
||||
|
|
@ -5991,7 +6166,10 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
t.Run("empty input "+platform, func(t *testing.T) {
|
||||
want := []fleet.ConfigurationProfileLabel{}
|
||||
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, want, platform)
|
||||
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, want, platform)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updatedDB)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expectLabels(t, uuid, platform, want)
|
||||
|
|
@ -6005,7 +6183,10 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID},
|
||||
}
|
||||
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updatedDB)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expectLabels(t, uuid, platform, profileLabels)
|
||||
|
|
@ -6018,7 +6199,10 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID, Exclude: true},
|
||||
}
|
||||
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updatedDB)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expectLabels(t, uuid, platform, profileLabels)
|
||||
|
|
@ -6033,7 +6217,8 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, platform)
|
||||
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, platform)
|
||||
return err
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
|
@ -6044,7 +6229,8 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
{ProfileUUID: uuid, LabelName: label.Name, LabelID: 12345},
|
||||
}
|
||||
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, platform)
|
||||
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, platform)
|
||||
return err
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
|
|
@ -6053,7 +6239,8 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
{ProfileUUID: uuid, LabelName: "xyz", LabelID: 1235},
|
||||
}
|
||||
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, platform)
|
||||
_, err := batchSetProfileLabelAssociationsDB(ctx, tx, invalidProfileLabels, platform)
|
||||
return err
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
|
@ -6074,7 +6261,10 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
{ProfileUUID: uuid, LabelName: newLabel.Name, LabelID: newLabel.ID, Exclude: true},
|
||||
}
|
||||
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updatedDB)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// both are stored in the DB
|
||||
|
|
@ -6085,7 +6275,10 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
{ProfileUUID: uuid, LabelName: label.Name, LabelID: label.ID},
|
||||
}
|
||||
err = ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
updatedDB, err := batchSetProfileLabelAssociationsDB(ctx, tx, profileLabels, platform)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updatedDB)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expectLabels(t, uuid, platform, profileLabels)
|
||||
|
|
@ -6098,12 +6291,13 @@ func testBatchSetProfileLabelAssociations(t *testing.T, ds *Datastore) {
|
|||
|
||||
t.Run("unsupported platform", func(t *testing.T) {
|
||||
err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return batchSetProfileLabelAssociationsDB(
|
||||
_, err := batchSetProfileLabelAssociationsDB(
|
||||
ctx,
|
||||
tx,
|
||||
[]fleet.ConfigurationProfileLabel{{}},
|
||||
"unsupported",
|
||||
)
|
||||
return err
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
|
@ -6185,7 +6379,7 @@ func testBatchSetMDMProfilesTransactionError(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "W2", "l2"),
|
||||
}
|
||||
// set the initial profiles without error
|
||||
err := ds.BatchSetMDMProfiles(ctx, nil, appleProfs, winProfs, nil)
|
||||
_, err := ds.BatchSetMDMProfiles(ctx, nil, appleProfs, winProfs, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// now ensure all steps are required (add a profile, delete a profile, set labels)
|
||||
|
|
@ -6201,7 +6395,7 @@ func testBatchSetMDMProfilesTransactionError(t *testing.T, ds *Datastore) {
|
|||
ds.testBatchSetMDMAppleProfilesErr = c.appleErr
|
||||
ds.testBatchSetMDMWindowsProfilesErr = c.windowsErr
|
||||
|
||||
err = ds.BatchSetMDMProfiles(ctx, nil, appleProfs, winProfs, nil)
|
||||
_, err = ds.BatchSetMDMProfiles(ctx, nil, appleProfs, winProfs, nil)
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
})
|
||||
}
|
||||
|
|
@ -7139,8 +7333,11 @@ func testBulkSetPendingMDMHostProfilesExcludeAny(t *testing.T, ds *Datastore) {
|
|||
declForTest("D1", "D1", "{}", labels[3], labels[4], labels[5]),
|
||||
}
|
||||
|
||||
err := ds.BatchSetMDMProfiles(ctx, nil, appleProfs, windowsProfs, appleDecls)
|
||||
updates, err := ds.BatchSetMDMProfiles(ctx, nil, appleProfs, windowsProfs, appleDecls)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
|
||||
// must reload them to get the profile/declaration uuid
|
||||
getProfs := func(teamID *uint) []*fleet.MDMConfigProfilePayload {
|
||||
|
|
@ -7185,8 +7382,11 @@ func testBulkSetPendingMDMHostProfilesExcludeAny(t *testing.T, ds *Datastore) {
|
|||
|
||||
// do a sync, they get all platform-specific profiles since they are not part
|
||||
// of any label
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
appleHost: {
|
||||
|
|
@ -7225,8 +7425,11 @@ func testBulkSetPendingMDMHostProfilesExcludeAny(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
appleHost: {
|
||||
|
|
@ -7257,8 +7460,11 @@ func testBulkSetPendingMDMHostProfilesExcludeAny(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updates.AppleConfigProfile)
|
||||
assert.True(t, updates.WindowsConfigProfile)
|
||||
assert.True(t, updates.AppleDeclaration)
|
||||
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
appleHost: {
|
||||
|
|
@ -7293,8 +7499,11 @@ func testBulkSetPendingMDMHostProfilesExcludeAny(t *testing.T, ds *Datastore) {
|
|||
err = ds.DeleteLabel(ctx, labels[3].Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// broken profiles do not get reported as "to remove"
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
|
|
@ -7345,8 +7554,11 @@ func testBulkSetPendingMDMHostProfilesExcludeAny(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
nanoEnroll(t, ds, appleHost2, false)
|
||||
|
||||
err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID, winHost2.ID, appleHost2.ID}, nil, nil, nil)
|
||||
updates, err = ds.BulkSetPendingMDMHostProfiles(ctx, []uint{winHost.ID, appleHost.ID, winHost2.ID, appleHost2.ID}, nil, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updates.AppleConfigProfile)
|
||||
assert.False(t, updates.WindowsConfigProfile)
|
||||
assert.False(t, updates.AppleDeclaration)
|
||||
|
||||
// broken profiles do not get reported as "to install"
|
||||
assertHostProfiles(t, ds, map[*fleet.Host][]anyProfile{
|
||||
|
|
|
|||
|
|
@ -1581,7 +1581,7 @@ INSERT INTO
|
|||
cp.LabelsExcludeAny[i].Exclude = true
|
||||
labels = append(labels, cp.LabelsExcludeAny[i])
|
||||
}
|
||||
if err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, "windows"); err != nil {
|
||||
if _, err := batchSetProfileLabelAssociationsDB(ctx, tx, labels, "windows"); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting windows profile label associations")
|
||||
}
|
||||
|
||||
|
|
@ -1653,7 +1653,7 @@ func (ds *Datastore) batchSetMDMWindowsProfilesDB(
|
|||
tx sqlx.ExtContext,
|
||||
tmID *uint,
|
||||
profiles []*fleet.MDMWindowsConfigProfile,
|
||||
) error {
|
||||
) (updatedDB bool, err error) {
|
||||
const loadExistingProfiles = `
|
||||
SELECT
|
||||
name,
|
||||
|
|
@ -1721,13 +1721,13 @@ ON DUPLICATE KEY UPDATE
|
|||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "build query to load existing profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "build query to load existing profiles")
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, tx, &existingProfiles, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "select") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "load existing profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "load existing profiles")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1748,40 +1748,48 @@ ON DUPLICATE KEY UPDATE
|
|||
var (
|
||||
stmt string
|
||||
args []interface{}
|
||||
err error
|
||||
)
|
||||
// delete the obsolete profiles (all those that are not in keepNames)
|
||||
var result sql.Result
|
||||
if len(keepNames) > 0 {
|
||||
stmt, args, err = sqlx.In(deleteProfilesNotInList, profTeamID, keepNames)
|
||||
if err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "indelete") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "build statement to delete obsolete profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "build statement to delete obsolete profiles")
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "delete") {
|
||||
if result, err = tx.ExecContext(ctx, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr,
|
||||
"delete") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "delete obsolete profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "delete obsolete profiles")
|
||||
}
|
||||
} else {
|
||||
if _, err := tx.ExecContext(ctx, deleteAllProfilesForTeam, profTeamID); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "delete") {
|
||||
if result, err = tx.ExecContext(ctx, deleteAllProfilesForTeam,
|
||||
profTeamID); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "delete") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "delete all profiles for team")
|
||||
return false, ctxerr.Wrap(ctx, err, "delete all profiles for team")
|
||||
}
|
||||
}
|
||||
if result != nil {
|
||||
rows, _ := result.RowsAffected()
|
||||
updatedDB = rows > 0
|
||||
}
|
||||
|
||||
// insert the new profiles and the ones that have changed
|
||||
for _, p := range incomingProfs {
|
||||
if _, err := tx.ExecContext(ctx, insertNewOrEditedProfile, profTeamID, p.Name, p.SyncML); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "insert") {
|
||||
if result, err = tx.ExecContext(ctx, insertNewOrEditedProfile, profTeamID, p.Name,
|
||||
p.SyncML); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "insert") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrapf(ctx, err, "insert new/edited profile with name %q", p.Name)
|
||||
return false, ctxerr.Wrapf(ctx, err, "insert new/edited profile with name %q", p.Name)
|
||||
}
|
||||
updatedDB = updatedDB || insertOnDuplicateDidInsertOrUpdate(result)
|
||||
}
|
||||
|
||||
// build a list of labels so the associations can be batch-set all at once
|
||||
|
|
@ -1797,19 +1805,19 @@ ON DUPLICATE KEY UPDATE
|
|||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "build query to load newly inserted profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "build query to load newly inserted profiles")
|
||||
}
|
||||
if err := sqlx.SelectContext(ctx, tx, &newlyInsertedProfs, stmt, args...); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "reselect") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "load newly inserted profiles")
|
||||
return false, ctxerr.Wrap(ctx, err, "load newly inserted profiles")
|
||||
}
|
||||
|
||||
for _, newlyInsertedProf := range newlyInsertedProfs {
|
||||
incomingProf, ok := incomingProfs[newlyInsertedProf.Name]
|
||||
if !ok {
|
||||
return ctxerr.Wrapf(ctx, err, "profile %q is in the database but was not incoming", newlyInsertedProf.Name)
|
||||
return false, ctxerr.Wrapf(ctx, err, "profile %q is in the database but was not incoming", newlyInsertedProf.Name)
|
||||
}
|
||||
|
||||
for _, label := range incomingProf.LabelsIncludeAll {
|
||||
|
|
@ -1825,47 +1833,56 @@ ON DUPLICATE KEY UPDATE
|
|||
}
|
||||
|
||||
// insert/delete the label associations
|
||||
if err := batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels, "windows"); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "labels") {
|
||||
var updatedLabels bool
|
||||
if updatedLabels, err = batchSetProfileLabelAssociationsDB(ctx, tx, incomingLabels,
|
||||
"windows"); err != nil || strings.HasPrefix(ds.testBatchSetMDMWindowsProfilesErr, "labels") {
|
||||
if err == nil {
|
||||
err = errors.New(ds.testBatchSetMDMWindowsProfilesErr)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "inserting windows profile label associations")
|
||||
return false, ctxerr.Wrap(ctx, err, "inserting windows profile label associations")
|
||||
}
|
||||
|
||||
return nil
|
||||
return updatedDB || updatedLabels, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) bulkSetPendingMDMWindowsHostProfilesDB(
|
||||
ctx context.Context,
|
||||
tx sqlx.ExtContext,
|
||||
uuids []string,
|
||||
) error {
|
||||
) (updatedDB bool, err error) {
|
||||
if len(uuids) == 0 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
profilesToInstall, err := listMDMWindowsProfilesToInstallDB(ctx, tx, uuids)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "list profiles to install")
|
||||
return false, ctxerr.Wrap(ctx, err, "list profiles to install")
|
||||
}
|
||||
|
||||
profilesToRemove, err := listMDMWindowsProfilesToRemoveDB(ctx, tx, uuids)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "list profiles to remove")
|
||||
return false, ctxerr.Wrap(ctx, err, "list profiles to remove")
|
||||
}
|
||||
|
||||
if len(profilesToInstall) == 0 && len(profilesToRemove) == 0 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := ds.bulkDeleteMDMWindowsHostsConfigProfilesDB(ctx, tx, profilesToRemove); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk delete profiles to remove")
|
||||
if len(profilesToRemove) > 0 {
|
||||
if err := ds.bulkDeleteMDMWindowsHostsConfigProfilesDB(ctx, tx, profilesToRemove); err != nil {
|
||||
return false, ctxerr.Wrap(ctx, err, "bulk delete profiles to remove")
|
||||
}
|
||||
updatedDB = true
|
||||
}
|
||||
if len(profilesToInstall) == 0 {
|
||||
return updatedDB, nil
|
||||
}
|
||||
|
||||
var (
|
||||
pargs []any
|
||||
psb strings.Builder
|
||||
batchCount int
|
||||
pargs []any
|
||||
profilesToInsert = make(map[string]*fleet.MDMWindowsProfilePayload)
|
||||
psb strings.Builder
|
||||
batchCount int
|
||||
)
|
||||
|
||||
const defaultBatchSize = 1000
|
||||
|
|
@ -1877,10 +1894,48 @@ func (ds *Datastore) bulkSetPendingMDMWindowsHostProfilesDB(
|
|||
resetBatch := func() {
|
||||
batchCount = 0
|
||||
pargs = pargs[:0]
|
||||
clear(profilesToInsert)
|
||||
psb.Reset()
|
||||
}
|
||||
|
||||
executeUpsertBatch := func(valuePart string, args []any) error {
|
||||
// Check if the update needs to be done at all.
|
||||
selectStmt := fmt.Sprintf(`
|
||||
SELECT
|
||||
profile_uuid,
|
||||
host_uuid,
|
||||
status,
|
||||
COALESCE(operation_type, '') AS operation_type,
|
||||
COALESCE(detail, '') AS detail,
|
||||
COALESCE(command_uuid, '') AS command_uuid,
|
||||
COALESCE(profile_name, '') AS profile_name
|
||||
FROM host_mdm_windows_profiles WHERE (profile_uuid, host_uuid) IN (%s)`,
|
||||
strings.TrimSuffix(strings.Repeat("(?,?),", len(profilesToInsert)), ","))
|
||||
var selectArgs []any
|
||||
for _, p := range profilesToInsert {
|
||||
selectArgs = append(selectArgs, p.ProfileUUID, p.HostUUID)
|
||||
}
|
||||
var existingProfiles []fleet.MDMWindowsProfilePayload
|
||||
if err := sqlx.SelectContext(ctx, tx, &existingProfiles, selectStmt, selectArgs...); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending profile status select existing")
|
||||
}
|
||||
var updateNeeded bool
|
||||
if len(existingProfiles) == len(profilesToInsert) {
|
||||
for _, exist := range existingProfiles {
|
||||
insert, ok := profilesToInsert[fmt.Sprintf("%s\n%s", exist.ProfileUUID, exist.HostUUID)]
|
||||
if !ok || !exist.Equal(*insert) {
|
||||
updateNeeded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateNeeded = true
|
||||
}
|
||||
if !updateNeeded {
|
||||
// All profiles are already in the database, no need to update.
|
||||
return nil
|
||||
}
|
||||
|
||||
baseStmt := fmt.Sprintf(`
|
||||
INSERT INTO host_mdm_windows_profiles (
|
||||
profile_uuid,
|
||||
|
|
@ -1898,11 +1953,25 @@ func (ds *Datastore) bulkSetPendingMDMWindowsHostProfilesDB(
|
|||
detail = ''
|
||||
`, strings.TrimSuffix(valuePart, ","))
|
||||
|
||||
_, err = tx.ExecContext(ctx, baseStmt, args...)
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute batch")
|
||||
_, err := tx.ExecContext(ctx, baseStmt, args...)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending profile status execute batch")
|
||||
}
|
||||
updatedDB = true
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range profilesToInstall {
|
||||
profilesToInsert[fmt.Sprintf("%s\n%s", p.ProfileUUID, p.HostUUID)] = &fleet.MDMWindowsProfilePayload{
|
||||
ProfileUUID: p.ProfileUUID,
|
||||
ProfileName: p.ProfileName,
|
||||
HostUUID: p.HostUUID,
|
||||
Status: nil,
|
||||
OperationType: fleet.MDMOperationTypeInstall,
|
||||
Detail: p.Detail,
|
||||
CommandUUID: p.CommandUUID,
|
||||
Retries: p.Retries,
|
||||
}
|
||||
pargs = append(
|
||||
pargs, p.ProfileUUID, p.HostUUID, p.ProfileName,
|
||||
fleet.MDMOperationTypeInstall)
|
||||
|
|
@ -1910,7 +1979,7 @@ func (ds *Datastore) bulkSetPendingMDMWindowsHostProfilesDB(
|
|||
batchCount++
|
||||
if batchCount >= batchSize {
|
||||
if err := executeUpsertBatch(psb.String(), pargs); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
resetBatch()
|
||||
}
|
||||
|
|
@ -1918,11 +1987,11 @@ func (ds *Datastore) bulkSetPendingMDMWindowsHostProfilesDB(
|
|||
|
||||
if batchCount > 0 {
|
||||
if err := executeUpsertBatch(psb.String(), pargs); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return updatedDB, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetHostMDMWindowsProfiles(ctx context.Context, hostUUID string) ([]fleet.HostMDMWindowsProfile, error) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -1885,7 +1886,7 @@ func testSetOrReplaceMDMWindowsConfigProfile(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &prof,
|
||||
`SELECT * FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`,
|
||||
`SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`,
|
||||
teamID, name)
|
||||
})
|
||||
return &prof
|
||||
|
|
@ -1983,7 +1984,9 @@ func expectWindowsProfiles(
|
|||
var got []*fleet.MDMWindowsConfigProfile
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
ctx := context.Background()
|
||||
return sqlx.SelectContext(ctx, q, &got, `SELECT * FROM mdm_windows_configuration_profiles WHERE team_id = ?`, tmID)
|
||||
return sqlx.SelectContext(ctx, q, &got,
|
||||
`SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ?`,
|
||||
tmID)
|
||||
})
|
||||
|
||||
// create map of expected profiles keyed by name
|
||||
|
|
@ -2025,9 +2028,13 @@ func expectWindowsProfiles(
|
|||
func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
applyAndExpect := func(newSet []*fleet.MDMWindowsConfigProfile, tmID *uint, want []*fleet.MDMWindowsConfigProfile) map[string]string {
|
||||
applyAndExpect := func(newSet []*fleet.MDMWindowsConfigProfile, tmID *uint, want []*fleet.MDMWindowsConfigProfile,
|
||||
wantUpdated bool) map[string]string {
|
||||
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
return ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet)
|
||||
updatedDB, err := ds.batchSetMDMWindowsProfilesDB(ctx, tx, tmID, newSet)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, wantUpdated, updatedDB)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return expectWindowsProfiles(t, ds, tmID, want)
|
||||
|
|
@ -2041,7 +2048,7 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &prof,
|
||||
`SELECT * FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`,
|
||||
`SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ? AND name = ?`,
|
||||
teamID, name)
|
||||
})
|
||||
return &prof
|
||||
|
|
@ -2057,14 +2064,14 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
// apply empty set for no-team
|
||||
applyAndExpect(nil, nil, nil)
|
||||
applyAndExpect(nil, nil, nil, false)
|
||||
|
||||
// apply single profile set for tm1
|
||||
mTm1 := applyAndExpect([]*fleet.MDMWindowsConfigProfile{
|
||||
windowsConfigProfileForTest(t, "N1", "l1"),
|
||||
}, ptr.Uint(1), []*fleet.MDMWindowsConfigProfile{
|
||||
withTeamID(windowsConfigProfileForTest(t, "N1", "l1"), 1),
|
||||
})
|
||||
}, true)
|
||||
profTm1N1 := getProfileByTeamAndName(ptr.Uint(1), "N1")
|
||||
|
||||
// apply single profile set for no-team
|
||||
|
|
@ -2072,7 +2079,7 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||
windowsConfigProfileForTest(t, "N1", "l1"),
|
||||
}, nil, []*fleet.MDMWindowsConfigProfile{
|
||||
windowsConfigProfileForTest(t, "N1", "l1"),
|
||||
})
|
||||
}, true)
|
||||
|
||||
// wait a second to ensure timestamps in the DB change
|
||||
time.Sleep(time.Second)
|
||||
|
|
@ -2084,7 +2091,7 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||
}, ptr.Uint(1), []*fleet.MDMWindowsConfigProfile{
|
||||
withUploadedAt(withTeamID(windowsConfigProfileForTest(t, "N1", "l1"), 1), profTm1N1.UploadedAt),
|
||||
withTeamID(windowsConfigProfileForTest(t, "N2", "l2"), 1),
|
||||
})
|
||||
}, true)
|
||||
// uuid for N1-I1 is unchanged
|
||||
require.Equal(t, mTm1["I1"], mTm1b["I1"])
|
||||
profTm1N2 := getProfileByTeamAndName(ptr.Uint(1), "N2")
|
||||
|
|
@ -2102,7 +2109,7 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||
withTeamID(windowsConfigProfileForTest(t, "N1", "l1b"), 1),
|
||||
withUploadedAt(withTeamID(windowsConfigProfileForTest(t, "N2", "l2"), 1), profTm1N2.UploadedAt),
|
||||
withTeamID(windowsConfigProfileForTest(t, "N3", "l3"), 1),
|
||||
})
|
||||
}, true)
|
||||
// uuid for N1-I1 is unchanged
|
||||
require.Equal(t, mTm1b["I1"], mTm1c["I1"])
|
||||
// uuid for N2-I2 is unchanged
|
||||
|
|
@ -2119,10 +2126,19 @@ func testBatchSetMDMWindowsProfiles(t *testing.T, ds *Datastore) {
|
|||
}, nil, []*fleet.MDMWindowsConfigProfile{
|
||||
windowsConfigProfileForTest(t, "N4", "l4"),
|
||||
windowsConfigProfileForTest(t, "N5", "l5"),
|
||||
})
|
||||
}, true)
|
||||
|
||||
// apply the same thing again -- nothing updated
|
||||
applyAndExpect([]*fleet.MDMWindowsConfigProfile{
|
||||
windowsConfigProfileForTest(t, "N4", "l4"),
|
||||
windowsConfigProfileForTest(t, "N5", "l5"),
|
||||
}, nil, []*fleet.MDMWindowsConfigProfile{
|
||||
windowsConfigProfileForTest(t, "N4", "l4"),
|
||||
windowsConfigProfileForTest(t, "N5", "l5"),
|
||||
}, false)
|
||||
|
||||
// clear profiles for tm1
|
||||
applyAndExpect(nil, ptr.Uint(1), nil)
|
||||
applyAndExpect(nil, ptr.Uint(1), nil, true)
|
||||
}
|
||||
|
||||
// if the label name starts with "exclude-", the label is considered an "exclude-any", otherwise
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20240827132940, Down_20240827132940)
|
||||
}
|
||||
|
||||
func Up_20240827132940(tx *sql.Tx) error {
|
||||
// The AUTO_INCREMENT columns are used to determine if a row was updated by an INSERT ... ON DUPLICATE KEY UPDATE statement.
|
||||
// This is needed because we are currently using CLIENT_FOUND_ROWS option to determine if a row was found.
|
||||
// And in order to find if the row was updated, we need to check LAST_INSERT_ID().
|
||||
// MySQL docs: https://dev.mysql.com/doc/refman/8.4/en/insert-on-duplicate.html
|
||||
|
||||
if !columnExists(tx, "mdm_windows_configuration_profiles", "auto_increment") {
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE mdm_windows_configuration_profiles
|
||||
ADD COLUMN auto_increment BIGINT NOT NULL AUTO_INCREMENT UNIQUE
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add auto_increment to mdm_windows_configuration_profiles: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !columnExists(tx, "mdm_apple_declarations", "auto_increment") {
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE mdm_apple_declarations
|
||||
ADD COLUMN auto_increment BIGINT NOT NULL AUTO_INCREMENT UNIQUE
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add auto_increment to mdm_apple_declarations: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20240827132940(_ *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20240829170024, Down_20240829170024)
|
||||
}
|
||||
|
||||
func Up_20240829170024(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE policies
|
||||
ADD COLUMN software_installer_id INT UNSIGNED DEFAULT NULL,
|
||||
ADD FOREIGN KEY fk_policies_software_installer_id (software_installer_id) REFERENCES software_installers (id);
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add software_installer_id to policies: %w", err)
|
||||
}
|
||||
|
||||
// We store `user_name` and `user_email` in case the user is deleted from Fleet (`user_id` set to NULL).
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE software_installers
|
||||
ADD COLUMN user_id INT(10) UNSIGNED DEFAULT NULL,
|
||||
ADD COLUMN user_name VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
ADD COLUMN user_email VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
ADD CONSTRAINT fk_software_installers_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL;
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add user_id to software_installers: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20240829170024(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1242,21 +1242,7 @@ func (ds *Datastore) ProcessList(ctx context.Context) ([]fleet.MySQLProcess, err
|
|||
return processList, nil
|
||||
}
|
||||
|
||||
func insertOnDuplicateDidInsert(res sql.Result) bool {
|
||||
// Note that connection string sets CLIENT_FOUND_ROWS (see
|
||||
// generateMysqlConnectionString in this package), so LastInsertId is 0
|
||||
// and RowsAffected 1 when a row is set to its current values.
|
||||
//
|
||||
// See [the docs][1] or @mna's comment in `insertOnDuplicateDidUpdate`
|
||||
// below for more details
|
||||
//
|
||||
// [1]: https://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html
|
||||
lastID, _ := res.LastInsertId()
|
||||
affected, _ := res.RowsAffected()
|
||||
return lastID != 0 && affected == 1
|
||||
}
|
||||
|
||||
func insertOnDuplicateDidUpdate(res sql.Result) bool {
|
||||
func insertOnDuplicateDidInsertOrUpdate(res sql.Result) bool {
|
||||
// From mysql's documentation:
|
||||
//
|
||||
// With ON DUPLICATE KEY UPDATE, the affected-rows value per row is 1 if
|
||||
|
|
@ -1266,7 +1252,10 @@ func insertOnDuplicateDidUpdate(res sql.Result) bool {
|
|||
// connecting to mysqld, the affected-rows value is 1 (not 0) if an
|
||||
// existing row is set to its current values.
|
||||
//
|
||||
// https://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html
|
||||
// If a table contains an AUTO_INCREMENT column and INSERT ... ON DUPLICATE KEY UPDATE
|
||||
// inserts or updates a row, the LAST_INSERT_ID() function returns the AUTO_INCREMENT value.
|
||||
//
|
||||
// https://dev.mysql.com/doc/refman/8.4/en/insert-on-duplicate.html
|
||||
//
|
||||
// Note that connection string sets CLIENT_FOUND_ROWS (see
|
||||
// generateMysqlConnectionString in this package), so it does return 1 when
|
||||
|
|
@ -1281,7 +1270,8 @@ func insertOnDuplicateDidUpdate(res sql.Result) bool {
|
|||
|
||||
lastID, _ := res.LastInsertId()
|
||||
aff, _ := res.RowsAffected()
|
||||
return lastID == 0 || aff != 1
|
||||
// something was updated (lastID != 0) AND row was found (aff == 1 or higher if more rows were found)
|
||||
return lastID != 0 && aff > 0
|
||||
}
|
||||
|
||||
type parameterizedStmt struct {
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ func (ds *Datastore) InsertOSVulnerability(ctx context.Context, v fleet.OSVulner
|
|||
return false, ctxerr.Wrap(ctx, err, "insert operating system vulnerability")
|
||||
}
|
||||
|
||||
return insertOnDuplicateDidInsert(res), nil
|
||||
return insertOnDuplicateDidInsertOrUpdate(res), nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) DeleteOSVulnerabilities(ctx context.Context, vulnerabilities []fleet.OSVulnerability) error {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -233,10 +234,15 @@ func testInsertOSVulnerability(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, didInsert)
|
||||
|
||||
// Inserting the same vulnerability should not insert
|
||||
didInsert, err = ds.InsertOSVulnerability(ctx, vulnsUpdate, fleet.MSRCSource)
|
||||
// Inserting the same vulnerability should not insert, but update
|
||||
didInsertOrUpdate, err := ds.InsertOSVulnerability(ctx, vulnsUpdate, fleet.MSRCSource)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, false, didInsert)
|
||||
assert.True(t, didInsertOrUpdate)
|
||||
|
||||
// Inserting the exact same vulnerability again should not insert and not update
|
||||
didInsertOrUpdate, err = ds.InsertOSVulnerability(ctx, vulnsUpdate, fleet.MSRCSource)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, didInsertOrUpdate)
|
||||
|
||||
expected := vulnsUpdate
|
||||
expected.Source = fleet.MSRCSource
|
||||
|
|
|
|||
|
|
@ -22,12 +22,18 @@ import (
|
|||
|
||||
const policyCols = `
|
||||
p.id, p.team_id, p.resolution, p.name, p.query, p.description,
|
||||
p.author_id, p.platforms, p.created_at, p.updated_at, p.critical, p.calendar_events_enabled
|
||||
p.author_id, p.platforms, p.created_at, p.updated_at, p.critical,
|
||||
p.calendar_events_enabled, p.software_installer_id
|
||||
`
|
||||
|
||||
var errSoftwareTitleIDOnGlobalPolicy = errors.New("install software title id can be set on team policies only")
|
||||
|
||||
var policySearchColumns = []string{"p.name"}
|
||||
|
||||
func (ds *Datastore) NewGlobalPolicy(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
|
||||
if args.SoftwareInstallerID != nil {
|
||||
return nil, ctxerr.Wrap(ctx, errSoftwareTitleIDOnGlobalPolicy, "create policy")
|
||||
}
|
||||
if args.QueryID != nil {
|
||||
q, err := ds.Query(ctx, *args.QueryID)
|
||||
if err != nil {
|
||||
|
|
@ -129,15 +135,18 @@ func (ds *Datastore) PolicyLite(ctx context.Context, id uint) (*fleet.PolicyLite
|
|||
//
|
||||
// Currently, SavePolicy does not allow updating the team of an existing policy.
|
||||
func (ds *Datastore) SavePolicy(ctx context.Context, p *fleet.Policy, shouldRemoveAllPolicyMemberships bool, removePolicyStats bool) error {
|
||||
if p.TeamID == nil && p.SoftwareInstallerID != nil {
|
||||
return ctxerr.Wrap(ctx, errSoftwareTitleIDOnGlobalPolicy, "save policy")
|
||||
}
|
||||
// We must normalize the name for full Unicode support (Unicode equivalence).
|
||||
p.Name = norm.NFC.String(p.Name)
|
||||
sql := `
|
||||
UPDATE policies
|
||||
SET name = ?, query = ?, description = ?, resolution = ?, platforms = ?, critical = ?, calendar_events_enabled = ?, checksum = ` + policiesChecksumComputedColumn() + `
|
||||
SET name = ?, query = ?, description = ?, resolution = ?, platforms = ?, critical = ?, calendar_events_enabled = ?, software_installer_id = ?, checksum = ` + policiesChecksumComputedColumn() + `
|
||||
WHERE id = ?
|
||||
`
|
||||
result, err := ds.writer(ctx).ExecContext(
|
||||
ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.Platform, p.Critical, p.CalendarEventsEnabled, p.ID,
|
||||
ctx, sql, p.Name, p.Query, p.Description, p.Resolution, p.Platform, p.Critical, p.CalendarEventsEnabled, p.SoftwareInstallerID, p.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating policy")
|
||||
|
|
@ -484,7 +493,8 @@ func (ds *Datastore) PoliciesByID(ctx context.Context, ids []uint) (map[uint]*fl
|
|||
COALESCE(u.email, '') AS author_email,
|
||||
ps.updated_at as host_count_updated_at,
|
||||
COALESCE(ps.passing_host_count, 0) as passing_host_count,
|
||||
COALESCE(ps.failing_host_count, 0) as failing_host_count
|
||||
COALESCE(ps.failing_host_count, 0) as failing_host_count,
|
||||
p.software_installer_id
|
||||
FROM policies p
|
||||
LEFT JOIN users u ON p.author_id = u.id
|
||||
LEFT JOIN policy_stats ps ON p.id = ps.policy_id
|
||||
|
|
@ -601,11 +611,11 @@ func (ds *Datastore) NewTeamPolicy(ctx context.Context, teamID uint, authorID *u
|
|||
nameUnicode := norm.NFC.String(args.Name)
|
||||
res, err := ds.writer(ctx).ExecContext(ctx,
|
||||
fmt.Sprintf(
|
||||
`INSERT INTO policies (name, query, description, team_id, resolution, author_id, platforms, critical, calendar_events_enabled, checksum) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, %s)`,
|
||||
`INSERT INTO policies (name, query, description, team_id, resolution, author_id, platforms, critical, calendar_events_enabled, software_installer_id, checksum) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, %s)`,
|
||||
policiesChecksumComputedColumn(),
|
||||
),
|
||||
nameUnicode, args.Query, args.Description, teamID, args.Resolution, authorID, args.Platform, args.Critical,
|
||||
args.CalendarEventsEnabled,
|
||||
args.CalendarEventsEnabled, args.SoftwareInstallerID,
|
||||
)
|
||||
switch {
|
||||
case err == nil:
|
||||
|
|
@ -792,7 +802,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
|
|||
return ctxerr.Wrap(ctx, err, "exec ApplyPolicySpecs insert")
|
||||
}
|
||||
|
||||
if insertOnDuplicateDidUpdate(res) {
|
||||
if insertOnDuplicateDidInsertOrUpdate(res) {
|
||||
// when the upsert results in an UPDATE that *did* change some values,
|
||||
// it returns the updated ID as last inserted id.
|
||||
if lastID, _ := res.LastInsertId(); lastID > 0 {
|
||||
|
|
@ -1429,6 +1439,22 @@ func (ds *Datastore) GetCalendarPolicies(ctx context.Context, teamID uint) ([]fl
|
|||
return policies, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetPoliciesWithAssociatedInstaller(ctx context.Context, teamID uint, policyIDs []uint) ([]fleet.PolicySoftwareInstallerData, error) {
|
||||
if len(policyIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
query := `SELECT id, software_installer_id FROM policies WHERE team_id = ? AND software_installer_id IS NOT NULL AND id IN (?);`
|
||||
query, args, err := sqlx.In(query, teamID, policyIDs)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrapf(ctx, err, "build sqlx.In for get policies with associated installer")
|
||||
}
|
||||
var policies []fleet.PolicySoftwareInstallerData
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &policies, query, args...); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get policies with associated installer")
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetTeamHostsPolicyMemberships(
|
||||
ctx context.Context,
|
||||
domain string,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5" //nolint:gosec // (only used for tests)
|
||||
"encoding/hex"
|
||||
|
|
@ -63,6 +64,8 @@ func TestPolicies(t *testing.T) {
|
|||
{"TestPoliciesNameSort", testPoliciesNameSort},
|
||||
{"TestGetCalendarPolicies", testGetCalendarPolicies},
|
||||
{"GetTeamHostsPolicyMemberships", testGetTeamHostsPolicyMemberships},
|
||||
{"TestPoliciesNewGlobalPolicyWithInstaller", testNewGlobalPolicyWithInstaller},
|
||||
{"TestPoliciesTeamPoliciesWithInstaller", testTeamPoliciesWithInstaller},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
|
@ -1219,9 +1222,29 @@ func testPolicyQueriesForHost(t *testing.T, ds *Datastore) {
|
|||
func testPoliciesByID(t *testing.T, ds *Datastore) {
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
policy1 := newTestPolicy(t, ds, user1, "policy1", "darwin", nil)
|
||||
_ = newTestPolicy(t, ds, user1, "policy2", "darwin", nil)
|
||||
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
policy2 := newTestPolicy(t, ds, user1, "policy2", "darwin", &team1.ID)
|
||||
host1 := newTestHostWithPlatform(t, ds, "host1", "darwin", nil)
|
||||
|
||||
// Associate an installer to policy2
|
||||
installerID, err := ds.MatchOrCreateSoftwareInstaller(context.Background(), &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "hello",
|
||||
PreInstallQuery: "SELECT 1",
|
||||
PostInstallScript: "world",
|
||||
InstallerFile: bytes.NewReader([]byte("hello")),
|
||||
StorageID: "storage1",
|
||||
Filename: "file1",
|
||||
Title: "file1",
|
||||
Version: "1.0",
|
||||
Source: "apps",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
policy2.SoftwareInstallerID = ptr.Uint(installerID)
|
||||
err = ds.SavePolicy(context.Background(), policy2, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host1, map[uint]*bool{policy1.ID: ptr.Bool(true)}, time.Now(), false))
|
||||
require.NoError(t, ds.UpdateHostPolicyCounts(context.Background()))
|
||||
|
||||
|
|
@ -1230,9 +1253,12 @@ func testPoliciesByID(t *testing.T, ds *Datastore) {
|
|||
assert.Equal(t, len(policiesByID), 2)
|
||||
assert.Equal(t, policiesByID[1].ID, policy1.ID)
|
||||
assert.Equal(t, policiesByID[1].Name, policy1.Name)
|
||||
assert.Nil(t, policiesByID[1].SoftwareInstallerID)
|
||||
assert.Equal(t, uint(1), policiesByID[1].PassingHostCount)
|
||||
assert.Equal(t, policiesByID[2].ID, uint(2))
|
||||
assert.Equal(t, policiesByID[2].Name, "policy2")
|
||||
assert.Equal(t, uint(1), policiesByID[1].PassingHostCount)
|
||||
assert.NotNil(t, policiesByID[2].SoftwareInstallerID)
|
||||
assert.Equal(t, uint(1), *policiesByID[2].SoftwareInstallerID)
|
||||
|
||||
_, err = ds.PoliciesByID(context.Background(), []uint{1, 2, 3})
|
||||
require.Error(t, err)
|
||||
|
|
@ -3875,3 +3901,106 @@ func testGetTeamHostsPolicyMemberships(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, "serial2", hostsTeam2[0].HostHardwareSerial)
|
||||
require.Equal(t, "display_name2", hostsTeam2[0].HostDisplayName)
|
||||
}
|
||||
|
||||
func testNewGlobalPolicyWithInstaller(t *testing.T, ds *Datastore) {
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
_, err := ds.NewGlobalPolicy(context.Background(), &user1.ID, fleet.PolicyPayload{
|
||||
Query: "SELECT 1;",
|
||||
SoftwareInstallerID: ptr.Uint(1),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errSoftwareTitleIDOnGlobalPolicy)
|
||||
}
|
||||
|
||||
func testTeamPoliciesWithInstaller(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"}) // team2 has no policies
|
||||
require.NoError(t, err)
|
||||
|
||||
// Policy p1 has no associated installer.
|
||||
p1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
|
||||
Name: "p1",
|
||||
Query: "SELECT 1;",
|
||||
SoftwareInstallerID: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Create and associate an installer to p2.
|
||||
installerID, err := ds.MatchOrCreateSoftwareInstaller(context.Background(), &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "hello",
|
||||
PreInstallQuery: "SELECT 1",
|
||||
PostInstallScript: "world",
|
||||
InstallerFile: bytes.NewReader([]byte("hello")),
|
||||
StorageID: "storage1",
|
||||
Filename: "file1",
|
||||
Title: "file1",
|
||||
Version: "1.0",
|
||||
Source: "apps",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, p1.SoftwareInstallerID)
|
||||
p2, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
|
||||
Name: "p2",
|
||||
Query: "SELECT 1;",
|
||||
SoftwareInstallerID: ptr.Uint(installerID),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p2.SoftwareInstallerID)
|
||||
require.Equal(t, installerID, *p2.SoftwareInstallerID)
|
||||
// Create p3 as global policy.
|
||||
_, err = ds.NewGlobalPolicy(ctx, &user1.ID, fleet.PolicyPayload{
|
||||
Name: "p3",
|
||||
Query: "SELECT 1;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
p2, err = ds.Policy(ctx, p2.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p2.SoftwareInstallerID)
|
||||
require.Equal(t, installerID, *p2.SoftwareInstallerID)
|
||||
|
||||
policiesWithInstallers, err := ds.GetPoliciesWithAssociatedInstaller(ctx, team1.ID, []uint{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, policiesWithInstallers)
|
||||
|
||||
// p1 has no associated installers.
|
||||
policiesWithInstallers, err = ds.GetPoliciesWithAssociatedInstaller(ctx, team1.ID, []uint{p1.ID})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, policiesWithInstallers)
|
||||
|
||||
policiesWithInstallers, err = ds.GetPoliciesWithAssociatedInstaller(ctx, team1.ID, []uint{p2.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, policiesWithInstallers, 1)
|
||||
require.Equal(t, p2.ID, policiesWithInstallers[0].ID)
|
||||
require.Equal(t, installerID, policiesWithInstallers[0].InstallerID)
|
||||
|
||||
// p2 has associated installer but belongs to team1.
|
||||
policiesWithInstallers, err = ds.GetPoliciesWithAssociatedInstaller(ctx, team2.ID, []uint{p2.ID})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, policiesWithInstallers)
|
||||
|
||||
p1.SoftwareInstallerID = ptr.Uint(installerID)
|
||||
err = ds.SavePolicy(ctx, p1, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
p2, err = ds.Policy(ctx, p2.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p2.SoftwareInstallerID)
|
||||
require.Equal(t, installerID, *p2.SoftwareInstallerID)
|
||||
|
||||
policiesWithInstallers, err = ds.GetPoliciesWithAssociatedInstaller(ctx, team1.ID, []uint{p1.ID, p2.ID})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, policiesWithInstallers, 2)
|
||||
require.Equal(t, p1.ID, policiesWithInstallers[0].ID)
|
||||
require.Equal(t, installerID, policiesWithInstallers[0].InstallerID)
|
||||
require.Equal(t, p2.ID, policiesWithInstallers[1].ID)
|
||||
require.Equal(t, installerID, policiesWithInstallers[1].InstallerID)
|
||||
|
||||
policiesWithInstallers, err = ds.GetPoliciesWithAssociatedInstaller(ctx, team2.ID, []uint{p1.ID, p2.ID})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, policiesWithInstallers)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1138,6 +1138,8 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
s, err := ds.NewScript(ctx, s)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := test.NewUser(t, ds, "Bob", "bob@example.com", true)
|
||||
|
||||
// create a sync script execution
|
||||
res, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{ScriptContents: "echo something_else", SyncRequest: true})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1153,6 +1155,7 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
Title: "file1",
|
||||
Version: "1.0",
|
||||
Source: "apps",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -1207,6 +1210,7 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
Title: "file1",
|
||||
Version: "1.0",
|
||||
Source: "apps",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
|||
|
|
@ -2020,7 +2020,7 @@ func (ds *Datastore) InsertSoftwareVulnerability(
|
|||
return false, ctxerr.Wrap(ctx, err, "insert software vulnerability")
|
||||
}
|
||||
|
||||
return insertOnDuplicateDidInsert(res), nil
|
||||
return insertOnDuplicateDidInsertOrUpdate(res), nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListSoftwareVulnerabilitiesByHostIDsSource(
|
||||
|
|
|
|||
|
|
@ -117,8 +117,11 @@ INSERT INTO software_installers (
|
|||
pre_install_query,
|
||||
post_install_script_content_id,
|
||||
platform,
|
||||
self_service
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
self_service,
|
||||
user_id,
|
||||
user_name,
|
||||
user_email
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?))`
|
||||
|
||||
args := []interface{}{
|
||||
tid,
|
||||
|
|
@ -132,6 +135,9 @@ INSERT INTO software_installers (
|
|||
postInstallScriptID,
|
||||
payload.Platform,
|
||||
payload.SelfService,
|
||||
payload.UserID,
|
||||
payload.UserID,
|
||||
payload.UserID,
|
||||
}
|
||||
|
||||
res, err := ds.writer(ctx).ExecContext(ctx, stmt, args...)
|
||||
|
|
@ -210,7 +216,8 @@ SELECT
|
|||
si.pre_install_query,
|
||||
si.post_install_script_content_id,
|
||||
si.uploaded_at,
|
||||
COALESCE(st.name, '') AS software_title
|
||||
COALESCE(st.name, '') AS software_title,
|
||||
si.platform
|
||||
FROM
|
||||
software_installers si
|
||||
LEFT OUTER JOIN software_titles st ON st.id = si.title_id
|
||||
|
|
@ -277,9 +284,21 @@ WHERE
|
|||
return &dest, nil
|
||||
}
|
||||
|
||||
var errDeleteInstallerWithAssociatedPolicy = errors.New("Couldn't delete. Policy automation uses this software. Please disable policy automation for this software and try again.")
|
||||
|
||||
func (ds *Datastore) DeleteSoftwareInstaller(ctx context.Context, id uint) error {
|
||||
res, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM software_installers WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
if isMySQLForeignKey(err) {
|
||||
// Check if the software installer is referenced by a policy automation.
|
||||
var count int
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(*) FROM policies WHERE software_installer_id = ?`, id); err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "getting reference from policies")
|
||||
}
|
||||
if count > 0 {
|
||||
return ctxerr.Wrap(ctx, errDeleteInstallerWithAssociatedPolicy, "delete software installer")
|
||||
}
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "delete software installer")
|
||||
}
|
||||
|
||||
|
|
@ -345,8 +364,11 @@ SELECT
|
|||
hsi.user_id AS user_id,
|
||||
hsi.post_install_script_exit_code,
|
||||
hsi.install_script_exit_code,
|
||||
hsi.self_service,
|
||||
hsi.host_deleted_at
|
||||
hsi.self_service,
|
||||
hsi.host_deleted_at,
|
||||
si.user_id AS software_installer_user_id,
|
||||
si.user_name AS software_installer_user_name,
|
||||
si.user_email AS software_installer_user_email
|
||||
FROM
|
||||
host_software_installs hsi
|
||||
JOIN software_installers si ON si.id = hsi.software_installer_id
|
||||
|
|
@ -485,6 +507,41 @@ WHERE
|
|||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetHostLastInstallData(ctx context.Context, hostID, installerID uint) (*fleet.HostLastInstallData, error) {
|
||||
stmt := fmt.Sprintf(`
|
||||
SELECT execution_id, %s AS status
|
||||
FROM host_software_installs hsi
|
||||
WHERE hsi.id = (
|
||||
SELECT
|
||||
MAX(id)
|
||||
FROM host_software_installs
|
||||
WHERE
|
||||
software_installer_id = :installer_id AND host_id = :host_id
|
||||
GROUP BY
|
||||
host_id, software_installer_id)
|
||||
`, softwareInstallerHostStatusNamedQuery("hsi", ""))
|
||||
|
||||
stmt, args, err := sqlx.Named(stmt, map[string]interface{}{
|
||||
"host_id": hostID,
|
||||
"installer_id": installerID,
|
||||
"software_status_installed": fleet.SoftwareInstallerInstalled,
|
||||
"software_status_failed": fleet.SoftwareInstallerFailed,
|
||||
"software_status_pending": fleet.SoftwareInstallerPending,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "build named query to get host last install data")
|
||||
}
|
||||
|
||||
var hostLastInstall fleet.HostLastInstallData
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &hostLastInstall, stmt, args...); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "get host last install data")
|
||||
}
|
||||
return &hostLastInstall, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) CleanupUnusedSoftwareInstallers(ctx context.Context, softwareInstallStore fleet.SoftwareInstallerStore, removeCreatedBefore time.Time) error {
|
||||
if softwareInstallStore == nil {
|
||||
// no-op in this case, possible if not running with a Premium license
|
||||
|
|
@ -547,10 +604,14 @@ INSERT INTO software_installers (
|
|||
post_install_script_content_id,
|
||||
platform,
|
||||
self_service,
|
||||
title_id
|
||||
title_id,
|
||||
user_id,
|
||||
user_name,
|
||||
user_email
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
(SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = '')
|
||||
(SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''),
|
||||
?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?)
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
install_script_content_id = VALUES(install_script_content_id),
|
||||
|
|
@ -560,7 +621,10 @@ ON DUPLICATE KEY UPDATE
|
|||
version = VALUES(version),
|
||||
pre_install_query = VALUES(pre_install_query),
|
||||
platform = VALUES(platform),
|
||||
self_service = VALUES(self_service)
|
||||
self_service = VALUES(self_service),
|
||||
user_id = VALUES(user_id),
|
||||
user_name = VALUES(user_name),
|
||||
user_email = VALUES(user_email)
|
||||
`
|
||||
|
||||
// use a team id of 0 if no-team
|
||||
|
|
@ -634,6 +698,9 @@ ON DUPLICATE KEY UPDATE
|
|||
installer.SelfService,
|
||||
installer.Title,
|
||||
installer.Source,
|
||||
installer.UserID,
|
||||
installer.UserID,
|
||||
installer.UserID,
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, insertNewOrEditedInstaller, args...); err != nil {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ func TestSoftwareInstallers(t *testing.T) {
|
|||
{"BatchSetSoftwareInstallers", testBatchSetSoftwareInstallers},
|
||||
{"GetSoftwareInstallerMetadataByTeamAndTitleID", testGetSoftwareInstallerMetadataByTeamAndTitleID},
|
||||
{"HasSelfServiceSoftwareInstallers", testHasSelfServiceSoftwareInstallers},
|
||||
{"DeleteSoftwareInstallersAssignedToPolicy", testDeleteSoftwareInstallersAssignedToPolicy},
|
||||
{"GetHostLastInstallData", testGetHostLastInstallData},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
|
@ -46,6 +48,7 @@ func testListPendingSoftwareInstalls(t *testing.T, ds *Datastore) {
|
|||
|
||||
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
|
||||
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now())
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
installerID1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "hello",
|
||||
|
|
@ -57,6 +60,7 @@ func testListPendingSoftwareInstalls(t *testing.T, ds *Datastore) {
|
|||
Title: "file1",
|
||||
Version: "1.0",
|
||||
Source: "apps",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -70,6 +74,7 @@ func testListPendingSoftwareInstalls(t *testing.T, ds *Datastore) {
|
|||
Title: "file2",
|
||||
Version: "2.0",
|
||||
Source: "apps",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -84,6 +89,7 @@ func testListPendingSoftwareInstalls(t *testing.T, ds *Datastore) {
|
|||
Version: "3.0",
|
||||
Source: "apps",
|
||||
SelfService: true,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -169,6 +175,8 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
|
|||
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
cases := map[string]*uint{
|
||||
"no team": nil,
|
||||
"team": &team.ID,
|
||||
|
|
@ -188,6 +196,7 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
|
|||
InstallScript: "echo",
|
||||
TeamID: teamID,
|
||||
Filename: "foo.pkg",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
installerMeta, err := ds.GetSoftwareInstallerMetadataByID(ctx, installerID)
|
||||
|
|
@ -249,6 +258,8 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
teamID := team.ID
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
expectedStatus fleet.SoftwareInstallerStatus
|
||||
|
|
@ -295,6 +306,7 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|||
InstallScript: "echo " + tc.name,
|
||||
TeamID: &teamID,
|
||||
Filename: swFilename,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
|
|
@ -342,6 +354,8 @@ func testCleanupUnusedSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
store, err := filesystem.NewSoftwareInstallerStore(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
assertExisting := func(want []string) {
|
||||
dirEnts, err := os.ReadDir(filepath.Join(dir, "software-installers"))
|
||||
require.NoError(t, err)
|
||||
|
|
@ -373,6 +387,7 @@ func testCleanupUnusedSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
Filename: "installer0",
|
||||
Title: "ins0",
|
||||
Source: "apps",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -403,6 +418,8 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
team, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name()})
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
// TODO(roberto): perform better assertions, we should have evertything
|
||||
// to check that the actual values of everything match.
|
||||
assertSoftware := func(wantTitles []fleet.SoftwareTitle) {
|
||||
|
|
@ -442,6 +459,7 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
Source: "apps",
|
||||
Version: "1",
|
||||
PreInstallQuery: "foo",
|
||||
UserID: user1.ID,
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
assertSoftware([]fleet.SoftwareTitle{
|
||||
|
|
@ -461,6 +479,7 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
Source: "apps",
|
||||
Version: "1",
|
||||
PreInstallQuery: "select 0 from foo;",
|
||||
UserID: user1.ID,
|
||||
},
|
||||
{
|
||||
InstallScript: "install",
|
||||
|
|
@ -472,6 +491,7 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
Source: "apps",
|
||||
Version: "2",
|
||||
PreInstallQuery: "select 1 from bar;",
|
||||
UserID: user1.ID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -492,6 +512,7 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
Source: "apps",
|
||||
Version: "2",
|
||||
PreInstallQuery: "select 1 from bar;",
|
||||
UserID: user1.ID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -509,6 +530,7 @@ func testGetSoftwareInstallerMetadataByTeamAndTitleID(t *testing.T, ds *Datastor
|
|||
ctx := context.Background()
|
||||
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"})
|
||||
require.NoError(t, err)
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
installerID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
Title: "foo",
|
||||
|
|
@ -518,10 +540,13 @@ func testGetSoftwareInstallerMetadataByTeamAndTitleID(t *testing.T, ds *Datastor
|
|||
PreInstallQuery: "SELECT 1",
|
||||
TeamID: &team.ID,
|
||||
Filename: "foo.pkg",
|
||||
Platform: "darwin",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
installerMeta, err := ds.GetSoftwareInstallerMetadataByID(ctx, installerID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "darwin", installerMeta.Platform)
|
||||
|
||||
metaByTeamAndTitle, err := ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, *installerMeta.TitleID, true)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -536,6 +561,7 @@ func testGetSoftwareInstallerMetadataByTeamAndTitleID(t *testing.T, ds *Datastor
|
|||
InstallScript: "echo install",
|
||||
TeamID: &team.ID,
|
||||
Filename: "foo.pkg",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
installerMeta, err = ds.GetSoftwareInstallerMetadataByID(ctx, installerID)
|
||||
|
|
@ -554,6 +580,7 @@ func testHasSelfServiceSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
|
||||
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"})
|
||||
require.NoError(t, err)
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
const platform = "linux"
|
||||
// No installers
|
||||
|
|
@ -573,6 +600,7 @@ func testHasSelfServiceSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
Filename: "foo.pkg",
|
||||
Platform: platform,
|
||||
SelfService: false,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hasSelfService, err = ds.HasSelfServiceSoftwareInstallers(ctx, platform, nil)
|
||||
|
|
@ -591,6 +619,7 @@ func testHasSelfServiceSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
Filename: "foo2.pkg",
|
||||
Platform: platform,
|
||||
SelfService: true,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hasSelfService, err = ds.HasSelfServiceSoftwareInstallers(ctx, platform, nil)
|
||||
|
|
@ -629,6 +658,7 @@ func testHasSelfServiceSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
Filename: "foo global.pkg",
|
||||
Platform: platform,
|
||||
SelfService: true,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
hasSelfService, err = ds.HasSelfServiceSoftwareInstallers(ctx, "ubuntu", nil)
|
||||
|
|
@ -649,3 +679,191 @@ func testHasSelfServiceSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
assert.True(t, hasSelfService)
|
||||
}
|
||||
|
||||
func testDeleteSoftwareInstallersAssignedToPolicy(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
dir := t.TempDir()
|
||||
store, err := filesystem.NewSoftwareInstallerStore(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
// put an installer and save it in the DB
|
||||
ins0 := "installer.pkg"
|
||||
ins0File := bytes.NewReader([]byte("installer0"))
|
||||
err = store.Put(ctx, ins0, ins0File)
|
||||
require.NoError(t, err)
|
||||
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
softwareInstallerID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install",
|
||||
InstallerFile: ins0File,
|
||||
StorageID: ins0,
|
||||
Filename: "installer.pkg",
|
||||
Title: "ins0",
|
||||
Source: "apps",
|
||||
Platform: "darwin",
|
||||
TeamID: &team1.ID,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
p1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{
|
||||
Name: "p1",
|
||||
Query: "SELECT 1;",
|
||||
SoftwareInstallerID: &softwareInstallerID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.DeleteSoftwareInstaller(ctx, softwareInstallerID)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, errDeleteInstallerWithAssociatedPolicy)
|
||||
|
||||
_, err = ds.DeleteTeamPolicies(ctx, team1.ID, []uint{p1.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ds.DeleteSoftwareInstaller(ctx, softwareInstallerID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func testGetHostLastInstallData(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now(), test.WithTeamID(team1.ID))
|
||||
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now(), test.WithTeamID(team1.ID))
|
||||
|
||||
dir := t.TempDir()
|
||||
store, err := filesystem.NewSoftwareInstallerStore(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// put an installer and save it in the DB
|
||||
ins0 := "installer.pkg"
|
||||
ins0File := bytes.NewReader([]byte("installer0"))
|
||||
err = store.Put(ctx, ins0, ins0File)
|
||||
require.NoError(t, err)
|
||||
|
||||
softwareInstallerID1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install",
|
||||
InstallerFile: ins0File,
|
||||
StorageID: ins0,
|
||||
Filename: "installer.pkg",
|
||||
Title: "ins1",
|
||||
Source: "apps",
|
||||
Platform: "darwin",
|
||||
TeamID: &team1.ID,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
softwareInstallerID2, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install2",
|
||||
InstallerFile: ins0File,
|
||||
StorageID: ins0,
|
||||
Filename: "installer2.pkg",
|
||||
Title: "ins2",
|
||||
Source: "apps",
|
||||
Platform: "darwin",
|
||||
TeamID: &team1.ID,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// No installations on host1 yet.
|
||||
host1LastInstall, err := ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID1)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, host1LastInstall)
|
||||
|
||||
// Install installer.pkg on host1.
|
||||
installUUID1, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, softwareInstallerID1, false)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, installUUID1)
|
||||
|
||||
// Last installation should be pending.
|
||||
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID1)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID1, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
|
||||
// Set result of last installation.
|
||||
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
||||
HostID: host1.ID,
|
||||
InstallUUID: installUUID1,
|
||||
|
||||
InstallScriptExitCode: ptr.Int(0),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Last installation should be "installed".
|
||||
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID1)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID1, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerInstalled, *host1LastInstall.Status)
|
||||
|
||||
// Install installer2.pkg on host1.
|
||||
installUUID2, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, softwareInstallerID2, false)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, installUUID2)
|
||||
|
||||
// Last installation for installer1.pkg should be "installed".
|
||||
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID1)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID1, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerInstalled, *host1LastInstall.Status)
|
||||
// Last installation for installer2.pkg should be "pending".
|
||||
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID2)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID2, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
|
||||
// Perform another installation of installer1.pkg.
|
||||
installUUID3, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, softwareInstallerID1, false)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, installUUID3)
|
||||
|
||||
// Last installation for installer1.pkg should be "pending" again.
|
||||
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID1)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID3, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
|
||||
// Set result of last installer1.pkg installation.
|
||||
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
||||
HostID: host1.ID,
|
||||
InstallUUID: installUUID3,
|
||||
|
||||
InstallScriptExitCode: ptr.Int(1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Last installation for installer1.pkg should be "failed".
|
||||
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID1)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID3, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerFailed, *host1LastInstall.Status)
|
||||
|
||||
// No installations on host2.
|
||||
host2LastInstall, err := ds.GetHostLastInstallData(ctx, host2.ID, softwareInstallerID1)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, host2LastInstall)
|
||||
host2LastInstall, err = ds.GetHostLastInstallData(ctx, host2.ID, softwareInstallerID2)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, host2LastInstall)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1958,11 +1958,14 @@ func testInsertSoftwareVulnerability(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, inserted)
|
||||
|
||||
inserted, err = ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
|
||||
// Sleep so that the updated_at timestamp is guaranteed to be updated.
|
||||
time.Sleep(1 * time.Second)
|
||||
insertedOrUpdated, err := ds.InsertSoftwareVulnerability(ctx, fleet.SoftwareVulnerability{
|
||||
SoftwareID: host.Software[0].ID, CVE: "cve-1",
|
||||
}, fleet.UbuntuOVALSource)
|
||||
require.NoError(t, err)
|
||||
require.False(t, inserted)
|
||||
// This will always return true because we always update the timestamp
|
||||
assert.True(t, insertedOrUpdated)
|
||||
|
||||
storedVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -2001,9 +2004,12 @@ func testInsertSoftwareVulnerability(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
require.True(t, inserted)
|
||||
|
||||
inserted, err = ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.UbuntuOVALSource)
|
||||
// Sleep so that the updated_at timestamp is guaranteed to be updated.
|
||||
time.Sleep(1 * time.Second)
|
||||
insertedOrUpdated, err := ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.UbuntuOVALSource)
|
||||
require.NoError(t, err)
|
||||
require.False(t, inserted)
|
||||
// This will always return true because we always update the timestamp
|
||||
assert.True(t, insertedOrUpdated)
|
||||
|
||||
storedVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -2567,9 +2573,9 @@ func testDeleteOutOfDateVulnerabilities(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// This should update the 'updated_at' timestamp.
|
||||
inserted, err = ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.NVDSource)
|
||||
insertedOrUpdated, err := ds.InsertSoftwareVulnerability(ctx, vulns[0], fleet.NVDSource)
|
||||
require.NoError(t, err)
|
||||
require.False(t, inserted)
|
||||
assert.True(t, insertedOrUpdated)
|
||||
|
||||
err = ds.DeleteOutOfDateVulnerabilities(ctx, fleet.NVDSource, 2*time.Hour)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -3715,26 +3721,36 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
|
||||
// add VPP apps, one for both no team and team, and two for no-team only.
|
||||
va1, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1"}, &tm.ID)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1",
|
||||
}, &tm.ID)
|
||||
require.NoError(t, err)
|
||||
vpp1 := va1.AdamID
|
||||
va2, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.MacOSPlatform}}, Name: "vpp2",
|
||||
BundleIdentifier: "com.app.vpp2"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.MacOSPlatform}}, Name: "vpp2",
|
||||
BundleIdentifier: "com.app.vpp2",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
// create vpp3 app that allows self-service
|
||||
va3, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_3", Platform: fleet.MacOSPlatform}, SelfService: true}, Name: "vpp3",
|
||||
BundleIdentifier: "com.app.vpp3"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_3", Platform: fleet.MacOSPlatform}, SelfService: true}, Name: "vpp3",
|
||||
BundleIdentifier: "com.app.vpp3",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
vpp2, vpp3 := va2.AdamID, va3.AdamID
|
||||
|
||||
|
|
@ -3927,8 +3943,10 @@ func testListIOSHostSoftware(t *testing.T, ds *Datastore) {
|
|||
ctx := context.Background()
|
||||
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("ios"))
|
||||
nanoEnroll(t, ds, host, false)
|
||||
opts := fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name",
|
||||
TestSecondaryOrderKey: "source"}}
|
||||
opts := fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{
|
||||
PerPage: 10, IncludeMetadata: true, OrderKey: "name",
|
||||
TestSecondaryOrderKey: "source",
|
||||
}}
|
||||
|
||||
user, err := ds.NewUser(ctx, &fleet.User{
|
||||
Password: []byte("p4ssw0rd.123"),
|
||||
|
|
@ -4012,24 +4030,31 @@ func testListIOSHostSoftware(t *testing.T, ds *Datastore) {
|
|||
require.NoError(t, err)
|
||||
|
||||
expected := map[string]fleet.HostSoftwareWithInstaller{
|
||||
byNSV[a1].Name + byNSV[a1].Source: {Name: byNSV[a1].Name, Source: byNSV[a1].Source,
|
||||
byNSV[a1].Name + byNSV[a1].Source: {
|
||||
Name: byNSV[a1].Name, Source: byNSV[a1].Source,
|
||||
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
||||
{Version: byNSV[a1].Version, Vulnerabilities: []string{vulns[0].CVE, vulns[1].CVE, vulns[2].CVE}},
|
||||
}},
|
||||
byNSV[b].Name + byNSV[b].Source: {Name: byNSV[b].Name, Source: byNSV[b].Source,
|
||||
},
|
||||
},
|
||||
byNSV[b].Name + byNSV[b].Source: {
|
||||
Name: byNSV[b].Name, Source: byNSV[b].Source,
|
||||
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
||||
{Version: byNSV[b].Version, Vulnerabilities: []string{vulns[3].CVE}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
// c1 and c2 are the same software title because they have the same name and source
|
||||
byNSV[c1].Name + byNSV[c1].Source: {Name: byNSV[c1].Name, Source: byNSV[c1].Source,
|
||||
byNSV[c1].Name + byNSV[c1].Source: {
|
||||
Name: byNSV[c1].Name, Source: byNSV[c1].Source,
|
||||
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
||||
{Version: byNSV[c1].Version},
|
||||
{Version: byNSV[c2].Version},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
compareResults := func(expected map[string]fleet.HostSoftwareWithInstaller, got []*fleet.HostSoftwareWithInstaller, expectAsc bool,
|
||||
expectOmitted ...string) {
|
||||
expectOmitted ...string,
|
||||
) {
|
||||
require.Len(t, got, len(expected)-len(expectOmitted))
|
||||
prev := ""
|
||||
for _, g := range got {
|
||||
|
|
@ -4116,33 +4141,47 @@ func testListIOSHostSoftware(t *testing.T, ds *Datastore) {
|
|||
|
||||
// add VPP apps, one for both no team and team, and three for no-team only.
|
||||
va1, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IPadOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IPadOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IPadOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1"}, &tm.ID)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.IPadOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1",
|
||||
}, &tm.ID)
|
||||
require.NoError(t, err)
|
||||
vpp1 := va1.AdamID
|
||||
va2, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.IOSPlatform}}, Name: "vpp2",
|
||||
BundleIdentifier: "com.app.vpp2"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_2", Platform: fleet.IOSPlatform}}, Name: "vpp2",
|
||||
BundleIdentifier: "com.app.vpp2",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
va3, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_3", Platform: fleet.IOSPlatform}}, Name: "vpp3",
|
||||
BundleIdentifier: "com.app.vpp3"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_3", Platform: fleet.IOSPlatform}}, Name: "vpp3",
|
||||
BundleIdentifier: "com.app.vpp3",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
va4, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_4", Platform: fleet.IOSPlatform}}, Name: "vpp4",
|
||||
BundleIdentifier: "com.app.vpp4"}, nil)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_4", Platform: fleet.IOSPlatform}}, Name: "vpp4",
|
||||
BundleIdentifier: "com.app.vpp4",
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
vpp2, vpp3, vpp4 := va2.AdamID, va3.AdamID, va4.AdamID
|
||||
|
||||
|
|
@ -4384,6 +4423,7 @@ func testListHostSoftwareInstallThenTransferTeam(t *testing.T, ds *Datastore) {
|
|||
Version: "1.0",
|
||||
Source: "apps",
|
||||
TeamID: &team1.ID,
|
||||
UserID: user.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -4399,8 +4439,10 @@ func testListHostSoftwareInstallThenTransferTeam(t *testing.T, ds *Datastore) {
|
|||
|
||||
// add a VPP app for team 1
|
||||
vppTm1, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1"}, &team1.ID)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1",
|
||||
}, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fail to install it on the host
|
||||
|
|
@ -4489,6 +4531,7 @@ func testListHostSoftwareInstallThenDeleteInstallers(t *testing.T, ds *Datastore
|
|||
Version: "1.0",
|
||||
Source: "apps",
|
||||
TeamID: &team1.ID,
|
||||
UserID: user.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -4504,8 +4547,10 @@ func testListHostSoftwareInstallThenDeleteInstallers(t *testing.T, ds *Datastore
|
|||
|
||||
// add a VPP app for team 1
|
||||
vppTm1, err := ds.InsertVPPAppWithTeam(ctx,
|
||||
&fleet.VPPApp{VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1", LatestVersion: "1.0"}, &team1.ID)
|
||||
&fleet.VPPApp{
|
||||
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: fleet.MacOSPlatform}}, Name: "vpp1",
|
||||
BundleIdentifier: "com.app.vpp1", LatestVersion: "1.0",
|
||||
}, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// install it on the host
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ SELECT
|
|||
vap.icon_url as vpp_app_icon_url
|
||||
FROM software_titles st
|
||||
LEFT JOIN software_installers si ON si.title_id = st.id AND %s
|
||||
LEFT JOIN vpp_apps vap ON vap.title_id = st.id
|
||||
LEFT JOIN vpp_apps vap ON vap.title_id = st.id AND %s
|
||||
LEFT JOIN vpp_apps_teams vat ON vat.adam_id = vap.adam_id AND vat.platform = vap.platform AND %s
|
||||
LEFT JOIN software_titles_host_counts sthc ON sthc.software_title_id = st.id AND (%s)
|
||||
-- placeholder for JOIN on software/software_cve
|
||||
|
|
@ -286,6 +286,7 @@ GROUP BY st.id, package_self_service, package_name, package_version, vpp_app_sel
|
|||
|
||||
countsJoin := "TRUE"
|
||||
softwareInstallersJoinCond := "TRUE"
|
||||
vppAppsJoinCond := "TRUE"
|
||||
vppAppsTeamsJoinCond := "TRUE"
|
||||
includeVPPAppsAndSoftwareInstallers := "TRUE"
|
||||
switch {
|
||||
|
|
@ -304,6 +305,11 @@ GROUP BY st.id, package_self_service, package_name, package_version, vpp_app_sel
|
|||
vppAppsTeamsJoinCond = fmt.Sprintf("vat.global_or_team_id = %d", *opt.TeamID)
|
||||
}
|
||||
|
||||
if opt.PackagesOnly {
|
||||
vppAppsJoinCond = "FALSE"
|
||||
vppAppsTeamsJoinCond = "FALSE"
|
||||
}
|
||||
|
||||
additionalWhere := "TRUE"
|
||||
match := opt.ListOptions.MatchQuery
|
||||
softwareJoin := ""
|
||||
|
|
@ -363,7 +369,7 @@ GROUP BY st.id, package_self_service, package_name, package_version, vpp_app_sel
|
|||
defaultFilter += ` AND ( si.self_service = 1 OR vat.self_service = 1 ) `
|
||||
}
|
||||
|
||||
stmt = fmt.Sprintf(stmt, softwareInstallersJoinCond, vppAppsTeamsJoinCond, countsJoin, softwareJoin, additionalWhere, defaultFilter)
|
||||
stmt = fmt.Sprintf(stmt, softwareInstallersJoinCond, vppAppsJoinCond, vppAppsTeamsJoinCond, countsJoin, softwareJoin, additionalWhere, defaultFilter)
|
||||
return stmt, args
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -271,6 +271,8 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
|
|||
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
||||
host3 := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now())
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
software1 := []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions", Browser: "chrome"},
|
||||
{Name: "foo", Version: "0.0.3", Source: "chrome_extensions", Browser: "chrome"},
|
||||
|
|
@ -303,6 +305,7 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
|
|||
Source: "apps",
|
||||
InstallScript: "echo",
|
||||
Filename: "installer1.pkg",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer1)
|
||||
|
|
@ -317,6 +320,7 @@ func testOrderSoftwareTitles(t *testing.T, ds *Datastore) {
|
|||
Source: "apps",
|
||||
InstallScript: "echo",
|
||||
Filename: "installer2.pkg",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertSoftwareInstallRequest(ctx, host1.ID, installer2, false)
|
||||
|
|
@ -594,6 +598,8 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) {
|
|||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now())
|
||||
require.NoError(t, ds.AddHostsToTeam(ctx, &team1.ID, []uint{host1.ID}))
|
||||
host2 := test.NewHost(t, ds, "host2", "", "host2key", "host2uuid", time.Now())
|
||||
|
|
@ -627,6 +633,7 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) {
|
|||
Filename: "installer1.pkg",
|
||||
BundleIdentifier: "foo.bar",
|
||||
TeamID: &team1.ID,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer1)
|
||||
|
|
@ -642,6 +649,7 @@ func testTeamFilterSoftwareTitles(t *testing.T, ds *Datastore) {
|
|||
InstallScript: "echo",
|
||||
Filename: "installer2.pkg",
|
||||
TeamID: &team2.ID,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer2)
|
||||
|
|
@ -856,12 +864,15 @@ func sortTitlesByName(titles []fleet.SoftwareTitleListResult) {
|
|||
func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
// create a couple software installers not installed on any host
|
||||
installer1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
Title: "installer1",
|
||||
Source: "apps",
|
||||
InstallScript: "echo",
|
||||
Filename: "installer1.pkg",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer1)
|
||||
|
|
@ -870,6 +881,7 @@ func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) {
|
|||
Source: "apps",
|
||||
InstallScript: "echo",
|
||||
Filename: "installer2.pkg",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer2)
|
||||
|
|
@ -955,6 +967,7 @@ func testListSoftwareTitlesInstallersOnly(t *testing.T, ds *Datastore) {
|
|||
|
||||
func testListSoftwareTitlesAvailableForInstallFilter(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
// create 2 software installers
|
||||
installer1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
|
|
@ -962,6 +975,7 @@ func testListSoftwareTitlesAvailableForInstallFilter(t *testing.T, ds *Datastore
|
|||
Source: "apps",
|
||||
InstallScript: "echo",
|
||||
Filename: "installer1.pkg",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer1)
|
||||
|
|
@ -970,6 +984,7 @@ func testListSoftwareTitlesAvailableForInstallFilter(t *testing.T, ds *Datastore
|
|||
Source: "apps",
|
||||
InstallScript: "echo",
|
||||
Filename: "installer2.pkg",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer2)
|
||||
|
|
@ -1140,6 +1155,8 @@ func testListSoftwareTitlesAllTeams(t *testing.T, ds *Datastore) {
|
|||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
// Create a macOS software foobar installer on "No team".
|
||||
macOSInstallerNoTeam, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
Title: "foobar",
|
||||
|
|
@ -1148,6 +1165,7 @@ func testListSoftwareTitlesAllTeams(t *testing.T, ds *Datastore) {
|
|||
InstallScript: "echo",
|
||||
Filename: "foobar.pkg",
|
||||
TeamID: nil,
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -1322,6 +1340,7 @@ func testUploadedSoftwareExists(t *testing.T, ds *Datastore) {
|
|||
|
||||
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team Foo"})
|
||||
require.NoError(t, err)
|
||||
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
|
||||
|
||||
installer1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
Title: "installer1",
|
||||
|
|
@ -1329,6 +1348,7 @@ func testUploadedSoftwareExists(t *testing.T, ds *Datastore) {
|
|||
InstallScript: "echo",
|
||||
Filename: "installer1.pkg",
|
||||
BundleIdentifier: "com.foo.installer1",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer1)
|
||||
|
|
@ -1339,6 +1359,7 @@ func testUploadedSoftwareExists(t *testing.T, ds *Datastore) {
|
|||
Filename: "installer2.pkg",
|
||||
TeamID: &tm.ID,
|
||||
BundleIdentifier: "com.foo.installer2",
|
||||
UserID: user1.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, installer2)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,14 @@ func saveTeamSecretsDB(ctx context.Context, q sqlx.ExtContext, team *fleet.Team)
|
|||
|
||||
func (ds *Datastore) DeleteTeam(ctx context.Context, tid uint) error {
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
_, err := tx.ExecContext(ctx, `DELETE FROM teams WHERE id = ?`, tid)
|
||||
// Delete team policies first, because policies can have associated installers which may be deleted on cascade
|
||||
// before deleting the policies (which are also deleted on cascade).
|
||||
_, err := tx.ExecContext(ctx, `DELETE FROM policies WHERE team_id = ?`, tid)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "deleting policies for team %d", tid)
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `DELETE FROM teams WHERE id = ?`, tid)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "delete team %d", tid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package fleet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5" // nolint: gosec
|
||||
"encoding/hex"
|
||||
|
|
@ -204,6 +205,13 @@ type MDMAppleConfigProfile struct {
|
|||
UploadedAt time.Time `db:"uploaded_at" json:"updated_at"` // NOTE: JSON field is still `updated_at` for historical reasons, would be an API breaking change
|
||||
}
|
||||
|
||||
// MDMProfilesUpdates flags updates that were done during batch processing of profiles.
|
||||
type MDMProfilesUpdates struct {
|
||||
AppleConfigProfile bool
|
||||
WindowsConfigProfile bool
|
||||
AppleDeclaration bool
|
||||
}
|
||||
|
||||
// ConfigurationProfileLabel represents the many-to-many relationship between
|
||||
// profiles and labels.
|
||||
//
|
||||
|
|
@ -309,6 +317,20 @@ func (p *MDMAppleProfilePayload) FailedToInstallOnHost() bool {
|
|||
return p.Status != nil && *p.Status == MDMDeliveryFailed && p.OperationType == MDMOperationTypeInstall
|
||||
}
|
||||
|
||||
func (p MDMAppleProfilePayload) Equal(other MDMAppleProfilePayload) bool {
|
||||
statusEqual := p.Status == nil && other.Status == nil || p.Status != nil && other.Status != nil && *p.Status == *other.Status
|
||||
return p.ProfileUUID == other.ProfileUUID &&
|
||||
p.ProfileIdentifier == other.ProfileIdentifier &&
|
||||
p.ProfileName == other.ProfileName &&
|
||||
p.HostUUID == other.HostUUID &&
|
||||
p.HostPlatform == other.HostPlatform &&
|
||||
bytes.Equal(p.Checksum, other.Checksum) &&
|
||||
statusEqual &&
|
||||
p.OperationType == other.OperationType &&
|
||||
p.Detail == other.Detail &&
|
||||
p.CommandUUID == other.CommandUUID
|
||||
}
|
||||
|
||||
type MDMAppleBulkUpsertHostProfilePayload struct {
|
||||
ProfileUUID string
|
||||
ProfileIdentifier string
|
||||
|
|
@ -660,6 +682,18 @@ type MDMAppleHostDeclaration struct {
|
|||
Checksum string `db:"checksum" json:"-"`
|
||||
}
|
||||
|
||||
func (p MDMAppleHostDeclaration) Equal(other MDMAppleHostDeclaration) bool {
|
||||
statusEqual := p.Status == nil && other.Status == nil || p.Status != nil && other.Status != nil && *p.Status == *other.Status
|
||||
return statusEqual &&
|
||||
p.HostUUID == other.HostUUID &&
|
||||
p.DeclarationUUID == other.DeclarationUUID &&
|
||||
p.Name == other.Name &&
|
||||
p.Identifier == other.Identifier &&
|
||||
p.OperationType == other.OperationType &&
|
||||
p.Detail == other.Detail &&
|
||||
p.Checksum == other.Checksum
|
||||
}
|
||||
|
||||
func NewMDMAppleDeclaration(raw []byte, teamID *uint, name string, declType, ident string) *MDMAppleDeclaration {
|
||||
var decl MDMAppleDeclaration
|
||||
|
||||
|
|
|
|||
|
|
@ -7,18 +7,20 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/mdm"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mozilla.org/pkcs7"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
|
||||
)
|
||||
|
||||
func TestMDMAppleConfigProfile(t *testing.T) {
|
||||
|
|
@ -416,3 +418,199 @@ func TestMDMProfileIsWithinGracePeriod(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMDMAppleHostDeclarationEqual(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// This test is intended to ensure that the Equal method on MDMAppleHostDeclaration is updated when new fields are added.
|
||||
// The Equal method is used to identify whether database update is needed.
|
||||
|
||||
items := [...]MDMAppleHostDeclaration{{}, {}}
|
||||
|
||||
numberOfFields := 0
|
||||
for i := 0; i < len(items); i++ {
|
||||
rValue := reflect.ValueOf(&items[i]).Elem()
|
||||
numberOfFields = rValue.NumField()
|
||||
for j := 0; j < numberOfFields; j++ {
|
||||
field := rValue.Field(j)
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
valueToSet := fmt.Sprintf("test %d", i)
|
||||
field.SetString(valueToSet)
|
||||
case reflect.Int:
|
||||
field.SetInt(int64(i))
|
||||
case reflect.Bool:
|
||||
field.SetBool(i%2 == 0)
|
||||
case reflect.Pointer:
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
default:
|
||||
t.Fatalf("unhandled field type %s", field.Kind())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
status0 := MDMDeliveryStatus("status")
|
||||
status1 := MDMDeliveryStatus("status")
|
||||
items[0].Status = &status0
|
||||
assert.False(t, items[0].Equal(items[1]))
|
||||
|
||||
// Set known fields to be equal
|
||||
fieldsInEqualMethod := 0
|
||||
items[1].HostUUID = items[0].HostUUID
|
||||
fieldsInEqualMethod++
|
||||
items[1].DeclarationUUID = items[0].DeclarationUUID
|
||||
fieldsInEqualMethod++
|
||||
items[1].Name = items[0].Name
|
||||
fieldsInEqualMethod++
|
||||
items[1].Identifier = items[0].Identifier
|
||||
fieldsInEqualMethod++
|
||||
items[1].OperationType = items[0].OperationType
|
||||
fieldsInEqualMethod++
|
||||
items[1].Detail = items[0].Detail
|
||||
fieldsInEqualMethod++
|
||||
items[1].Checksum = items[0].Checksum
|
||||
fieldsInEqualMethod++
|
||||
items[1].Status = &status1
|
||||
fieldsInEqualMethod++
|
||||
assert.Equal(t, fieldsInEqualMethod, numberOfFields, "MDMAppleHostDeclaration.Equal needs to be updated for new/updated field(s)")
|
||||
assert.True(t, items[0].Equal(items[1]))
|
||||
|
||||
// Set pointers to nil
|
||||
items[0].Status = nil
|
||||
items[1].Status = nil
|
||||
assert.True(t, items[0].Equal(items[1]))
|
||||
|
||||
}
|
||||
|
||||
func TestMDMAppleProfilePayloadEqual(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// This test is intended to ensure that the Equal method on MDMAppleProfilePayload is updated when new fields are added.
|
||||
// The Equal method is used to identify whether database update is needed.
|
||||
|
||||
items := [...]MDMAppleProfilePayload{{}, {}}
|
||||
|
||||
numberOfFields := 0
|
||||
for i := 0; i < len(items); i++ {
|
||||
rValue := reflect.ValueOf(&items[i]).Elem()
|
||||
numberOfFields = rValue.NumField()
|
||||
for j := 0; j < numberOfFields; j++ {
|
||||
field := rValue.Field(j)
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
valueToSet := fmt.Sprintf("test %d", i)
|
||||
field.SetString(valueToSet)
|
||||
case reflect.Int:
|
||||
field.SetInt(int64(i))
|
||||
case reflect.Bool:
|
||||
field.SetBool(i%2 == 0)
|
||||
case reflect.Pointer:
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
case reflect.Slice:
|
||||
switch field.Type().Elem().Kind() {
|
||||
case reflect.Uint8:
|
||||
valueToSet := []byte("test")
|
||||
field.Set(reflect.ValueOf(valueToSet))
|
||||
default:
|
||||
t.Fatalf("unhandled slice type %s", field.Type().Elem().Kind())
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unhandled field type %s", field.Kind())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
status0 := MDMDeliveryStatus("status")
|
||||
status1 := MDMDeliveryStatus("status")
|
||||
items[0].Status = &status0
|
||||
checksum0 := []byte("checksum")
|
||||
checksum1 := []byte("checksum")
|
||||
items[0].Checksum = checksum0
|
||||
assert.False(t, items[0].Equal(items[1]))
|
||||
|
||||
// Set known fields to be equal
|
||||
fieldsInEqualMethod := 0
|
||||
items[1].ProfileUUID = items[0].ProfileUUID
|
||||
fieldsInEqualMethod++
|
||||
items[1].ProfileIdentifier = items[0].ProfileIdentifier
|
||||
fieldsInEqualMethod++
|
||||
items[1].ProfileName = items[0].ProfileName
|
||||
fieldsInEqualMethod++
|
||||
items[1].HostUUID = items[0].HostUUID
|
||||
fieldsInEqualMethod++
|
||||
items[1].HostPlatform = items[0].HostPlatform
|
||||
fieldsInEqualMethod++
|
||||
items[1].Checksum = checksum1
|
||||
fieldsInEqualMethod++
|
||||
items[1].Status = &status1
|
||||
fieldsInEqualMethod++
|
||||
items[1].OperationType = items[0].OperationType
|
||||
fieldsInEqualMethod++
|
||||
items[1].Detail = items[0].Detail
|
||||
fieldsInEqualMethod++
|
||||
items[1].CommandUUID = items[0].CommandUUID
|
||||
fieldsInEqualMethod++
|
||||
assert.Equal(t, fieldsInEqualMethod, numberOfFields, "MDMAppleProfilePayload.Equal needs to be updated for new/updated field(s)")
|
||||
assert.True(t, items[0].Equal(items[1]))
|
||||
|
||||
// Set pointers and slices to nil
|
||||
items[0].Status = nil
|
||||
items[1].Status = nil
|
||||
items[0].Checksum = nil
|
||||
items[1].Checksum = nil
|
||||
assert.True(t, items[0].Equal(items[1]))
|
||||
|
||||
}
|
||||
|
||||
func TestConfigurationProfileLabelEqual(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// This test is intended to ensure that the cmp.Equal method on ConfigurationProfileLabel is updated when new fields are added.
|
||||
// The cmp.Equal method is used to identify whether database update is needed.
|
||||
|
||||
items := [...]ConfigurationProfileLabel{{}, {}}
|
||||
|
||||
numberOfFields := 0
|
||||
for i := 0; i < len(items); i++ {
|
||||
rValue := reflect.ValueOf(&items[i]).Elem()
|
||||
numberOfFields = rValue.NumField()
|
||||
for j := 0; j < numberOfFields; j++ {
|
||||
field := rValue.Field(j)
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
valueToSet := fmt.Sprintf("test %d", i)
|
||||
field.SetString(valueToSet)
|
||||
case reflect.Int:
|
||||
field.SetInt(int64(i))
|
||||
case reflect.Uint:
|
||||
field.SetUint(uint64(i))
|
||||
case reflect.Bool:
|
||||
field.SetBool(i%2 == 0)
|
||||
case reflect.Pointer:
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
default:
|
||||
t.Fatalf("unhandled field type %s", field.Kind())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.False(t, cmp.Equal(items[0], items[1]))
|
||||
|
||||
// Set known fields to be equal
|
||||
fieldsInEqualMethod := 0
|
||||
items[1].ProfileUUID = items[0].ProfileUUID
|
||||
fieldsInEqualMethod++
|
||||
items[1].LabelName = items[0].LabelName
|
||||
fieldsInEqualMethod++
|
||||
items[1].LabelID = items[0].LabelID
|
||||
fieldsInEqualMethod++
|
||||
items[1].Broken = items[0].Broken
|
||||
fieldsInEqualMethod++
|
||||
items[1].Exclude = items[0].Exclude
|
||||
fieldsInEqualMethod++
|
||||
|
||||
assert.Equal(t, fieldsInEqualMethod, numberOfFields,
|
||||
"Does cmp.Equal for ConfigurationProfileLabel needs to be updated for new/updated field(s)?")
|
||||
assert.True(t, cmp.Equal(items[0], items[1]))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@ type Datastore interface {
|
|||
// InsertSoftwareInstallRequest tracks a new request to install the provided
|
||||
// software installer in the host. It returns the auto-generated installation
|
||||
// uuid.
|
||||
InsertSoftwareInstallRequest(ctx context.Context, hostID uint, softwareTitleID uint, selfService bool) (string, error)
|
||||
InsertSoftwareInstallRequest(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool) (string, error)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// SoftwareStore
|
||||
|
|
@ -679,6 +679,8 @@ type Datastore interface {
|
|||
// and have a calendar event scheduled.
|
||||
GetTeamHostsPolicyMemberships(ctx context.Context, domain string, teamID uint, policyIDs []uint,
|
||||
hostID *uint) ([]HostPolicyMembershipData, error)
|
||||
// GetPoliciesWithAssociatedInstaller returns team policies that have an associated installer.
|
||||
GetPoliciesWithAssociatedInstaller(ctx context.Context, teamID uint, policyIDs []uint) ([]PolicySoftwareInstallerData, error)
|
||||
GetCalendarPolicies(ctx context.Context, teamID uint) ([]PolicyCalendarData, error)
|
||||
|
||||
// Methods used for async processing of host policy query results.
|
||||
|
|
@ -1161,7 +1163,9 @@ type Datastore interface {
|
|||
// remove for each affected host to pending for the provided criteria, which
|
||||
// may be either a list of hostIDs, teamIDs, profileUUIDs or hostUUIDs (only
|
||||
// one of those ID types can be provided).
|
||||
BulkSetPendingMDMHostProfiles(ctx context.Context, hostIDs, teamIDs []uint, profileUUIDs, hostUUIDs []string) error
|
||||
BulkSetPendingMDMHostProfiles(ctx context.Context, hostIDs, teamIDs []uint,
|
||||
profileUUIDs, hostUUIDs []string) (updates MDMProfilesUpdates,
|
||||
err error)
|
||||
|
||||
// GetMDMAppleProfilesContents retrieves the XML contents of the
|
||||
// profiles requested.
|
||||
|
|
@ -1513,7 +1517,8 @@ type Datastore interface {
|
|||
|
||||
// BatchSetMDMProfiles sets the MDM Apple or Windows profiles for the given team or
|
||||
// no team in a single transaction.
|
||||
BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*MDMAppleConfigProfile, winProfiles []*MDMWindowsConfigProfile, macDeclarations []*MDMAppleDeclaration) error
|
||||
BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*MDMAppleConfigProfile, winProfiles []*MDMWindowsConfigProfile,
|
||||
macDeclarations []*MDMAppleDeclaration) (updates MDMProfilesUpdates, err error)
|
||||
|
||||
// NewMDMAppleDeclaration creates and returns a new MDM Apple declaration.
|
||||
NewMDMAppleDeclaration(ctx context.Context, declaration *MDMAppleDeclaration) (*MDMAppleDeclaration, error)
|
||||
|
|
@ -1621,6 +1626,9 @@ type Datastore interface {
|
|||
// installer execution IDs that have not yet been run for a given host
|
||||
ListPendingSoftwareInstalls(ctx context.Context, hostID uint) ([]string, error)
|
||||
|
||||
// GetHostLastInstallData returns the data for the last installation of a package on a host.
|
||||
GetHostLastInstallData(ctx context.Context, hostID, installerID uint) (*HostLastInstallData, error)
|
||||
|
||||
// MatchOrCreateSoftwareInstaller matches or creates a new software installer.
|
||||
MatchOrCreateSoftwareInstaller(ctx context.Context, payload *UploadSoftwareInstallerPayload) (uint, error)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,44 @@ type PolicyPayload struct {
|
|||
//
|
||||
// Empty string targets all platforms.
|
||||
Platform string
|
||||
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy. Only applies to team policies.
|
||||
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy.
|
||||
//
|
||||
// Only applies to team policies.
|
||||
CalendarEventsEnabled bool
|
||||
// SoftwareInstallerID is the ID of the software installer that will be installed if the policy fails.
|
||||
//
|
||||
// Only applies to team policies.
|
||||
SoftwareInstallerID *uint
|
||||
}
|
||||
|
||||
// NewTeamPolicyPayload holds data for team policy creation.
|
||||
//
|
||||
// If QueryID is not nil, then Name, Query and Description are ignored
|
||||
// (such fields are fetched from the queries table).
|
||||
type NewTeamPolicyPayload struct {
|
||||
// QueryID allows creating a policy from an existing query.
|
||||
//
|
||||
// Using QueryID is the old way of creating policies.
|
||||
// Use Query, Name and Description instead.
|
||||
QueryID *uint
|
||||
// Name is the name of the policy (ignored if QueryID != nil).
|
||||
Name string
|
||||
// Query is the policy query (ignored if QueryID != nil).
|
||||
Query string
|
||||
// Critical marks the policy as high impact.
|
||||
Critical bool
|
||||
// Description is the policy description text (ignored if QueryID != nil).
|
||||
Description string
|
||||
// Resolution indicates the steps needed to solve a failing policy.
|
||||
Resolution string
|
||||
// Platform is a comma-separated string to indicate the target platforms.
|
||||
//
|
||||
// Empty string targets all platforms.
|
||||
Platform string
|
||||
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy.
|
||||
CalendarEventsEnabled bool
|
||||
// SoftwareTitleID is the ID of the software title that will be installed if the policy fails.
|
||||
SoftwareTitleID *uint
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -109,8 +145,15 @@ type ModifyPolicyPayload struct {
|
|||
Platform *string `json:"platform"`
|
||||
// Critical marks the policy as high impact.
|
||||
Critical *bool `json:"critical" premium:"true"`
|
||||
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy. Only applies to team policies.
|
||||
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy.
|
||||
//
|
||||
// Only applies to team policies.
|
||||
CalendarEventsEnabled *bool `json:"calendar_events_enabled" premium:"true"`
|
||||
// SoftwareTitleID is the ID of the software title that will be installed if the policy fails.
|
||||
// Value 0 will unset the current installer from the policy.
|
||||
//
|
||||
// Only applies to team policies.
|
||||
SoftwareTitleID *uint `json:"software_title_id" premium:"true"`
|
||||
}
|
||||
|
||||
// Verify verifies the policy payload is valid.
|
||||
|
|
@ -163,7 +206,8 @@ type PolicyData struct {
|
|||
// Empty string targets all platforms.
|
||||
Platform string `json:"platform" db:"platforms"`
|
||||
|
||||
CalendarEventsEnabled bool `json:"calendar_events_enabled" db:"calendar_events_enabled"`
|
||||
CalendarEventsEnabled bool `json:"calendar_events_enabled" db:"calendar_events_enabled"`
|
||||
SoftwareInstallerID *uint `json:"-" db:"software_installer_id"`
|
||||
|
||||
UpdateCreateTimestamps
|
||||
}
|
||||
|
|
@ -177,6 +221,14 @@ type Policy struct {
|
|||
// FailingHostCount is the number of hosts this policy fails on.
|
||||
FailingHostCount uint `json:"failing_host_count" db:"failing_host_count"`
|
||||
HostCountUpdatedAt *time.Time `json:"host_count_updated_at" db:"host_count_updated_at"`
|
||||
|
||||
// InstallSoftware is used to trigger installation of a software title
|
||||
// when this policy fails.
|
||||
//
|
||||
// Only applies to team policies.
|
||||
//
|
||||
// This field is populated from PolicyData.SoftwareInstallerID.
|
||||
InstallSoftware *PolicySoftwareTitle `json:"install_software,omitempty"`
|
||||
}
|
||||
|
||||
type PolicyCalendarData struct {
|
||||
|
|
@ -184,6 +236,11 @@ type PolicyCalendarData struct {
|
|||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
type PolicySoftwareInstallerData struct {
|
||||
ID uint `db:"id"`
|
||||
InstallerID uint `db:"software_installer_id"`
|
||||
}
|
||||
|
||||
// PolicyLite is a stripped down version of the policy.
|
||||
type PolicyLite struct {
|
||||
ID uint `db:"id"`
|
||||
|
|
@ -232,10 +289,21 @@ type PolicySpec struct {
|
|||
//
|
||||
// Empty string targets all platforms.
|
||||
Platform string `json:"platform,omitempty"`
|
||||
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy. Only applies to team policies.
|
||||
// CalendarEventsEnabled indicates whether calendar events are enabled for the policy.
|
||||
//
|
||||
// Only applies to team policies.
|
||||
CalendarEventsEnabled bool `json:"calendar_events_enabled"`
|
||||
}
|
||||
|
||||
// PolicySoftwareTitle contains software title data for policies.
|
||||
type PolicySoftwareTitle struct {
|
||||
// SoftwareTitleID is the ID of the title associated to the policy.
|
||||
SoftwareTitleID uint `json:"software_title_id"`
|
||||
// Name is the associated installer title name
|
||||
// (not the package name, but the installed software title).
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Verify verifies the policy data is valid.
|
||||
func (p PolicySpec) Verify() error {
|
||||
if err := verifyPolicyName(p.Name); err != nil {
|
||||
|
|
|
|||
|
|
@ -672,7 +672,7 @@ type Service interface {
|
|||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Team Policies
|
||||
|
||||
NewTeamPolicy(ctx context.Context, teamID uint, p PolicyPayload) (*Policy, error)
|
||||
NewTeamPolicy(ctx context.Context, teamID uint, p NewTeamPolicyPayload) (*Policy, error)
|
||||
ListTeamPolicies(ctx context.Context, teamID uint, opts ListOptions, iopts ListOptions, mergeInherited bool) (teamPolicies, inheritedPolicies []*Policy, err error)
|
||||
DeleteTeamPolicies(ctx context.Context, teamID uint, ids []uint) ([]uint, error)
|
||||
ModifyTeamPolicy(ctx context.Context, teamID uint, id uint, p ModifyPolicyPayload) (*Policy, error)
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ type SoftwareTitleListOptions struct {
|
|||
KnownExploit bool `query:"exploit,optional"`
|
||||
MinimumCVSS float64 `query:"min_cvss_score,optional"`
|
||||
MaximumCVSS float64 `query:"max_cvss_score,optional"`
|
||||
PackagesOnly bool `query:"packages_only,optional"`
|
||||
}
|
||||
|
||||
type HostSoftwareTitleListOptions struct {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ type SoftwareInstaller struct {
|
|||
Name string `json:"name" db:"filename"`
|
||||
// Version is the version of the software package.
|
||||
Version string `json:"version" db:"version"`
|
||||
// Platform can be "darwin" (for pkgs), "windows" (for exes/msis) or "linux" (for debs).
|
||||
Platform string `json:"platform" db:"platform"`
|
||||
// UploadedAt is the time the software package was uploaded.
|
||||
UploadedAt time.Time `json:"uploaded_at" db:"uploaded_at"`
|
||||
// InstallerID is the unique identifier for the software package metadata in Fleet.
|
||||
|
|
@ -140,6 +142,14 @@ func (s SoftwareInstallerStatus) IsValid() bool {
|
|||
}
|
||||
}
|
||||
|
||||
// HostLastInstallData contains data for the last installation of a package on a host.
|
||||
type HostLastInstallData struct {
|
||||
// ExecutionID is the installation ID of the package on the host.
|
||||
ExecutionID string `db:"execution_id"`
|
||||
// Status is the status of the installation on the host.
|
||||
Status *SoftwareInstallerStatus `db:"status"`
|
||||
}
|
||||
|
||||
// HostSoftwareInstaller represents a software installer package that has been installed on a host.
|
||||
type HostSoftwareInstallerResult struct {
|
||||
// ID is the unique numerical ID of the result assigned by the datastore.
|
||||
|
|
@ -183,6 +193,12 @@ type HostSoftwareInstallerResult struct {
|
|||
// HostDeletedAt indicates if the data is associated with a
|
||||
// deleted host
|
||||
HostDeletedAt *time.Time `json:"-" db:"host_deleted_at"`
|
||||
// SoftwareInstallerUserID is the ID of the user that uploaded the software installer.
|
||||
SoftwareInstallerUserID *uint `json:"-" db:"software_installer_user_id"`
|
||||
// SoftwareInstallerUserID is the name of the user that uploaded the software installer.
|
||||
SoftwareInstallerUserName string `json:"-" db:"software_installer_user_name"`
|
||||
// SoftwareInstallerUserEmail is the email of the user that uploaded the software installer.
|
||||
SoftwareInstallerUserEmail string `json:"-" db:"software_installer_user_email"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -262,6 +278,7 @@ type UploadSoftwareInstallerPayload struct {
|
|||
Platform string
|
||||
BundleIdentifier string
|
||||
SelfService bool
|
||||
UserID uint
|
||||
}
|
||||
|
||||
// DownloadSoftwareInstallerPayload is the payload for downloading a software installer.
|
||||
|
|
|
|||
|
|
@ -158,6 +158,18 @@ type MDMWindowsProfilePayload struct {
|
|||
Retries int `db:"retries"`
|
||||
}
|
||||
|
||||
func (p MDMWindowsProfilePayload) Equal(other MDMWindowsProfilePayload) bool {
|
||||
statusEqual := p.Status == nil && other.Status == nil || p.Status != nil && other.Status != nil && *p.Status == *other.Status
|
||||
return statusEqual &&
|
||||
p.ProfileUUID == other.ProfileUUID &&
|
||||
p.HostUUID == other.HostUUID &&
|
||||
p.ProfileName == other.ProfileName &&
|
||||
p.OperationType == other.OperationType &&
|
||||
p.Detail == other.Detail &&
|
||||
p.CommandUUID == other.CommandUUID &&
|
||||
p.Retries == other.Retries
|
||||
}
|
||||
|
||||
type MDMWindowsBulkUpsertHostProfilePayload struct {
|
||||
ProfileUUID string
|
||||
ProfileName string
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ type ListSoftwareTitlesFunc func(ctx context.Context, opt fleet.SoftwareTitleLis
|
|||
|
||||
type SoftwareTitleByIDFunc func(ctx context.Context, id uint, teamID *uint, tmFilter fleet.TeamFilter) (*fleet.SoftwareTitle, error)
|
||||
|
||||
type InsertSoftwareInstallRequestFunc func(ctx context.Context, hostID uint, softwareTitleID uint, selfService bool) (string, error)
|
||||
type InsertSoftwareInstallRequestFunc func(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool) (string, error)
|
||||
|
||||
type ListSoftwareForVulnDetectionFunc func(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error)
|
||||
|
||||
|
|
@ -494,6 +494,8 @@ type PolicyQueriesForHostFunc func(ctx context.Context, host *fleet.Host) (map[s
|
|||
|
||||
type GetTeamHostsPolicyMembershipsFunc func(ctx context.Context, domain string, teamID uint, policyIDs []uint, hostID *uint) ([]fleet.HostPolicyMembershipData, error)
|
||||
|
||||
type GetPoliciesWithAssociatedInstallerFunc func(ctx context.Context, teamID uint, policyIDs []uint) ([]fleet.PolicySoftwareInstallerData, error)
|
||||
|
||||
type GetCalendarPoliciesFunc func(ctx context.Context, teamID uint) ([]fleet.PolicyCalendarData, error)
|
||||
|
||||
type AsyncBatchInsertPolicyMembershipFunc func(ctx context.Context, batch []fleet.PolicyMembershipResult) error
|
||||
|
|
@ -778,7 +780,7 @@ type ListMDMAppleProfilesToRemoveFunc func(ctx context.Context) ([]*fleet.MDMApp
|
|||
|
||||
type BulkUpsertMDMAppleHostProfilesFunc func(ctx context.Context, payload []*fleet.MDMAppleBulkUpsertHostProfilePayload) error
|
||||
|
||||
type BulkSetPendingMDMHostProfilesFunc func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string) error
|
||||
type BulkSetPendingMDMHostProfilesFunc func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string) (updates fleet.MDMProfilesUpdates, err error)
|
||||
|
||||
type GetMDMAppleProfilesContentsFunc func(ctx context.Context, profileUUIDs []string) (map[string]mobileconfig.Mobileconfig, error)
|
||||
|
||||
|
|
@ -970,7 +972,7 @@ type NewMDMWindowsConfigProfileFunc func(ctx context.Context, cp fleet.MDMWindow
|
|||
|
||||
type SetOrUpdateMDMWindowsConfigProfileFunc func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error
|
||||
|
||||
type BatchSetMDMProfilesFunc func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) error
|
||||
type BatchSetMDMProfilesFunc func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) (updates fleet.MDMProfilesUpdates, err error)
|
||||
|
||||
type NewMDMAppleDeclarationFunc func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (*fleet.MDMAppleDeclaration, error)
|
||||
|
||||
|
|
@ -1024,6 +1026,8 @@ type GetSoftwareInstallDetailsFunc func(ctx context.Context, executionId string)
|
|||
|
||||
type ListPendingSoftwareInstallsFunc func(ctx context.Context, hostID uint) ([]string, error)
|
||||
|
||||
type GetHostLastInstallDataFunc func(ctx context.Context, hostID uint, installerID uint) (*fleet.HostLastInstallData, error)
|
||||
|
||||
type MatchOrCreateSoftwareInstallerFunc func(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, error)
|
||||
|
||||
type GetSoftwareInstallerMetadataByIDFunc func(ctx context.Context, id uint) (*fleet.SoftwareInstaller, error)
|
||||
|
|
@ -1776,6 +1780,9 @@ type DataStore struct {
|
|||
GetTeamHostsPolicyMembershipsFunc GetTeamHostsPolicyMembershipsFunc
|
||||
GetTeamHostsPolicyMembershipsFuncInvoked bool
|
||||
|
||||
GetPoliciesWithAssociatedInstallerFunc GetPoliciesWithAssociatedInstallerFunc
|
||||
GetPoliciesWithAssociatedInstallerFuncInvoked bool
|
||||
|
||||
GetCalendarPoliciesFunc GetCalendarPoliciesFunc
|
||||
GetCalendarPoliciesFuncInvoked bool
|
||||
|
||||
|
|
@ -2571,6 +2578,9 @@ type DataStore struct {
|
|||
ListPendingSoftwareInstallsFunc ListPendingSoftwareInstallsFunc
|
||||
ListPendingSoftwareInstallsFuncInvoked bool
|
||||
|
||||
GetHostLastInstallDataFunc GetHostLastInstallDataFunc
|
||||
GetHostLastInstallDataFuncInvoked bool
|
||||
|
||||
MatchOrCreateSoftwareInstallerFunc MatchOrCreateSoftwareInstallerFunc
|
||||
MatchOrCreateSoftwareInstallerFuncInvoked bool
|
||||
|
||||
|
|
@ -3950,11 +3960,11 @@ func (s *DataStore) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint
|
|||
return s.SoftwareTitleByIDFunc(ctx, id, teamID, tmFilter)
|
||||
}
|
||||
|
||||
func (s *DataStore) InsertSoftwareInstallRequest(ctx context.Context, hostID uint, softwareTitleID uint, selfService bool) (string, error) {
|
||||
func (s *DataStore) InsertSoftwareInstallRequest(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool) (string, error) {
|
||||
s.mu.Lock()
|
||||
s.InsertSoftwareInstallRequestFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.InsertSoftwareInstallRequestFunc(ctx, hostID, softwareTitleID, selfService)
|
||||
return s.InsertSoftwareInstallRequestFunc(ctx, hostID, softwareInstallerID, selfService)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListSoftwareForVulnDetection(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error) {
|
||||
|
|
@ -4293,6 +4303,13 @@ func (s *DataStore) GetTeamHostsPolicyMemberships(ctx context.Context, domain st
|
|||
return s.GetTeamHostsPolicyMembershipsFunc(ctx, domain, teamID, policyIDs, hostID)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetPoliciesWithAssociatedInstaller(ctx context.Context, teamID uint, policyIDs []uint) ([]fleet.PolicySoftwareInstallerData, error) {
|
||||
s.mu.Lock()
|
||||
s.GetPoliciesWithAssociatedInstallerFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetPoliciesWithAssociatedInstallerFunc(ctx, teamID, policyIDs)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetCalendarPolicies(ctx context.Context, teamID uint) ([]fleet.PolicyCalendarData, error) {
|
||||
s.mu.Lock()
|
||||
s.GetCalendarPoliciesFuncInvoked = true
|
||||
|
|
@ -5287,7 +5304,7 @@ func (s *DataStore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
|
|||
return s.BulkUpsertMDMAppleHostProfilesFunc(ctx, payload)
|
||||
}
|
||||
|
||||
func (s *DataStore) BulkSetPendingMDMHostProfiles(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string) error {
|
||||
func (s *DataStore) BulkSetPendingMDMHostProfiles(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
s.mu.Lock()
|
||||
s.BulkSetPendingMDMHostProfilesFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
|
|
@ -5959,7 +5976,7 @@ func (s *DataStore) SetOrUpdateMDMWindowsConfigProfile(ctx context.Context, cp f
|
|||
return s.SetOrUpdateMDMWindowsConfigProfileFunc(ctx, cp)
|
||||
}
|
||||
|
||||
func (s *DataStore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) error {
|
||||
func (s *DataStore) BatchSetMDMProfiles(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDeclarations []*fleet.MDMAppleDeclaration) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
s.mu.Lock()
|
||||
s.BatchSetMDMProfilesFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
|
|
@ -6148,6 +6165,13 @@ func (s *DataStore) ListPendingSoftwareInstalls(ctx context.Context, hostID uint
|
|||
return s.ListPendingSoftwareInstallsFunc(ctx, hostID)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetHostLastInstallData(ctx context.Context, hostID uint, installerID uint) (*fleet.HostLastInstallData, error) {
|
||||
s.mu.Lock()
|
||||
s.GetHostLastInstallDataFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetHostLastInstallDataFunc(ctx, hostID, installerID)
|
||||
}
|
||||
|
||||
func (s *DataStore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, error) {
|
||||
s.mu.Lock()
|
||||
s.MatchOrCreateSoftwareInstallerFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -85,7 +85,12 @@ func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityD
|
|||
var userName *string
|
||||
var userEmail *string
|
||||
if user != nil {
|
||||
userID = &user.ID
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
// the installation.
|
||||
if user.ID != 0 {
|
||||
userID = &user.ID
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
}
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ func (svc *Service) NewMDMAppleConfigProfile(ctx context.Context, teamID uint, r
|
|||
}
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newCP.ProfileUUID}, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newCP.ProfileUUID}, nil); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
|
||||
|
|
@ -470,7 +470,7 @@ func (svc *Service) NewMDMAppleDeclaration(ctx context.Context, teamID uint, r i
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{decl.DeclarationUUID}, nil); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "bulk set pending host declarations")
|
||||
}
|
||||
|
||||
|
|
@ -773,7 +773,7 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID
|
|||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
// cannot use the profile ID as it is now deleted
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
|
||||
|
|
@ -853,7 +853,7 @@ func (svc *Service) DeleteMDMAppleDeclaration(ctx context.Context, declUUID stri
|
|||
return ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
|
||||
|
|
@ -1978,7 +1978,7 @@ func (svc *Service) BatchSetMDMAppleProfiles(ctx context.Context, tmID *uint, tm
|
|||
}
|
||||
|
||||
if !skipBulkPending {
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{bulkTeamID}, nil, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{bulkTeamID}, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -599,8 +599,9 @@ func TestMDMAppleConfigProfileAuthz(t *testing.T) {
|
|||
ds.GetMDMAppleProfilesSummaryFunc = func(context.Context, *uint) (*fleet.MDMProfilesSummary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
mockGetFuncWithTeamID := func(teamID uint) mock.GetMDMAppleConfigProfileFunc {
|
||||
return func(ctx context.Context, puid string) (*fleet.MDMAppleConfigProfile, error) {
|
||||
|
|
@ -706,8 +707,9 @@ func TestNewMDMAppleConfigProfile(t *testing.T) {
|
|||
ds.NewActivityFunc = func(context.Context, *fleet.User, fleet.ActivityDetails, []byte, time.Time) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
cp, err := svc.NewMDMAppleConfigProfile(ctx, 0, r, nil, false)
|
||||
|
|
@ -1499,8 +1501,9 @@ func TestMDMBatchSetAppleProfiles(t *testing.T) {
|
|||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, tid *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
|
||||
return nil, nil, nil
|
||||
|
|
@ -1815,8 +1818,9 @@ func TestMDMBatchSetAppleProfilesBoolArgs(t *testing.T) {
|
|||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, profileUUIDs, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, profileUUIDs, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, tid *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
|
||||
return nil, nil, nil
|
||||
|
|
|
|||
|
|
@ -1285,9 +1285,13 @@ func (c *Client) DoGitOps(
|
|||
team["webhook_settings"] = map[string]interface{}{}
|
||||
clearHostStatusWebhook := true
|
||||
if webhookSettings, ok := config.TeamSettings["webhook_settings"]; ok {
|
||||
if hostStatusWebhook, ok := webhookSettings.(map[string]interface{})["host_status_webhook"]; ok {
|
||||
clearHostStatusWebhook = false
|
||||
team["webhook_settings"].(map[string]interface{})["host_status_webhook"] = hostStatusWebhook
|
||||
if _, ok := webhookSettings.(map[string]interface{}); ok {
|
||||
if hostStatusWebhook, ok := webhookSettings.(map[string]interface{})["host_status_webhook"]; ok {
|
||||
clearHostStatusWebhook = false
|
||||
team["webhook_settings"].(map[string]interface{})["host_status_webhook"] = hostStatusWebhook
|
||||
}
|
||||
} else if webhookSettings != nil {
|
||||
return nil, fmt.Errorf("team_settings.webhook_settings config is not a map but a %T", webhookSettings)
|
||||
}
|
||||
}
|
||||
if clearHostStatusWebhook {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||
|
|
@ -155,6 +155,9 @@ func (svc Service) GetPolicyByIDQueries(ctx context.Context, policyID uint) (*fl
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := svc.populatePolicyInstallSoftware(ctx, policy); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "populate install_software")
|
||||
}
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
|
@ -583,8 +586,10 @@ func autofillPoliciesEndpoint(ctx context.Context, request interface{}, svc flee
|
|||
}
|
||||
|
||||
// Exposing external URL and timeout for testing purposes
|
||||
var getHumanInterpretationFromOsquerySqlUrl = "https://fleetdm.com/api/v1/get-human-interpretation-from-osquery-sql"
|
||||
var getHumanInterpretationFromOsquerySqlTimeout = 30 * time.Second
|
||||
var (
|
||||
getHumanInterpretationFromOsquerySqlUrl = "https://fleetdm.com/api/v1/get-human-interpretation-from-osquery-sql"
|
||||
getHumanInterpretationFromOsquerySqlTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
type AutofillError struct {
|
||||
Message string
|
||||
|
|
|
|||
|
|
@ -826,7 +826,7 @@ func (svc *Service) AddHostsToTeam(ctx context.Context, teamID *uint, hostIDs []
|
|||
return err
|
||||
}
|
||||
if !skipBulkPending {
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, hostIDs, nil, nil, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, hostIDs, nil, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
}
|
||||
|
|
@ -962,7 +962,7 @@ func (svc *Service) AddHostsToTeamByFilter(ctx context.Context, teamID *uint, fi
|
|||
if err := svc.ds.AddHostsToTeam(ctx, teamID, hostIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, hostIDs, nil, nil, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, hostIDs, nil, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
serials, err := svc.ds.ListMDMAppleDEPSerialsInHostIDs(ctx, hostIDs)
|
||||
|
|
|
|||
|
|
@ -610,8 +610,9 @@ func TestHostAuth(t *testing.T) {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) {
|
||||
return nil, nil
|
||||
|
|
@ -889,8 +890,9 @@ func TestAddHostsToTeamByFilter(t *testing.T) {
|
|||
assert.Equal(t, expectedHostIDs, hostIDs)
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) {
|
||||
return nil, nil
|
||||
|
|
@ -931,8 +933,9 @@ func TestAddHostsToTeamByFilterLabel(t *testing.T) {
|
|||
assert.Equal(t, expectedHostIDs, hostIDs)
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.ListMDMAppleDEPSerialsInHostIDsFunc = func(ctx context.Context, hids []uint) ([]string, error) {
|
||||
return nil, nil
|
||||
|
|
@ -963,8 +966,9 @@ func TestAddHostsToTeamByFilterEmptyHosts(t *testing.T) {
|
|||
ds.AddHostsToTeamFunc = func(ctx context.Context, teamID *uint, hostIDs []uint) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
emptyFilter := &map[string]interface{}{}
|
||||
|
|
|
|||
|
|
@ -11595,6 +11595,9 @@ func (s *integrationTestSuite) TestListHostUpcomingActivities() {
|
|||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
adminUser, err := s.ds.UserByEmail(ctx, "admin1@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
// there is already a datastore-layer test that verifies that correct values
|
||||
// are returned for users, saved scripts, etc. so this is more focused on
|
||||
// verifying that the service layer passes the proper options and the
|
||||
|
|
@ -11639,6 +11642,7 @@ func (s *integrationTestSuite) TestListHostUpcomingActivities() {
|
|||
Title: "foo",
|
||||
Source: "apps",
|
||||
Version: "0.0.1",
|
||||
UserID: adminUser.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
s1Meta, err := s.ds.GetSoftwareInstallerMetadataByID(ctx, sw1)
|
||||
|
|
|
|||
|
|
@ -10009,8 +10009,10 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
|
|||
|
||||
// Add new software to host -- installed on host, but not by Fleet
|
||||
installedVersion := "1.0.1"
|
||||
softwareAlreadyInstalled := fleet.Software{Name: "DummyApp.app", Version: installedVersion, Source: "apps",
|
||||
BundleIdentifier: "com.example.dummy"}
|
||||
softwareAlreadyInstalled := fleet.Software{
|
||||
Name: "DummyApp.app", Version: installedVersion, Source: "apps",
|
||||
BundleIdentifier: "com.example.dummy",
|
||||
}
|
||||
software = append(software, softwareAlreadyInstalled)
|
||||
_, err = s.ds.UpdateHostSoftware(ctx, host.ID, software)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -10034,7 +10036,6 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
|
|||
assert.Equal(t, installedVersion, getHostSw.Software[0].InstalledVersions[0].Version)
|
||||
assert.NotNil(t, getHostSw.Software[0].SoftwarePackage)
|
||||
assert.Nil(t, getHostSw.Software[0].Status)
|
||||
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndDelete() {
|
||||
|
|
@ -11116,7 +11117,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
|
||||
host := createOrbitEnrolledHost(t, "linux", "", s.ds)
|
||||
|
||||
// create a software installer and some host install requests
|
||||
// Create software installers and corresponding host install requests.
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install script",
|
||||
PreInstallQuery: "pre install query",
|
||||
|
|
@ -11126,6 +11127,24 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
}
|
||||
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
|
||||
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
|
||||
payload2 := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install script 2",
|
||||
PreInstallQuery: "pre install query 2",
|
||||
PostInstallScript: "post install script 2",
|
||||
Filename: "vim.deb",
|
||||
Title: "vim",
|
||||
}
|
||||
s.uploadSoftwareInstaller(payload2, http.StatusOK, "")
|
||||
titleID2 := getSoftwareTitleID(t, s.ds, payload2.Title, "deb_packages")
|
||||
payload3 := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install script 3",
|
||||
PreInstallQuery: "pre install query 3",
|
||||
PostInstallScript: "post install script 3",
|
||||
Filename: "emacs.deb",
|
||||
Title: "emacs",
|
||||
}
|
||||
s.uploadSoftwareInstaller(payload3, http.StatusOK, "")
|
||||
titleID3 := getSoftwareTitleID(t, s.ds, payload3.Title, "deb_packages")
|
||||
|
||||
latestInstallUUID := func() string {
|
||||
var id string
|
||||
|
|
@ -11137,9 +11156,10 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
|
||||
// create some install requests for the host
|
||||
installUUIDs := make([]string, 3)
|
||||
titleIDs := []uint{titleID, titleID2, titleID3}
|
||||
for i := 0; i < len(installUUIDs); i++ {
|
||||
resp := installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/install/%d", host.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/install/%d", host.ID, titleIDs[i]), nil, http.StatusAccepted, &resp)
|
||||
installUUIDs[i] = latestInstallUUID()
|
||||
}
|
||||
|
||||
|
|
@ -11202,7 +11222,14 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
Status: fleet.SoftwareInstallerFailed,
|
||||
PreInstallQueryOutput: ptr.String(fleet.SoftwareInstallerQueryFailCopy),
|
||||
})
|
||||
wantAct.InstallUUID = installUUIDs[1]
|
||||
wantAct = fleet.ActivityTypeInstalledSoftware{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
SoftwareTitle: payload2.Title,
|
||||
SoftwarePackage: payload2.Filename,
|
||||
InstallUUID: installUUIDs[1],
|
||||
Status: string(fleet.SoftwareInstallerFailed),
|
||||
}
|
||||
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result",
|
||||
|
|
@ -11224,8 +11251,14 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
Output: ptr.String(fmt.Sprintf(fleet.SoftwareInstallerInstallSuccessCopy, "success")),
|
||||
PostInstallScriptOutput: ptr.String(fmt.Sprintf(fleet.SoftwareInstallerPostInstallSuccessCopy, "ok")),
|
||||
})
|
||||
wantAct.InstallUUID = installUUIDs[2]
|
||||
wantAct.Status = string(fleet.SoftwareInstallerInstalled)
|
||||
wantAct = fleet.ActivityTypeInstalledSoftware{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
SoftwareTitle: payload3.Title,
|
||||
SoftwarePackage: payload3.Filename,
|
||||
InstallUUID: installUUIDs[2],
|
||||
Status: string(fleet.SoftwareInstallerInstalled),
|
||||
}
|
||||
lastActID := s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
// non-existing installation uuid
|
||||
|
|
@ -11343,7 +11376,11 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
|
|||
require.EqualValues(t, 0, *scriptRes.ExitCode)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) uploadSoftwareInstaller(payload *fleet.UploadSoftwareInstallerPayload, expectedStatus int, expectedError string) {
|
||||
func (s *integrationEnterpriseTestSuite) uploadSoftwareInstaller(
|
||||
payload *fleet.UploadSoftwareInstallerPayload,
|
||||
expectedStatus int,
|
||||
expectedError string,
|
||||
) {
|
||||
t := s.T()
|
||||
t.Helper()
|
||||
openFile := func(name string) *os.File {
|
||||
|
|
@ -11388,6 +11425,8 @@ func (s *integrationEnterpriseTestSuite) uploadSoftwareInstaller(payload *fleet.
|
|||
}
|
||||
|
||||
r := s.DoRawWithHeaders("POST", "/api/latest/fleet/software/package", b.Bytes(), expectedStatus, headers)
|
||||
defer r.Body.Close()
|
||||
|
||||
if expectedError != "" {
|
||||
errMsg := extractServerErrorText(r.Body)
|
||||
require.Contains(t, errMsg, expectedError)
|
||||
|
|
@ -11711,6 +11750,21 @@ func (s *integrationEnterpriseTestSuite) TestPKGNewSoftwareTitleFlow() {
|
|||
)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestPKGNoVersion() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
team, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "team1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "some installer script",
|
||||
Filename: "no_version.pkg",
|
||||
TeamID: &team.ID,
|
||||
}
|
||||
s.uploadSoftwareInstaller(payload, http.StatusBadRequest, "Couldn't add. Fleet couldn't read the version from no_version.pkg.")
|
||||
}
|
||||
|
||||
// 1. host reports software
|
||||
// 2. reconciler runs, creates title
|
||||
// 3. installer is uploaded, matches existing software title
|
||||
|
|
@ -12700,3 +12754,619 @@ func (s *integrationEnterpriseTestSuite) TestVPPAppsWithoutMDM() {
|
|||
r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", orbitHost.ID, app.TitleID), &installSoftwareRequest{}, http.StatusUnprocessableEntity)
|
||||
require.Contains(t, extractServerErrorText(r.Body), "Couldn't install. MDM is turned off. Please make sure that MDM is turned on to install App Store apps.")
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
team1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "team1"})
|
||||
require.NoError(t, err)
|
||||
team2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "team2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
newHost := func(name string, teamID *uint, platform string) *fleet.Host {
|
||||
h, err := s.ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now().Add(-1 * time.Minute),
|
||||
OsqueryHostID: ptr.String(t.Name() + name),
|
||||
NodeKey: ptr.String(t.Name() + name),
|
||||
UUID: uuid.New().String(),
|
||||
Hostname: fmt.Sprintf("%s.%s.local", name, t.Name()),
|
||||
Platform: platform,
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return h
|
||||
}
|
||||
newFleetdHost := func(name string, teamID *uint, platform string) *fleet.Host {
|
||||
h := newHost(name, teamID, platform)
|
||||
orbitKey := setOrbitEnrollment(t, h, s.ds)
|
||||
h.OrbitNodeKey = &orbitKey
|
||||
return h
|
||||
}
|
||||
|
||||
host0NoTeam := newFleetdHost("host1NoTeam", nil, "darwin")
|
||||
host1Team1 := newFleetdHost("host1Team1", &team1.ID, "darwin")
|
||||
host2Team1 := newFleetdHost("host2Team1", &team1.ID, "ubuntu")
|
||||
host3Team2 := newFleetdHost("host3Team2", &team2.ID, "windows")
|
||||
hostVanillaOsquery5Team1 := newHost("hostVanillaOsquery5Team2", &team1.ID, "darwin")
|
||||
|
||||
// Upload dummy_installer.pkg to team1.
|
||||
pkgPayload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "some pkg install script",
|
||||
Filename: "dummy_installer.pkg",
|
||||
TeamID: &team1.ID,
|
||||
}
|
||||
s.uploadSoftwareInstaller(pkgPayload, http.StatusOK, "")
|
||||
// Get software title ID of the uploaded installer.
|
||||
resp := listSoftwareTitlesResponse{}
|
||||
s.DoJSON(
|
||||
"GET", "/api/latest/fleet/software/titles",
|
||||
listSoftwareTitlesRequest{},
|
||||
http.StatusOK, &resp,
|
||||
"query", "DummyApp.app",
|
||||
"team_id", fmt.Sprintf("%d", team1.ID),
|
||||
)
|
||||
require.Len(t, resp.SoftwareTitles, 1)
|
||||
require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage)
|
||||
dummyInstallerPkgTitleID := resp.SoftwareTitles[0].ID
|
||||
var dummyInstallerPkg struct {
|
||||
ID uint `db:"id"`
|
||||
UserID *uint `db:"user_id"`
|
||||
UserName string `db:"user_name"`
|
||||
UserEmail string `db:"user_email"`
|
||||
}
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q,
|
||||
&dummyInstallerPkg,
|
||||
`SELECT id, user_id, user_name, user_email FROM software_installers WHERE global_or_team_id = ? AND filename = ?`,
|
||||
team1.ID, "dummy_installer.pkg",
|
||||
)
|
||||
})
|
||||
dummyInstallerPkgInstallerID := dummyInstallerPkg.ID
|
||||
require.NotZero(t, dummyInstallerPkgInstallerID)
|
||||
require.NotNil(t, dummyInstallerPkg.UserID)
|
||||
globalAdmin, err := s.ds.UserByEmail(ctx, "admin1@example.com")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, globalAdmin.ID, *dummyInstallerPkg.UserID)
|
||||
require.Equal(t, "Test Name admin1@example.com", dummyInstallerPkg.UserName)
|
||||
require.Equal(t, "admin1@example.com", dummyInstallerPkg.UserEmail)
|
||||
|
||||
// Upload ruby.deb to team1 by a user who will be deleted.
|
||||
u2 := &fleet.User{
|
||||
Name: "admin team1",
|
||||
Email: "admin_team1@example.com",
|
||||
GlobalRole: nil,
|
||||
Teams: []fleet.UserTeam{
|
||||
{
|
||||
Team: *team1,
|
||||
Role: fleet.RoleAdmin,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, u2.SetPassword(test.GoodPassword, 10, 10))
|
||||
adminTeam1, err := s.ds.NewUser(context.Background(), u2)
|
||||
require.NoError(t, err)
|
||||
rubyPayload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "some deb install script",
|
||||
Filename: "ruby.deb",
|
||||
TeamID: &team1.ID,
|
||||
}
|
||||
sessionKey := uuid.New().String()
|
||||
adminTeam1Session, err := s.ds.NewSession(ctx, adminTeam1.ID, sessionKey)
|
||||
require.NoError(t, err)
|
||||
adminToken := s.token
|
||||
t.Cleanup(func() {
|
||||
s.token = adminToken
|
||||
})
|
||||
s.token = adminTeam1Session.Key
|
||||
s.uploadSoftwareInstaller(rubyPayload, http.StatusOK, "")
|
||||
s.token = adminToken
|
||||
err = s.ds.DeleteUser(ctx, adminTeam1.ID)
|
||||
require.NoError(t, err)
|
||||
// Get software title ID of the uploaded installer.
|
||||
resp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON(
|
||||
"GET", "/api/latest/fleet/software/titles",
|
||||
listSoftwareTitlesRequest{},
|
||||
http.StatusOK, &resp,
|
||||
"query", "ruby",
|
||||
"team_id", fmt.Sprintf("%d", team1.ID),
|
||||
)
|
||||
require.Len(t, resp.SoftwareTitles, 1)
|
||||
require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage)
|
||||
rubyDebTitleID := resp.SoftwareTitles[0].ID
|
||||
var rubyDeb struct {
|
||||
ID uint `db:"id"`
|
||||
UserID *uint `db:"user_id"`
|
||||
UserName string `db:"user_name"`
|
||||
UserEmail string `db:"user_email"`
|
||||
}
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q,
|
||||
&rubyDeb,
|
||||
`SELECT id, user_id, user_name, user_email FROM software_installers WHERE global_or_team_id = ? AND filename = ?`,
|
||||
team1.ID, "ruby.deb",
|
||||
)
|
||||
})
|
||||
rubyDebInstallerID := rubyDeb.ID
|
||||
require.NotZero(t, rubyDebInstallerID)
|
||||
require.Nil(t, rubyDeb.UserID)
|
||||
require.Equal(t, "admin team1", rubyDeb.UserName)
|
||||
require.Equal(t, "admin_team1@example.com", rubyDeb.UserEmail)
|
||||
|
||||
// Upload fleet-osquery.msi to team2.
|
||||
fleetOsqueryPayload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "some msi install script",
|
||||
Filename: "fleet-osquery.msi",
|
||||
TeamID: &team2.ID,
|
||||
// Set as Self-service to check that the generated host_software_installs
|
||||
// is generated with self_service=false and the activity has the correct
|
||||
// author (the admin that uploaded the installer).
|
||||
SelfService: true,
|
||||
}
|
||||
s.uploadSoftwareInstaller(fleetOsqueryPayload, http.StatusOK, "")
|
||||
// Get software title ID of the uploaded installer.
|
||||
resp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON(
|
||||
"GET", "/api/latest/fleet/software/titles",
|
||||
listSoftwareTitlesRequest{},
|
||||
http.StatusOK, &resp,
|
||||
"query", "Fleet osquery",
|
||||
"team_id", fmt.Sprintf("%d", team2.ID),
|
||||
)
|
||||
require.Len(t, resp.SoftwareTitles, 1)
|
||||
require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage)
|
||||
fleetOsqueryMSITitleID := resp.SoftwareTitles[0].ID
|
||||
var fleetOsqueryMSIInstallerID uint
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q,
|
||||
&fleetOsqueryMSIInstallerID,
|
||||
`SELECT id FROM software_installers WHERE global_or_team_id = ? AND filename = ?`,
|
||||
team2.ID, "fleet-osquery.msi",
|
||||
)
|
||||
})
|
||||
require.NotZero(t, fleetOsqueryMSIInstallerID)
|
||||
|
||||
// Create a VPP app to test that policies cannot be assigned to them.
|
||||
_, err = s.ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
|
||||
Name: "App123 " + t.Name(),
|
||||
BundleIdentifier: "bid_" + t.Name(),
|
||||
VPPAppTeam: fleet.VPPAppTeam{
|
||||
VPPAppID: fleet.VPPAppID{
|
||||
AdamID: "adam_" + t.Name(),
|
||||
Platform: fleet.MacOSPlatform,
|
||||
},
|
||||
},
|
||||
}, &team1.ID)
|
||||
require.NoError(t, err)
|
||||
// Get software title ID of the uploaded VPP app.
|
||||
resp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON(
|
||||
"GET", "/api/latest/fleet/software/titles",
|
||||
listSoftwareTitlesRequest{},
|
||||
http.StatusOK, &resp,
|
||||
"query", "App123",
|
||||
"team_id", fmt.Sprintf("%d", team1.ID),
|
||||
)
|
||||
require.Len(t, resp.SoftwareTitles, 1)
|
||||
require.NotNil(t, resp.SoftwareTitles[0].AppStoreApp)
|
||||
vppAppTitleID := resp.SoftwareTitles[0].ID
|
||||
|
||||
// Populate software for host1Team1 (to have a software title
|
||||
// that doesn't have an associated installer)
|
||||
software := []fleet.Software{
|
||||
{Name: "Foobar.app", Version: "0.0.1", Source: "apps"},
|
||||
}
|
||||
_, err = s.ds.UpdateHostSoftware(ctx, host1Team1.ID, software)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, s.ds.SyncHostsSoftware(ctx, time.Now()))
|
||||
require.NoError(t, s.ds.ReconcileSoftwareTitles(ctx))
|
||||
require.NoError(t, s.ds.SyncHostsSoftwareTitles(ctx, time.Now()))
|
||||
// Get software title ID of the software.
|
||||
resp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON(
|
||||
"GET", "/api/latest/fleet/software/titles",
|
||||
listSoftwareTitlesRequest{},
|
||||
http.StatusOK, &resp,
|
||||
"query", "Foobar.app",
|
||||
"team_id", fmt.Sprintf("%d", team1.ID),
|
||||
)
|
||||
require.Len(t, resp.SoftwareTitles, 1)
|
||||
require.Nil(t, resp.SoftwareTitles[0].SoftwarePackage)
|
||||
foobarAppTitleID := resp.SoftwareTitles[0].ID
|
||||
|
||||
// policy0AllTeams is a global policy that runs on all devices.
|
||||
policy0AllTeams, err := s.ds.NewGlobalPolicy(ctx, nil, fleet.PolicyPayload{
|
||||
Name: "policy0AllTeams",
|
||||
Query: "SELECT 1;",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// policy1Team1 runs on macOS devices.
|
||||
policy1Team1, err := s.ds.NewTeamPolicy(ctx, team1.ID, nil, fleet.PolicyPayload{
|
||||
Name: "policy1Team1",
|
||||
Query: "SELECT 1;",
|
||||
Platform: "darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// policy2Team1 runs on macOS and Linux devices.
|
||||
policy2Team1, err := s.ds.NewTeamPolicy(ctx, team1.ID, nil, fleet.PolicyPayload{
|
||||
Name: "policy2Team1",
|
||||
Query: "SELECT 2;",
|
||||
Platform: "linux,darwin",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// policy3Team1 runs on all devices in team1 (will have no associated installers).
|
||||
policy3Team1, err := s.ds.NewTeamPolicy(ctx, team1.ID, nil, fleet.PolicyPayload{
|
||||
Name: "policy3Team1",
|
||||
Query: "SELECT 3;",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// policy4Team2 runs on Windows devices.
|
||||
policy4Team2, err := s.ds.NewTeamPolicy(ctx, team2.ID, nil, fleet.PolicyPayload{
|
||||
Name: "policy4Team2",
|
||||
Query: "SELECT 4;",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Attempt to associate to an unknown software title.
|
||||
mtplr := modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: ptr.Uint(999_999),
|
||||
},
|
||||
}, http.StatusBadRequest, &mtplr)
|
||||
// Attempt to associate to a software title without associated installer.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: ptr.Uint(foobarAppTitleID),
|
||||
},
|
||||
}, http.StatusBadRequest, &mtplr)
|
||||
// Attempt to associate vppApp to policy1Team1 which should fail because we only allow associating software installers.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: &vppAppTitleID,
|
||||
},
|
||||
}, http.StatusBadRequest, &mtplr)
|
||||
// Associate dummy_installer.pkg to policy1Team1.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: &dummyInstallerPkgTitleID,
|
||||
},
|
||||
}, http.StatusOK, &mtplr)
|
||||
// Change name only (to test not setting a software_title_id).
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID),
|
||||
json.RawMessage(`{"name": "policy1Team1_Renamed"}`), http.StatusOK, &mtplr,
|
||||
)
|
||||
policy1Team1, err = s.ds.Policy(ctx, policy1Team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, policy1Team1.SoftwareInstallerID)
|
||||
require.Equal(t, dummyInstallerPkgInstallerID, *policy1Team1.SoftwareInstallerID)
|
||||
require.Equal(t, "policy1Team1_Renamed", *&policy1Team1.Name)
|
||||
// Explicit set to 0 to disable.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: ptr.Uint(0),
|
||||
},
|
||||
}, http.StatusOK, &mtplr)
|
||||
policy1Team1, err = s.ds.Policy(ctx, policy1Team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, policy1Team1.SoftwareInstallerID)
|
||||
// Back to associating dummy_installer.pkg to policy1Team1.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy1Team1.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: &dummyInstallerPkgTitleID,
|
||||
},
|
||||
}, http.StatusOK, &mtplr)
|
||||
policy1Team1, err = s.ds.Policy(ctx, policy1Team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, policy1Team1.SoftwareInstallerID)
|
||||
require.Equal(t, dummyInstallerPkgInstallerID, *policy1Team1.SoftwareInstallerID)
|
||||
|
||||
// Associate ruby.deb to policy2Team1.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team1.ID, policy2Team1.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: &rubyDebTitleID,
|
||||
},
|
||||
}, http.StatusOK, &mtplr)
|
||||
|
||||
host1LastInstall, err := s.ds.GetHostLastInstallData(ctx, host1Team1.ID, dummyInstallerPkgInstallerID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, host1LastInstall)
|
||||
|
||||
// We use DoJSONWithoutAuth for distributed/write because we want the requests to not have the
|
||||
// current user's "Authorization: Bearer <API_TOKEN>" header.
|
||||
|
||||
// host1Team1 fails all policies on the first report.
|
||||
// Failing policy1Team1 means an install request must be generated.
|
||||
// Failing policy2Team1 should not trigger a install request because it has a .deb attached to it (does not apply to macOS hosts).
|
||||
// Failing policy3Team1 should do nothing because it doesn't have any installers associated to it.
|
||||
distributedResp := submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host1Team1,
|
||||
map[uint]*bool{
|
||||
policy1Team1.ID: ptr.Bool(false),
|
||||
policy2Team1.ID: ptr.Bool(false),
|
||||
policy3Team1.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host1Team1.ID, dummyInstallerPkgInstallerID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.NotEmpty(t, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
prevExecutionID := host1LastInstall.ExecutionID
|
||||
|
||||
// Request a manual installation on the host for the same installer, which should fail.
|
||||
var installResp installSoftwareResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d",
|
||||
host1Team1.ID, dummyInstallerPkgTitleID), nil, http.StatusBadRequest, &installResp)
|
||||
|
||||
// Submit same results as before, which should not trigger a installation because the policy is already failing.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host1Team1,
|
||||
map[uint]*bool{
|
||||
policy1Team1.ID: ptr.Bool(false),
|
||||
policy2Team1.ID: ptr.Bool(false),
|
||||
policy3Team1.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host1Team1.ID, dummyInstallerPkgInstallerID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, prevExecutionID, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
|
||||
// Submit same results but policy1Team1 now passes,
|
||||
// and then submit again but policy1Team1 fails.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host1Team1,
|
||||
map[uint]*bool{
|
||||
policy1Team1.ID: ptr.Bool(true),
|
||||
policy2Team1.ID: ptr.Bool(false),
|
||||
policy3Team1.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host1Team1,
|
||||
map[uint]*bool{
|
||||
policy1Team1.ID: ptr.Bool(false),
|
||||
policy2Team1.ID: ptr.Bool(false),
|
||||
policy3Team1.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
// Another installation should not be triggered because the last installation is pending.
|
||||
host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host1Team1.ID, dummyInstallerPkgInstallerID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, prevExecutionID, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
|
||||
// host2Team1 is failing policy2Team1 and policy3Team1 policies.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host2Team1,
|
||||
map[uint]*bool{
|
||||
policy2Team1.ID: ptr.Bool(false),
|
||||
policy3Team1.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
host2LastInstall, err := s.ds.GetHostLastInstallData(ctx, host2Team1.ID, rubyDebInstallerID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host2LastInstall)
|
||||
require.NotEmpty(t, host2LastInstall.ExecutionID)
|
||||
require.NotNil(t, host2LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host2LastInstall.Status)
|
||||
|
||||
// Associate fleet-osquery.msi to policy4Team2.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team2.ID, policy4Team2.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: &fleetOsqueryMSITitleID,
|
||||
},
|
||||
}, http.StatusOK, &mtplr)
|
||||
|
||||
// host3Team2 reports a failing result for policy4Team2, which should trigger an installation.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host3Team2,
|
||||
map[uint]*bool{
|
||||
policy4Team2.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
host3LastInstall, err := s.ds.GetHostLastInstallData(ctx, host3Team2.ID, fleetOsqueryMSIInstallerID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host3LastInstall)
|
||||
require.NotEmpty(t, host3LastInstall.ExecutionID)
|
||||
require.NotNil(t, host3LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host3LastInstall.Status)
|
||||
host3LastInstallDetails, err := s.ds.GetSoftwareInstallDetails(ctx, host3LastInstall.ExecutionID)
|
||||
require.NoError(t, err)
|
||||
// Even if fleet-osquery.msi was uploaded as Self-service, it was installed by Fleet, so
|
||||
// host3LastInstallDetails.SelfService should be false.
|
||||
require.False(t, host3LastInstallDetails.SelfService)
|
||||
|
||||
//
|
||||
// The following increase coverage of policies result processing in distributed/write.
|
||||
//
|
||||
|
||||
// host3Team2 reports a passing result for policy0AllTeams which is a global policy.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host3Team2,
|
||||
map[uint]*bool{
|
||||
policy0AllTeams.ID: ptr.Bool(true),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
// host0NoTeam reports a failing result for policy0AllTeams which is a global policy.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host0NoTeam,
|
||||
map[uint]*bool{
|
||||
policy0AllTeams.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
// host3Team2 reports a failing result for policy0AllTeams which is a global policy.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host3Team2,
|
||||
map[uint]*bool{
|
||||
policy0AllTeams.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
// Unassociate policy4Team2 from installer.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team2.ID, policy4Team2.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
SoftwareTitleID: ptr.Uint(0),
|
||||
},
|
||||
}, http.StatusOK, &mtplr)
|
||||
|
||||
// host3Team2 reports a failing result for policy4Team2.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
host3Team2,
|
||||
map[uint]*bool{
|
||||
policy4Team2.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
// Upcoming activities for host1Team1 should show the automatic installation of dummy_installer.pkg.
|
||||
// Check the author should be the admin that uploaded the installer.
|
||||
var listUpcomingAct listHostUpcomingActivitiesResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", host1Team1.ID), nil, http.StatusOK, &listUpcomingAct)
|
||||
require.Len(t, listUpcomingAct.Activities, 1)
|
||||
require.NotNil(t, listUpcomingAct.Activities[0].ActorID)
|
||||
require.Equal(t, globalAdmin.ID, *listUpcomingAct.Activities[0].ActorID)
|
||||
require.Equal(t, globalAdmin.Name, *listUpcomingAct.Activities[0].ActorFullName)
|
||||
require.Equal(t, globalAdmin.Email, *listUpcomingAct.Activities[0].ActorEmail)
|
||||
|
||||
//
|
||||
// Finally have orbit install the packages and check activities.
|
||||
//
|
||||
|
||||
// host1Team1 posts the installation result for dummy_installer.pkg.
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
|
||||
"orbit_node_key": %q,
|
||||
"install_uuid": %q,
|
||||
"pre_install_condition_output": "ok",
|
||||
"install_script_exit_code": 0,
|
||||
"install_script_output": "ok"
|
||||
}`, *host1Team1.OrbitNodeKey, host1LastInstall.ExecutionID)), http.StatusNoContent)
|
||||
s.lastActivityMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), fmt.Sprintf(`{
|
||||
"host_id": %d,
|
||||
"host_display_name": "%s",
|
||||
"software_title": "%s",
|
||||
"software_package": "%s",
|
||||
"self_service": false,
|
||||
"install_uuid": "%s",
|
||||
"status": "installed"
|
||||
}`, host1Team1.ID, host1Team1.DisplayName(), "DummyApp.app", "dummy_installer.pkg", host1LastInstall.ExecutionID), 0)
|
||||
|
||||
// host2Team1 posts the installation result for ruby.deb.
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
|
||||
"orbit_node_key": %q,
|
||||
"install_uuid": %q,
|
||||
"pre_install_condition_output": "ok",
|
||||
"install_script_exit_code": 1,
|
||||
"install_script_output": "failed"
|
||||
}`, *host2Team1.OrbitNodeKey, host2LastInstall.ExecutionID)), http.StatusNoContent)
|
||||
activityID := s.lastActivityMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), fmt.Sprintf(`{
|
||||
"host_id": %d,
|
||||
"host_display_name": "%s",
|
||||
"software_title": "%s",
|
||||
"software_package": "%s",
|
||||
"self_service": false,
|
||||
"install_uuid": "%s",
|
||||
"status": "failed"
|
||||
}`, host2Team1.ID, host2Team1.DisplayName(), "ruby", "ruby.deb", host2LastInstall.ExecutionID), 0)
|
||||
|
||||
// Check that the activity item generated for ruby.deb installation has a null user,
|
||||
// but has name and email set.
|
||||
var actor struct {
|
||||
UserID *uint `db:"user_id"`
|
||||
UserName *string `db:"user_name"`
|
||||
UserEmail string `db:"user_email"`
|
||||
}
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q,
|
||||
&actor,
|
||||
`SELECT user_id, user_name, user_email FROM activities WHERE id = ?`,
|
||||
activityID,
|
||||
)
|
||||
})
|
||||
require.Nil(t, actor.UserID)
|
||||
require.NotNil(t, actor.UserName)
|
||||
require.Equal(t, "admin team1", *actor.UserName)
|
||||
require.Equal(t, "admin_team1@example.com", actor.UserEmail)
|
||||
|
||||
// host3Team2 posts the installation result for fleet-osquery.msi.
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
|
||||
"orbit_node_key": %q,
|
||||
"install_uuid": %q,
|
||||
"pre_install_condition_output": "ok",
|
||||
"install_script_exit_code": 1,
|
||||
"install_script_output": "failed"
|
||||
}`, *host3Team2.OrbitNodeKey, host3LastInstall.ExecutionID)), http.StatusNoContent)
|
||||
activityID = s.lastActivityMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), fmt.Sprintf(`{
|
||||
"host_id": %d,
|
||||
"host_display_name": "%s",
|
||||
"software_title": "%s",
|
||||
"software_package": "%s",
|
||||
"self_service": false,
|
||||
"install_uuid": "%s",
|
||||
"status": "failed"
|
||||
}`, host3Team2.ID, host3Team2.DisplayName(), "Fleet osquery", "fleet-osquery.msi", host3LastInstall.ExecutionID), 0)
|
||||
|
||||
// Check that the activity item generated for fleet-osquery.msi installation has the admin user set as author.
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q,
|
||||
&actor,
|
||||
`SELECT user_id, user_name, user_email FROM activities WHERE id = ?`,
|
||||
activityID,
|
||||
)
|
||||
})
|
||||
require.NotNil(t, actor.UserID)
|
||||
require.Equal(t, globalAdmin.ID, *actor.UserID)
|
||||
require.NotNil(t, actor.UserName)
|
||||
require.Equal(t, "Test Name admin1@example.com", *actor.UserName)
|
||||
require.Equal(t, "admin1@example.com", actor.UserEmail)
|
||||
|
||||
// hostVanillaOsquery5Team1 sends policy results with failed policies with associated installers.
|
||||
// Fleet should not queue an install for vanilla osquery hosts.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
|
||||
hostVanillaOsquery5Team1,
|
||||
map[uint]*bool{
|
||||
policy1Team1.ID: ptr.Bool(false),
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
hostVanillaOsquery5Team1LastInstall, err := s.ds.GetHostLastInstallData(ctx, hostVanillaOsquery5Team1.ID, dummyInstallerPkgInstallerID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, hostVanillaOsquery5Team1LastInstall)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3782,17 +3782,18 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfiles() {
|
|||
|
||||
// apply an empty set to no-team
|
||||
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: nil}, http.StatusNoContent)
|
||||
s.lastActivityOfTypeMatches(
|
||||
// Nothing changed, so no activity items
|
||||
s.lastActivityOfTypeDoesNotMatch(
|
||||
fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
|
||||
`{"team_id": null, "team_name": null}`,
|
||||
0,
|
||||
)
|
||||
s.lastActivityOfTypeMatches(
|
||||
s.lastActivityOfTypeDoesNotMatch(
|
||||
fleet.ActivityTypeEditedWindowsProfile{}.ActivityName(),
|
||||
`{"team_id": null, "team_name": null}`,
|
||||
0,
|
||||
)
|
||||
s.lastActivityOfTypeMatches(
|
||||
s.lastActivityOfTypeDoesNotMatch(
|
||||
fleet.ActivityTypeEditedDeclarationProfile{}.ActivityName(),
|
||||
`{"team_id": null, "team_name": null}`,
|
||||
0,
|
||||
|
|
@ -4062,12 +4063,13 @@ func (s *integrationMDMTestSuite) TestBatchSetMDMProfilesBackwardsCompat() {
|
|||
|
||||
// apply an empty set to no-team
|
||||
s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", map[string]any{"profiles": nil}, http.StatusNoContent)
|
||||
s.lastActivityOfTypeMatches(
|
||||
// Nothing changed, so no activity
|
||||
s.lastActivityOfTypeDoesNotMatch(
|
||||
fleet.ActivityTypeEditedMacosProfile{}.ActivityName(),
|
||||
`{"team_id": null, "team_name": null}`,
|
||||
0,
|
||||
)
|
||||
s.lastActivityOfTypeMatches(
|
||||
s.lastActivityOfTypeDoesNotMatch(
|
||||
fleet.ActivityTypeEditedWindowsProfile{}.ActivityName(),
|
||||
`{"team_id": null, "team_name": null}`,
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -4426,7 +4426,9 @@ func (s *integrationMDMTestSuite) assertWindowsConfigProfilesByName(teamID *uint
|
|||
}
|
||||
var cfgProfs []*fleet.MDMWindowsConfigProfile
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT * FROM mdm_windows_configuration_profiles WHERE team_id = ?`, teamID)
|
||||
return sqlx.SelectContext(context.Background(), q, &cfgProfs,
|
||||
`SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ?`,
|
||||
teamID)
|
||||
})
|
||||
|
||||
label := "exist"
|
||||
|
|
|
|||
|
|
@ -1167,7 +1167,7 @@ func (svc *Service) DeleteMDMWindowsConfigProfile(ctx context.Context, profileUU
|
|||
}
|
||||
|
||||
// cannot use the profile ID as it is now deleted
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
|
||||
|
|
@ -1412,7 +1412,7 @@ func (svc *Service) NewMDMWindowsConfigProfile(ctx context.Context, teamID uint,
|
|||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newCP.ProfileUUID}, nil); err != nil {
|
||||
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, []string{newCP.ProfileUUID}, nil); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
|
||||
}
|
||||
|
||||
|
|
@ -1600,7 +1600,8 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := svc.ds.BatchSetMDMProfiles(ctx, tmID, appleProfiles, windowsProfiles, appleDecls); err != nil {
|
||||
var profUpdates fleet.MDMProfilesUpdates
|
||||
if profUpdates, err = svc.ds.BatchSetMDMProfiles(ctx, tmID, appleProfiles, windowsProfiles, appleDecls); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting config profiles")
|
||||
}
|
||||
|
||||
|
|
@ -1609,7 +1610,8 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||
for _, p := range windowsProfiles {
|
||||
winProfUUIDs = append(winProfUUIDs, p.ProfileUUID)
|
||||
}
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, winProfUUIDs, nil); err != nil {
|
||||
winUpdates, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, winProfUUIDs, nil)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending windows host profiles")
|
||||
}
|
||||
|
||||
|
|
@ -1618,33 +1620,42 @@ func (svc *Service) BatchSetMDMProfiles(
|
|||
for _, p := range appleProfiles {
|
||||
appleProfUUIDs = append(appleProfUUIDs, p.ProfileUUID)
|
||||
}
|
||||
if err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, appleProfUUIDs, nil); err != nil {
|
||||
appleUpdates, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, nil, appleProfUUIDs, nil)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "bulk set pending apple host profiles")
|
||||
}
|
||||
updates := fleet.MDMProfilesUpdates{
|
||||
AppleConfigProfile: profUpdates.AppleConfigProfile || winUpdates.AppleConfigProfile || appleUpdates.AppleConfigProfile,
|
||||
WindowsConfigProfile: profUpdates.WindowsConfigProfile || winUpdates.WindowsConfigProfile || appleUpdates.WindowsConfigProfile,
|
||||
AppleDeclaration: profUpdates.AppleDeclaration || winUpdates.AppleDeclaration || appleUpdates.AppleDeclaration,
|
||||
}
|
||||
|
||||
// TODO(roberto): should we generate activities only of any profiles were
|
||||
// changed? this is the existing behavior for macOS profiles so I'm
|
||||
// leaving it as-is for now.
|
||||
if err := svc.NewActivity(
|
||||
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedMacosProfile{
|
||||
TeamID: tmID,
|
||||
TeamName: tmName,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "logging activity for edited macos profile")
|
||||
if updates.AppleConfigProfile {
|
||||
if err := svc.NewActivity(
|
||||
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedMacosProfile{
|
||||
TeamID: tmID,
|
||||
TeamName: tmName,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "logging activity for edited macos profile")
|
||||
}
|
||||
}
|
||||
if err := svc.NewActivity(
|
||||
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedWindowsProfile{
|
||||
TeamID: tmID,
|
||||
TeamName: tmName,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "logging activity for edited windows profile")
|
||||
if updates.WindowsConfigProfile {
|
||||
if err := svc.NewActivity(
|
||||
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedWindowsProfile{
|
||||
TeamID: tmID,
|
||||
TeamName: tmName,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "logging activity for edited windows profile")
|
||||
}
|
||||
}
|
||||
if err := svc.NewActivity(
|
||||
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedDeclarationProfile{
|
||||
TeamID: tmID,
|
||||
TeamName: tmName,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "logging activity for edited macos declarations")
|
||||
if updates.AppleDeclaration {
|
||||
if err := svc.NewActivity(
|
||||
ctx, authz.UserFromContext(ctx), &fleet.ActivityTypeEditedDeclarationProfile{
|
||||
TeamID: tmID,
|
||||
TeamName: tmName,
|
||||
}); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "logging activity for edited macos declarations")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1068,8 +1068,10 @@ func TestMDMWindowsConfigProfileAuthz(t *testing.T) {
|
|||
ds.ListMDMConfigProfilesFunc = func(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.MDMConfigProfilePayload, *fleet.PaginationMetadata, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string,
|
||||
hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
checkShouldFail := func(t *testing.T, err error, shouldFail bool) {
|
||||
|
|
@ -1142,8 +1144,10 @@ func TestUploadWindowsMDMConfigProfileValidations(t *testing.T) {
|
|||
cp.ProfileUUID = uuid.New().String()
|
||||
return &cp, nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string,
|
||||
hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
|
|
@ -1225,16 +1229,20 @@ func TestMDMBatchSetProfiles(t *testing.T) {
|
|||
ds.TeamFunc = func(ctx context.Context, id uint) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: id, Name: "team"}, nil
|
||||
}
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration) error {
|
||||
return nil
|
||||
ds.BatchSetMDMProfilesFunc = func(ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile,
|
||||
winProfiles []*fleet.MDMWindowsConfigProfile, macDecls []*fleet.MDMAppleDeclaration,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.NewActivityFunc = func(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string,
|
||||
hostUUIDs []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
|
|
|
|||
|
|
@ -998,11 +998,31 @@ func (svc *Service) SaveHostSoftwareInstallResult(ctx context.Context, result *f
|
|||
return ctxerr.Wrap(ctx, err, "get host software installation result information")
|
||||
}
|
||||
|
||||
// Self-Service packages will have a nil author for the activity.
|
||||
var user *fleet.User
|
||||
if hsi.UserID != nil && !hsi.SelfService {
|
||||
user, err = svc.ds.UserByID(ctx, *hsi.UserID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host software installation user")
|
||||
if !hsi.SelfService {
|
||||
if hsi.UserID != nil {
|
||||
user, err = svc.ds.UserByID(ctx, *hsi.UserID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host software installation user")
|
||||
}
|
||||
} else {
|
||||
// hsi.UserID can be nil if the user was deleted and/or if the installation was
|
||||
// triggered by Fleet (policy automation). Thus we set the author of the installation
|
||||
// to be the user that uploaded the package (by design).
|
||||
var userID uint
|
||||
if hsi.SoftwareInstallerUserID != nil {
|
||||
userID = *hsi.SoftwareInstallerUserID
|
||||
}
|
||||
// If there's no name or email then this may be a package uploaded
|
||||
// before we added authorship to uploaded packages.
|
||||
if hsi.SoftwareInstallerUserName != "" && hsi.SoftwareInstallerUserEmail != "" {
|
||||
user = &fleet.User{
|
||||
ID: userID,
|
||||
Name: hsi.SoftwareInstallerUserName,
|
||||
Email: hsi.SoftwareInstallerUserEmail,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func (svc *Service) AuthenticateHost(ctx context.Context, nodeKey string) (*flee
|
|||
case err == nil:
|
||||
// OK
|
||||
case fleet.IsNotFound(err):
|
||||
return nil, false, newOsqueryErrorWithInvalidNode("authentication error: invalid node key: " + nodeKey)
|
||||
return nil, false, newOsqueryErrorWithInvalidNode("authentication error: invalid node key")
|
||||
default:
|
||||
return nil, false, newOsqueryError("authentication error: " + err.Error())
|
||||
}
|
||||
|
|
@ -1008,6 +1008,10 @@ func (svc *Service) SubmitDistributedQueryResults(
|
|||
logging.WithErr(ctx, err)
|
||||
}
|
||||
|
||||
if err := svc.processSoftwareForNewlyFailingPolicies(ctx, host.ID, host.TeamID, host.Platform, host.OrbitNodeKey, policyResults); err != nil {
|
||||
logging.WithErr(ctx, err)
|
||||
}
|
||||
|
||||
// filter policy results for webhooks
|
||||
var policyIDs []uint
|
||||
if globalPolicyAutomationsEnabled(ac.WebhookSettings, ac.Integrations) {
|
||||
|
|
@ -1038,6 +1042,7 @@ func (svc *Service) SubmitDistributedQueryResults(
|
|||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(mna): currently, failing policies webhook wouldn't see the new
|
||||
// flipped policies on the next run if async processing is enabled and the
|
||||
// collection has not been done yet (not persisted in mysql). Should
|
||||
|
|
@ -1606,6 +1611,141 @@ func (svc *Service) registerFlippedPolicies(ctx context.Context, hostID uint, ho
|
|||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) processSoftwareForNewlyFailingPolicies(
|
||||
ctx context.Context,
|
||||
hostID uint,
|
||||
hostTeamID *uint,
|
||||
hostPlatform string,
|
||||
hostOrbitNodeKey *string,
|
||||
incomingPolicyResults map[uint]*bool,
|
||||
) error {
|
||||
if hostOrbitNodeKey == nil || *hostOrbitNodeKey == "" {
|
||||
// We do not want to queue software installations on vanilla osquery hosts.
|
||||
return nil
|
||||
}
|
||||
if hostTeamID == nil {
|
||||
// TODO(lucas): Support hosts in "No team".
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter out results that are not failures (we are only interested on failing policies,
|
||||
// we don't care about passing policies or policies that failed to execute).
|
||||
incomingFailingPolicies := make(map[uint]*bool)
|
||||
var incomingFailingPoliciesIDs []uint
|
||||
for policyID, policyResult := range incomingPolicyResults {
|
||||
if policyResult != nil && !*policyResult {
|
||||
incomingFailingPolicies[policyID] = policyResult
|
||||
incomingFailingPoliciesIDs = append(incomingFailingPoliciesIDs, policyID)
|
||||
}
|
||||
}
|
||||
if len(incomingFailingPolicies) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get policies with associated installers for the team.
|
||||
policiesWithInstaller, err := svc.ds.GetPoliciesWithAssociatedInstaller(ctx, *hostTeamID, incomingFailingPoliciesIDs)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "failed to get policies with installer")
|
||||
}
|
||||
if len(policiesWithInstaller) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter out results of policies that are not associated to installers.
|
||||
policiesWithInstallersMap := make(map[uint]fleet.PolicySoftwareInstallerData)
|
||||
for _, policyWithInstaller := range policiesWithInstaller {
|
||||
policiesWithInstallersMap[policyWithInstaller.ID] = policyWithInstaller
|
||||
}
|
||||
policyResultsOfPoliciesWithInstallers := make(map[uint]*bool)
|
||||
for policyID, passes := range incomingFailingPolicies {
|
||||
if _, ok := policiesWithInstallersMap[policyID]; !ok {
|
||||
continue
|
||||
}
|
||||
policyResultsOfPoliciesWithInstallers[policyID] = passes
|
||||
}
|
||||
if len(policyResultsOfPoliciesWithInstallers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the policies associated with installers that are flipping from passing to failing on this host.
|
||||
policyIDsOfNewlyFailingPoliciesWithInstallers, _, err := svc.ds.FlippingPoliciesForHost(
|
||||
ctx, hostID, policyResultsOfPoliciesWithInstallers,
|
||||
)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "failed to get flipping policies for host")
|
||||
}
|
||||
if len(policyIDsOfNewlyFailingPoliciesWithInstallers) == 0 {
|
||||
return nil
|
||||
}
|
||||
policyIDsOfNewlyFailingPoliciesWithInstallersSet := make(map[uint]struct{})
|
||||
for _, policyID := range policyIDsOfNewlyFailingPoliciesWithInstallers {
|
||||
policyIDsOfNewlyFailingPoliciesWithInstallersSet[policyID] = struct{}{}
|
||||
}
|
||||
|
||||
// Finally filter out policies with installers that are not newly failing.
|
||||
var failingPoliciesWithInstaller []fleet.PolicySoftwareInstallerData
|
||||
for _, policyWithInstaller := range policiesWithInstaller {
|
||||
if _, ok := policyIDsOfNewlyFailingPoliciesWithInstallersSet[policyWithInstaller.ID]; ok {
|
||||
failingPoliciesWithInstaller = append(failingPoliciesWithInstaller, policyWithInstaller)
|
||||
}
|
||||
}
|
||||
|
||||
for _, failingPolicyWithInstaller := range failingPoliciesWithInstaller {
|
||||
installerMetadata, err := svc.ds.GetSoftwareInstallerMetadataByID(ctx, failingPolicyWithInstaller.InstallerID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get software installer metadata by id")
|
||||
}
|
||||
logger := log.With(svc.logger,
|
||||
"host_id", hostID,
|
||||
"host_platform", hostPlatform,
|
||||
"policy_id", failingPolicyWithInstaller.ID,
|
||||
"software_installer_id", failingPolicyWithInstaller.InstallerID,
|
||||
"software_title_id", installerMetadata.TitleID,
|
||||
"software_installer_platform", installerMetadata.Platform,
|
||||
)
|
||||
if fleet.PlatformFromHost(hostPlatform) != installerMetadata.Platform {
|
||||
level.Debug(logger).Log("msg", "installer platform does not match host platform")
|
||||
continue
|
||||
}
|
||||
hostLastInstall, err := svc.ds.GetHostLastInstallData(ctx, hostID, installerMetadata.InstallerID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host last install data")
|
||||
}
|
||||
// hostLastInstall.Status == nil can happen when a software is installed by Fleet and later removed.
|
||||
if hostLastInstall != nil && hostLastInstall.Status != nil &&
|
||||
*hostLastInstall.Status == fleet.SoftwareInstallerPending {
|
||||
// There's a pending install for this host and installer,
|
||||
// thus we do not queue another install request.
|
||||
level.Debug(svc.logger).Log(
|
||||
"msg", "found pending install request for this host and installer",
|
||||
"pending_execution_id", hostLastInstall.ExecutionID,
|
||||
)
|
||||
continue
|
||||
}
|
||||
// NOTE(lucas): The user_id set in this software install will be NULL
|
||||
// so this means that when generating the activity for this action
|
||||
// (in SaveHostSoftwareInstallResult)
|
||||
// the author will be set to the user that uploaded the software (we want this
|
||||
// by design).
|
||||
installUUID, err := svc.ds.InsertSoftwareInstallRequest(
|
||||
ctx, hostID,
|
||||
installerMetadata.InstallerID,
|
||||
false, // Set Self-service as false because this is triggered by Fleet.
|
||||
)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err,
|
||||
"insert software install request: host_id=%d, software_installer_id=%d",
|
||||
hostID, installerMetadata.InstallerID,
|
||||
)
|
||||
}
|
||||
level.Debug(logger).Log(
|
||||
"msg", "install request sent",
|
||||
"install_uuid", installUUID,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) maybeDebugHost(
|
||||
ctx context.Context,
|
||||
host *fleet.Host,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type teamPolicyRequest struct {
|
|||
Platform string `json:"platform"`
|
||||
Critical bool `json:"critical" premium:"true"`
|
||||
CalendarEventsEnabled bool `json:"calendar_events_enabled"`
|
||||
SoftwareTitleID *uint `json:"software_title_id"`
|
||||
}
|
||||
|
||||
type teamPolicyResponse struct {
|
||||
|
|
@ -40,7 +41,7 @@ func (r teamPolicyResponse) error() error { return r.Err }
|
|||
|
||||
func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*teamPolicyRequest)
|
||||
resp, err := svc.NewTeamPolicy(ctx, req.TeamID, fleet.PolicyPayload{
|
||||
resp, err := svc.NewTeamPolicy(ctx, req.TeamID, fleet.NewTeamPolicyPayload{
|
||||
QueryID: req.QueryID,
|
||||
Name: req.Name,
|
||||
Query: req.Query,
|
||||
|
|
@ -49,6 +50,7 @@ func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
|
|||
Platform: req.Platform,
|
||||
Critical: req.Critical,
|
||||
CalendarEventsEnabled: req.CalendarEventsEnabled,
|
||||
SoftwareTitleID: req.SoftwareTitleID,
|
||||
})
|
||||
if err != nil {
|
||||
return teamPolicyResponse{Err: err}, nil
|
||||
|
|
@ -56,7 +58,7 @@ func teamPolicyEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
|
|||
return teamPolicyResponse{Policy: resp}, nil
|
||||
}
|
||||
|
||||
func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, p fleet.PolicyPayload) (*fleet.Policy, error) {
|
||||
func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, tp fleet.NewTeamPolicyPayload) (*fleet.Policy, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Policy{
|
||||
PolicyData: fleet.PolicyData{
|
||||
TeamID: ptr.Uint(teamID),
|
||||
|
|
@ -70,6 +72,11 @@ func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, p fleet.Polic
|
|||
return nil, errors.New("user must be authenticated to create team policies")
|
||||
}
|
||||
|
||||
p, err := svc.newTeamPolicyPayloadToPolicyPayload(ctx, teamID, tp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.Verify(); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("policy payload verification: %s", err),
|
||||
|
|
@ -80,6 +87,10 @@ func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, p fleet.Polic
|
|||
return nil, ctxerr.Wrap(ctx, err, "creating policy")
|
||||
}
|
||||
|
||||
if err := svc.populatePolicyInstallSoftware(ctx, policy); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "populate install_software")
|
||||
}
|
||||
|
||||
// Note: Issue #4191 proposes that we move to SQL transactions for actions so that we can
|
||||
// rollback an action in the event of an error writing the associated activity
|
||||
if err := svc.NewActivity(
|
||||
|
|
@ -95,6 +106,39 @@ func (svc Service) NewTeamPolicy(ctx context.Context, teamID uint, p fleet.Polic
|
|||
return policy, nil
|
||||
}
|
||||
|
||||
func (svc *Service) populatePolicyInstallSoftware(ctx context.Context, p *fleet.Policy) error {
|
||||
if p.SoftwareInstallerID == nil {
|
||||
return nil
|
||||
}
|
||||
installerMetadata, err := svc.ds.GetSoftwareInstallerMetadataByID(ctx, *p.SoftwareInstallerID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get software installer metadata by id")
|
||||
}
|
||||
p.InstallSoftware = &fleet.PolicySoftwareTitle{
|
||||
SoftwareTitleID: *installerMetadata.TitleID,
|
||||
Name: installerMetadata.SoftwareTitle,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) newTeamPolicyPayloadToPolicyPayload(ctx context.Context, teamID uint, p fleet.NewTeamPolicyPayload) (fleet.PolicyPayload, error) {
|
||||
softwareInstallerID, err := svc.deduceSoftwareInstallerIDFromTitleID(ctx, &teamID, p.SoftwareTitleID)
|
||||
if err != nil {
|
||||
return fleet.PolicyPayload{}, err
|
||||
}
|
||||
return fleet.PolicyPayload{
|
||||
QueryID: p.QueryID,
|
||||
Name: p.Name,
|
||||
Query: p.Query,
|
||||
Critical: p.Critical,
|
||||
Description: p.Description,
|
||||
Resolution: p.Resolution,
|
||||
Platform: p.Platform,
|
||||
CalendarEventsEnabled: p.CalendarEventsEnabled,
|
||||
SoftwareInstallerID: softwareInstallerID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// List
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -148,11 +192,27 @@ func (svc *Service) ListTeamPolicies(ctx context.Context, teamID uint, opts flee
|
|||
}
|
||||
|
||||
if mergeInherited {
|
||||
p, err := svc.ds.ListMergedTeamPolicies(ctx, teamID, opts)
|
||||
return p, nil, err
|
||||
policies, err := svc.ds.ListMergedTeamPolicies(ctx, teamID, opts)
|
||||
for i := range policies {
|
||||
if err := svc.populatePolicyInstallSoftware(ctx, policies[i]); err != nil {
|
||||
return nil, nil, ctxerr.Wrapf(ctx, err, "populate install_software for policy_id: %d", policies[i].ID)
|
||||
}
|
||||
}
|
||||
return policies, nil, err
|
||||
}
|
||||
|
||||
return svc.ds.ListTeamPolicies(ctx, teamID, opts, iopts)
|
||||
teamPolicies, inheritedPolicies, err = svc.ds.ListTeamPolicies(ctx, teamID, opts, iopts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for i := range teamPolicies {
|
||||
if err := svc.populatePolicyInstallSoftware(ctx, teamPolicies[i]); err != nil {
|
||||
return nil, nil, ctxerr.Wrapf(ctx, err, "populate install_software for policy_id: %d", teamPolicies[i].ID)
|
||||
}
|
||||
}
|
||||
|
||||
return teamPolicies, inheritedPolicies, nil
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -240,6 +300,10 @@ func (svc Service) GetTeamPolicyByIDQueries(ctx context.Context, teamID uint, po
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := svc.populatePolicyInstallSoftware(ctx, teamPolicy); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "populate install_software")
|
||||
}
|
||||
|
||||
return teamPolicy, nil
|
||||
}
|
||||
|
||||
|
|
@ -418,6 +482,14 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
|
|||
policy.FailingHostCount = 0
|
||||
policy.PassingHostCount = 0
|
||||
}
|
||||
if p.SoftwareTitleID != nil {
|
||||
softwareInstallerID, err := svc.deduceSoftwareInstallerIDFromTitleID(ctx, teamID, p.SoftwareTitleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy.SoftwareInstallerID = softwareInstallerID
|
||||
}
|
||||
|
||||
logging.WithExtras(ctx, "name", policy.Name, "sql", policy.Query)
|
||||
|
||||
err = svc.ds.SavePolicy(ctx, policy, removeAllMemberships, removeStats)
|
||||
|
|
@ -425,6 +497,10 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
|
|||
return nil, ctxerr.Wrap(ctx, err, "saving policy")
|
||||
}
|
||||
|
||||
if err := svc.populatePolicyInstallSoftware(ctx, policy); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "populate install_software")
|
||||
}
|
||||
|
||||
// Note: Issue #4191 proposes that we move to SQL transactions for actions so that we can
|
||||
// rollback an action in the event of an error writing the associated activity
|
||||
if err := svc.NewActivity(
|
||||
|
|
@ -440,3 +516,48 @@ func (svc *Service) modifyPolicy(ctx context.Context, teamID *uint, id uint, p f
|
|||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (svc *Service) deduceSoftwareInstallerIDFromTitleID(ctx context.Context, teamID *uint, softwareTitleID *uint) (*uint, error) {
|
||||
if softwareTitleID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If *p.SoftwareTitleID with value 0 is used to unset the current installer from the policy.
|
||||
if *softwareTitleID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if teamID == nil {
|
||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: "software_title_id cannot be set on global policies",
|
||||
})
|
||||
}
|
||||
|
||||
softwareTitle, err := svc.SoftwareTitleByID(ctx, *softwareTitleID, teamID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("software_title_id %d on team_id %d not found", *softwareTitleID, *teamID),
|
||||
})
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "software title by id")
|
||||
}
|
||||
if softwareTitle.AppStoreApp != nil {
|
||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("software_title_id %d on team_id %d is assocated to a VPP app, only software installers are supported", *softwareTitleID, *teamID),
|
||||
})
|
||||
}
|
||||
if softwareTitle.SoftwarePackage == nil {
|
||||
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("software_title_id %d on team_id %d does not have associated package", *softwareTitleID, *teamID),
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// TODO(lucas): Support "No team" (softwareTitle.SoftwarePackage.TeamID == nil).
|
||||
//
|
||||
|
||||
// At this point we assume *softwareTitle.SoftwarePackage.TeamID == *teamID,
|
||||
// because SoftwareTitleByID above receives the teamID.
|
||||
return ptr.Uint(softwareTitle.SoftwarePackage.InstallerID), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func TestTeamPoliciesAuth(t *testing.T) {
|
|||
return nil, nil
|
||||
}
|
||||
ds.TeamPolicyFunc = func(ctx context.Context, teamID uint, policyID uint) (*fleet.Policy, error) {
|
||||
return nil, nil
|
||||
return &fleet.Policy{}, nil
|
||||
}
|
||||
ds.PolicyFunc = func(ctx context.Context, id uint) (*fleet.Policy, error) {
|
||||
if id == 1 {
|
||||
|
|
@ -68,6 +68,9 @@ func TestTeamPoliciesAuth(t *testing.T) {
|
|||
ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
|
||||
return &fleet.Team{ID: 1}, nil
|
||||
}
|
||||
ds.GetSoftwareInstallerMetadataByIDFunc = func(ctx context.Context, id uint) (*fleet.SoftwareInstaller, error) {
|
||||
return &fleet.SoftwareInstaller{}, nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
@ -149,7 +152,7 @@ func TestTeamPoliciesAuth(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := viewer.NewContext(ctx, viewer.Viewer{User: tt.user})
|
||||
|
||||
_, err := svc.NewTeamPolicy(ctx, 1, fleet.PolicyPayload{
|
||||
_, err := svc.NewTeamPolicy(ctx, 1, fleet.NewTeamPolicyPayload{
|
||||
Name: "query1",
|
||||
Query: "select 1;",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -53,8 +53,9 @@ func TestTeamAuth(t *testing.T) {
|
|||
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
||||
return nil
|
||||
}
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string) error {
|
||||
return nil
|
||||
ds.BulkSetPendingMDMHostProfilesFunc = func(ctx context.Context, hids, tids []uint, puuids, uuids []string,
|
||||
) (updates fleet.MDMProfilesUpdates, err error) {
|
||||
return fleet.MDMProfilesUpdates{}, nil
|
||||
}
|
||||
ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) {
|
||||
return []*fleet.Host{}, nil
|
||||
|
|
|
|||
3
server/service/testdata/software-installers/README.md
vendored
Normal file
3
server/service/testdata/software-installers/README.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# testdata
|
||||
|
||||
- `fleet-osquery.msi` is a dummy MSI installer created by `packaging.BuildMSI` with a fake `orbit.exe` that just has `hello world` in it. Its software title is `Fleet osquery` and its version is `1.0.0`.
|
||||
BIN
server/service/testdata/software-installers/fleet-osquery.msi
vendored
Normal file
BIN
server/service/testdata/software-installers/fleet-osquery.msi
vendored
Normal file
Binary file not shown.
BIN
server/service/testdata/software-installers/no_version.pkg
vendored
Normal file
BIN
server/service/testdata/software-installers/no_version.pkg
vendored
Normal file
Binary file not shown.
|
|
@ -118,12 +118,6 @@ func (ts *withServer) commonTearDownTest(t *testing.T) {
|
|||
require.NoError(t, ts.ds.DeleteHost(ctx, host.ID))
|
||||
}
|
||||
|
||||
// clean up any software installers
|
||||
mysql.ExecAdhocSQL(t, ts.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `DELETE FROM software_installers`)
|
||||
return err
|
||||
})
|
||||
|
||||
lbls, err := ts.ds.ListLabels(ctx, fleet.TeamFilter{}, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
for _, lbl := range lbls {
|
||||
|
|
@ -161,6 +155,12 @@ func (ts *withServer) commonTearDownTest(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Clean software installers in "No team" (the others are deleted in ts.ds.DeleteTeam above).
|
||||
mysql.ExecAdhocSQL(t, ts.ds, func(q sqlx.ExtContext) error {
|
||||
_, err := q.ExecContext(ctx, `DELETE FROM software_installers WHERE global_or_team_id = 0;`)
|
||||
return err
|
||||
})
|
||||
|
||||
globalPolicies, err := ts.ds.ListGlobalPolicies(ctx, fleet.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
if len(globalPolicies) > 0 {
|
||||
|
|
@ -279,6 +279,21 @@ func (ts *withServer) DoJSON(verb, path string, params interface{}, expectedStat
|
|||
}
|
||||
}
|
||||
|
||||
func (ts *withServer) DoJSONWithoutAuth(verb, path string, params interface{}, expectedStatusCode int, v interface{}, queryParams ...string) {
|
||||
t := ts.s.T()
|
||||
rawBytes, err := json.Marshal(params)
|
||||
require.NoError(t, err)
|
||||
resp := ts.DoRawWithHeaders(verb, path, rawBytes, expectedStatusCode, map[string]string{}, queryParams...)
|
||||
t.Cleanup(func() {
|
||||
resp.Body.Close()
|
||||
})
|
||||
err = json.NewDecoder(resp.Body).Decode(v)
|
||||
require.NoError(ts.s.T(), err)
|
||||
if e, ok := v.(errorer); ok {
|
||||
require.NoError(ts.s.T(), e.error())
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *withServer) getTestAdminToken() string {
|
||||
testUser := testUsers["admin1"]
|
||||
|
||||
|
|
@ -480,3 +495,24 @@ func (ts *withServer) lastActivityOfTypeMatches(name, details string, id uint) u
|
|||
t.Fatalf("no activity of type %s found in the last %d activities", name, len(listActivities.Activities))
|
||||
return 0
|
||||
}
|
||||
|
||||
func (ts *withServer) lastActivityOfTypeDoesNotMatch(name, details string, id uint) {
|
||||
t := ts.s.T()
|
||||
|
||||
var listActivities listActivitiesResponse
|
||||
ts.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK,
|
||||
&listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "10")
|
||||
require.True(t, len(listActivities.Activities) > 0)
|
||||
|
||||
for _, act := range listActivities.Activities {
|
||||
if act.Type == name {
|
||||
if details != "" {
|
||||
require.NotNil(t, act.Details)
|
||||
assert.NotEqual(t, details, string(*act.Details))
|
||||
}
|
||||
if id > 0 {
|
||||
assert.NotEqual(t, id, act.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,6 +210,12 @@ func WithPlatform(s string) NewHostOption {
|
|||
}
|
||||
}
|
||||
|
||||
func WithTeamID(teamID uint) NewHostOption {
|
||||
return func(h *fleet.Host) {
|
||||
h.TeamID = &teamID
|
||||
}
|
||||
}
|
||||
|
||||
func NewHost(tb testing.TB, ds fleet.Datastore, name, ip, key, uuid string, now time.Time, options ...NewHostOption) *fleet.Host {
|
||||
osqueryHostID, _ := server.GenerateRandomText(10)
|
||||
h := &fleet.Host{
|
||||
|
|
|
|||
|
|
@ -73,17 +73,28 @@ module.exports = {
|
|||
if(_.includes(sails.config.custom.bannedEmailDomainsForWebsiteSubmissions, emailDomain.toLowerCase())){
|
||||
throw 'invalidEmailDomain';
|
||||
}
|
||||
|
||||
// Set a default psychological stage and change reason.
|
||||
let psyStageAndChangeReason = {
|
||||
psychologicalStage: '4 - Has use case',
|
||||
psychologicalStageChangeReason: 'Website - Contact forms'
|
||||
};
|
||||
if(this.req.me){
|
||||
// If this user is logged in, check their current psychological stage, and if it is higher than 4, we won't set a psystage.
|
||||
// This way, if a user has a psytage >4, we won't regress their psystage because they submitted this form.
|
||||
if(['4 - Has use case', '5 - Personally confident', '6 - Has team buy-in'].includes(this.req.me.psychologicalStage)) {
|
||||
psyStageAndChangeReason = {};
|
||||
}
|
||||
}
|
||||
if(numberOfHosts >= 700){
|
||||
sails.helpers.salesforce.updateOrCreateContactAndAccountAndCreateLead.with({
|
||||
sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
|
||||
emailAddress: emailAddress,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
organization: organization,
|
||||
numberOfHosts: numberOfHosts,
|
||||
primaryBuyingSituation: primaryBuyingSituation === 'eo-security' ? 'Endpoint operations - Security' : primaryBuyingSituation === 'eo-it' ? 'Endpoint operations - IT' : primaryBuyingSituation === 'mdm' ? 'Device management (MDM)' : primaryBuyingSituation === 'vm' ? 'Vulnerability management' : undefined,
|
||||
contactSource: 'Website - Contact forms',
|
||||
leadDescription: `Submitted the "Talk to us" form and was taken to the Calendly page for the "Talk to us" event.`,
|
||||
description: `Submitted the "Talk to us" form and was taken to the Calendly page for the "Talk to us" event.`,
|
||||
...psyStageAndChangeReason// Only (potentially) set psystage and change reason for >700 hosts.
|
||||
}).exec((err)=>{
|
||||
if(err) {
|
||||
sails.log.warn(`Background task failed: When a user submitted the "Talk to us" form, a lead/contact could not be updated in the CRM for this email address: ${emailAddress}.`, err);
|
||||
|
|
|
|||
19
website/api/controllers/view-endpoint-ops.js
vendored
19
website/api/controllers/view-endpoint-ops.js
vendored
|
|
@ -22,13 +22,23 @@ module.exports = {
|
|||
}
|
||||
// Get testimonials for the <scrolalble-tweets> component.
|
||||
let testimonialsForScrollableTweets = _.clone(sails.config.builtStaticContent.testimonials);
|
||||
// Default the pagePersonalization to the user's primaryBuyingSituation.
|
||||
let pagePersonalization = this.req.session.primaryBuyingSituation;
|
||||
// If a pageMode query parameter is set, update the pagePersonalization value.
|
||||
// Note: This is the only page we're using this method instead of using the primaryBuyingSiutation value set in the users session.
|
||||
// This lets us link to the security and IT versions of the endpoint ops page from the unpersonalized homepage without changing the users primaryBuyingSituation.
|
||||
if(this.req.param('pageMode') === 'it'){
|
||||
pagePersonalization = 'eo-it';
|
||||
} else if(this.req.param('pageMode') === 'security'){
|
||||
pagePersonalization = 'eo-security';
|
||||
}
|
||||
|
||||
|
||||
// Specify an order for the testimonials on this page using the last names of quote authors
|
||||
let testimonialOrderForThisPage = ['Charles Zaffery','Dan Grzelak','Nico Waisman','Tom Larkin','Austin Anderson','Erik Gomez','Nick Fohs','Brendan Shaklovitz','Mike Arpaia','Andre Shields','Dhruv Majumdar','Ahmed Elshaer','Abubakar Yousafzai','Harrison Ravazzolo','Wes Whetstone','Kenny Botelho', 'Chandra Majumdar','Eric Tan', 'Alvaro Gutierrez', 'Joe Pistone'];
|
||||
if(['eo-it', 'mdm'].includes(this.req.session.primaryBuyingSituation)){
|
||||
testimonialOrderForThisPage = [ 'Harrison Ravazzolo', 'Eric Tan','Erik Gomez', 'Tom Larkin', 'Nick Fohs', 'Wes Whetstone', 'Mike Arpaia', 'Kenny Botelho', 'Alvaro Gutierrez'];
|
||||
} else if(['eo-security', 'vm'].includes(this.req.session.primaryBuyingSituation)){
|
||||
let testimonialOrderForThisPage = ['Charles Zaffery','Dan Grzelak','Nico Waisman','Tom Larkin','Austin Anderson','Erik Gomez','Nick Fohs','Brendan Shaklovitz','Mike Arpaia','Andre Shields','Dhruv Majumdar','Ahmed Elshaer','Abubakar Yousafzai','Wes Whetstone','Kenny Botelho', 'Chandra Majumdar','Eric Tan', 'Alvaro Gutierrez', 'Joe Pistone'];
|
||||
if(['eo-it', 'mdm'].includes(pagePersonalization)){
|
||||
testimonialOrderForThisPage = [ 'Eric Tan','Erik Gomez', 'Tom Larkin', 'Nick Fohs', 'Wes Whetstone', 'Mike Arpaia', 'Kenny Botelho', 'Alvaro Gutierrez'];
|
||||
} else if(['eo-security', 'vm'].includes(pagePersonalization)){
|
||||
testimonialOrderForThisPage = ['Nico Waisman','Charles Zaffery','Abubakar Yousafzai','Eric Tan','Mike Arpaia','Chandra Majumdar','Ahmed Elshaer','Brendan Shaklovitz','Austin Anderson','Dan Grzelak','Dhruv Majumdar','Alvaro Gutierrez', 'Joe Pistone'];
|
||||
}
|
||||
// Filter the testimonials by product category and the filtered list we built above.
|
||||
|
|
@ -48,6 +58,7 @@ module.exports = {
|
|||
// Respond with view.
|
||||
return {
|
||||
testimonialsForScrollableTweets,
|
||||
pagePersonalization,
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ module.exports = {
|
|||
numHostSoftwareInstalledPaths: {type: 'number', defaultsTo: 0},
|
||||
numSoftwareCPEs: {type: 'number', defaultsTo: 0},
|
||||
numSoftwareCVEs: {type: 'number', defaultsTo: 0},
|
||||
aiFeaturesDisabled: {type: 'boolean', defaultsTo: false },
|
||||
maintenanceWindowsEnabled: {type: 'boolean', defaultsTo: false },
|
||||
maintenanceWindowsConfigured: {type: 'boolean', defaultsTo: false },
|
||||
numHostsFleetDesktopEnabled: {type: 'number', defaultsTo: 0 },
|
||||
},
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ module.exports = {
|
|||
'6 - Has team buy-in'
|
||||
]
|
||||
},
|
||||
psychologicalStageChangeReason: {
|
||||
type: 'string',
|
||||
example: 'Website - Organic start flow'
|
||||
},
|
||||
// For new leads.
|
||||
leadDescription: {
|
||||
type: 'string',
|
||||
|
|
@ -58,7 +62,7 @@ module.exports = {
|
|||
|
||||
|
||||
|
||||
fn: async function ({emailAddress, linkedinUrl, firstName, lastName, organization, primaryBuyingSituation, psychologicalStage, contactSource, leadDescription, numberOfHosts}) {
|
||||
fn: async function ({emailAddress, linkedinUrl, firstName, lastName, organization, primaryBuyingSituation, psychologicalStage, psychologicalStageChangeReason, contactSource, leadDescription, numberOfHosts}) {
|
||||
if(sails.config.environment !== 'production') {
|
||||
sails.log('Skipping Salesforce integration...');
|
||||
return;
|
||||
|
|
@ -72,6 +76,7 @@ module.exports = {
|
|||
linkedinUrl,
|
||||
primaryBuyingSituation,
|
||||
psychologicalStage,
|
||||
psychologicalStageChangeReason,
|
||||
contactSource,
|
||||
description: leadDescription,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ module.exports = {
|
|||
numHostSoftwareInstalledPaths: {required: true, type: 'number'},
|
||||
numSoftwareCPEs: {required: true, type: 'number'},
|
||||
numSoftwareCVEs: {required: true, type: 'number'},
|
||||
aiFeaturesDisabled: {required: true, type: 'boolean'},
|
||||
maintenanceWindowsEnabled: {required: true, type: 'boolean'},
|
||||
maintenanceWindowsConfigured: {required: true, type: 'boolean'},
|
||||
numHostsFleetDesktopEnabled: {required: true, type: 'number'},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
|
|
|
|||
|
|
@ -406,6 +406,69 @@ module.exports = {
|
|||
}],
|
||||
tags: [`enabled:false`],
|
||||
});
|
||||
// aiFeaturesDisabled
|
||||
let numberOfInstancesWithAiFeaturesDisabled = _.where(latestStatisticsReportedByReleasedFleetVersions, {aiFeaturesDisabled: true}).length;
|
||||
let numberOfInstancesWithAiFeaturesEnabled = numberOfInstancesToReport - numberOfInstancesWithAiFeaturesDisabled;
|
||||
metricsToReport.push({
|
||||
metric: 'usage_statistics.ai_features',
|
||||
type: 3,
|
||||
points: [{
|
||||
timestamp: timestampForTheseMetrics,
|
||||
value: numberOfInstancesWithAiFeaturesEnabled
|
||||
}],
|
||||
tags: [`enabled:true`],
|
||||
});
|
||||
metricsToReport.push({
|
||||
metric: 'usage_statistics.ai_features',
|
||||
type: 3,
|
||||
points: [{
|
||||
timestamp: timestampForTheseMetrics,
|
||||
value: numberOfInstancesWithAiFeaturesDisabled
|
||||
}],
|
||||
tags: [`enabled:false`],
|
||||
});
|
||||
// maintenanceWindowsEnabled
|
||||
let numberOfInstancesWithMaintenanceWindowsEnabled = _.where(latestStatisticsReportedByReleasedFleetVersions, {maintenanceWindowsEnabled: true}).length;
|
||||
let numberOfInstancesWithMaintenanceWindowsDisabled = numberOfInstancesToReport - numberOfInstancesWithMaintenanceWindowsEnabled;
|
||||
metricsToReport.push({
|
||||
metric: 'usage_statistics.maintenance_windows',
|
||||
type: 3,
|
||||
points: [{
|
||||
timestamp: timestampForTheseMetrics,
|
||||
value: numberOfInstancesWithMaintenanceWindowsEnabled
|
||||
}],
|
||||
tags: [`enabled:true`],
|
||||
});
|
||||
metricsToReport.push({
|
||||
metric: 'usage_statistics.maintenance_windows',
|
||||
type: 3,
|
||||
points: [{
|
||||
timestamp: timestampForTheseMetrics,
|
||||
value: numberOfInstancesWithMaintenanceWindowsDisabled
|
||||
}],
|
||||
tags: [`enabled:false`],
|
||||
});
|
||||
// maintenanceWindowsConfigured
|
||||
let numberOfInstancesWithMaintenanceWindowsConfigured = _.where(latestStatisticsReportedByReleasedFleetVersions, {maintenanceWindowsEnabled: true}).length;
|
||||
let numberOfInstancesWithoutMaintenanceWindowsConfigured = numberOfInstancesToReport - numberOfInstancesWithMaintenanceWindowsEnabled;
|
||||
metricsToReport.push({
|
||||
metric: 'usage_statistics.maintenance_windows_configured',
|
||||
type: 3,
|
||||
points: [{
|
||||
timestamp: timestampForTheseMetrics,
|
||||
value: numberOfInstancesWithMaintenanceWindowsConfigured
|
||||
}],
|
||||
tags: [`configured:true`],
|
||||
});
|
||||
metricsToReport.push({
|
||||
metric: 'usage_statistics.maintenance_windows_configured',
|
||||
type: 3,
|
||||
points: [{
|
||||
timestamp: timestampForTheseMetrics,
|
||||
value: numberOfInstancesWithoutMaintenanceWindowsConfigured
|
||||
}],
|
||||
tags: [`configured:false`],
|
||||
});
|
||||
|
||||
// Create two metrics to track total number of hosts reported in the last week.
|
||||
let totalNumberOfHostsReportedByPremiumInstancesInTheLastWeek = _.sum(_.pluck(_.filter(latestStatisticsReportedByReleasedFleetVersions, {licenseTier: 'premium'}), 'numHostsEnrolled'));
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue