2021-10-11 14:37:48 +00:00
package service
import (
"context"
2023-02-08 23:20:23 +00:00
"crypto/x509"
"encoding/base64"
2022-06-10 21:52:24 +00:00
"errors"
2022-04-18 21:19:58 +00:00
"fmt"
2024-08-02 19:06:21 +00:00
"net/http"
2023-03-08 20:42:23 +00:00
"strconv"
2021-10-11 14:37:48 +00:00
"testing"
2021-11-09 14:35:36 +00:00
"time"
2021-10-11 14:37:48 +00:00
2021-12-14 21:34:11 +00:00
"github.com/WatchBeam/clock"
2021-10-11 14:37:48 +00:00
"github.com/fleetdm/fleet/v4/server/authz"
2023-02-08 23:20:23 +00:00
"github.com/fleetdm/fleet/v4/server/config"
2024-08-02 19:06:21 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/capabilities"
2023-10-06 22:04:33 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/license"
2021-12-14 21:34:11 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
2021-10-11 14:37:48 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2024-11-18 22:44:25 +00:00
"github.com/fleetdm/fleet/v4/server/mdm"
2023-02-08 23:20:23 +00:00
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
2023-03-17 21:52:30 +00:00
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
2024-02-26 15:26:00 +00:00
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
2021-10-11 14:37:48 +00:00
"github.com/fleetdm/fleet/v4/server/mock"
2021-12-14 21:34:11 +00:00
"github.com/fleetdm/fleet/v4/server/ptr"
2021-10-11 14:37:48 +00:00
"github.com/fleetdm/fleet/v4/server/test"
2024-08-02 19:06:21 +00:00
kitlog "github.com/go-kit/log"
2024-10-09 18:47:27 +00:00
"github.com/jmoiron/sqlx"
2024-09-10 19:52:17 +00:00
"github.com/smallstep/pkcs7"
2021-12-14 21:34:11 +00:00
"github.com/stretchr/testify/assert"
2021-10-11 14:37:48 +00:00
"github.com/stretchr/testify/require"
)
2025-04-08 14:35:06 +00:00
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
2021-12-14 21:34:11 +00:00
func TestHostDetails ( t * testing . T ) {
ds := new ( mock . Store )
svc := & Service { ds : ds }
host := & fleet . Host { ID : 3 }
expectedLabels := [ ] * fleet . Label {
{
Name : "foobar" ,
Description : "the foobar label" ,
} ,
}
2023-02-22 22:26:06 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
2021-12-14 21:34:11 +00:00
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return expectedLabels , nil
}
expectedPacks := [ ] * fleet . Pack {
{
Name : "pack1" ,
} ,
{
Name : "pack2" ,
} ,
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return expectedPacks , nil
}
2022-06-01 16:06:57 +00:00
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
2021-12-14 21:34:11 +00:00
return nil
}
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
2024-10-02 21:43:19 +00:00
dsBats := [ ] * fleet . HostBattery { { HostID : host . ID , SerialNumber : "a" , CycleCount : 999 , Health : "Normal" } , { HostID : host . ID , SerialNumber : "b" , CycleCount : 1001 , Health : "Service recommended" } }
2022-06-28 18:11:49 +00:00
ds . ListHostBatteriesFunc = func ( ctx context . Context , hostID uint ) ( [ ] * fleet . HostBattery , error ) {
2022-07-21 02:16:03 +00:00
return dsBats , nil
2022-06-28 18:11:49 +00:00
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820)
## Addresses full stack for #18554
- Add new `timezone` column to `calendar_events` table
- When fetched from Google's API, save calendar user's timezone in this
new column along with rest of event data
- Implement datastore method to retrieve the start time and timezone for
a host's next calendar event as a `HostMaintenanceWindow`
- Localize and add UTC offset to the `HostMaintenanceWindow`'s start
time according to its `timezone`
- Include the processed `HostMaintenanceWindow`, if present, in the
response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}`
endpoints
- Implement UI on the host details page to display this data
- Add new and update existing UI, core integration, datastore, and
`fleetctl` tests
- Update `date-fns` package to the latest version
<img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed">
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified tables for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2025-04-08 14:35:06 +00:00
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
2025-07-15 18:21:39 +00:00
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
2021-12-14 21:34:11 +00:00
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
opts := fleet . HostDetailOptions {
IncludeCVEScores : false ,
IncludePolicies : false ,
}
2022-11-15 14:08:05 +00:00
hostDetail , err := svc . getHostDetails ( test . UserContext ( context . Background ( ) , test . UserAdmin ) , host , opts )
2021-12-14 21:34:11 +00:00
require . NoError ( t , err )
assert . Equal ( t , expectedLabels , hostDetail . Labels )
assert . Equal ( t , expectedPacks , hostDetail . Packs )
2022-06-28 18:11:49 +00:00
require . NotNil ( t , hostDetail . Batteries )
2024-10-02 21:43:19 +00:00
assert . Equal ( t , dsBats , * hostDetail . Batteries )
2023-03-08 20:42:23 +00:00
require . Nil ( t , hostDetail . MDM . MacOSSettings )
}
2025-04-08 14:35:06 +00:00
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
2023-10-06 22:04:33 +00:00
func TestHostDetailsMDMAppleDiskEncryption ( t * testing . T ) {
2023-03-08 20:42:23 +00:00
ds := new ( mock . Store )
svc := & Service { ds : ds }
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { MDM : fleet . MDM { EnabledAndConfigured : true } } , nil
}
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return nil , nil
}
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
return nil
}
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , hostID uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820)
## Addresses full stack for #18554
- Add new `timezone` column to `calendar_events` table
- When fetched from Google's API, save calendar user's timezone in this
new column along with rest of event data
- Implement datastore method to retrieve the start time and timezone for
a host's next calendar event as a `HostMaintenanceWindow`
- Localize and add UTC offset to the `HostMaintenanceWindow`'s start
time according to its `timezone`
- Include the processed `HostMaintenanceWindow`, if present, in the
response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}`
endpoints
- Implement UI on the host details page to display this data
- Add new and update existing UI, core integration, datastore, and
`fleetctl` tests
- Update `date-fns` package to the latest version
<img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed">
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified tables for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2025-04-08 14:35:06 +00:00
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
2025-05-09 18:18:48 +00:00
ds . GetNanoMDMEnrollmentTimesFunc = func ( ctx context . Context , hostUUID string ) ( * time . Time , * time . Time , error ) {
return nil , nil , nil
}
2025-07-15 18:21:39 +00:00
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
2023-03-08 20:42:23 +00:00
cases := [ ] struct {
name string
rawDecrypt * int
fvProf * fleet . HostMDMAppleProfile
2023-04-24 21:27:15 +00:00
wantState fleet . DiskEncryptionStatus
2023-03-08 20:42:23 +00:00
wantAction fleet . ActionRequiredState
2023-11-07 21:03:03 +00:00
wantStatus * fleet . MDMDeliveryStatus
2023-03-08 20:42:23 +00:00
} {
2023-03-20 20:22:57 +00:00
{ "no profile" , ptr . Int ( - 1 ) , nil , "" , "" , nil } ,
2023-03-08 20:42:23 +00:00
{
"installed profile, no key" ,
ptr . Int ( - 1 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryVerifying ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionActionRequired ,
2024-07-31 19:59:30 +00:00
fleet . ActionRequiredRotateKey ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryPending ,
2023-03-08 20:42:23 +00:00
} ,
{
"installed profile, unknown decryptable" ,
nil ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryVerifying ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-03-08 20:42:23 +00:00
} ,
2024-02-29 17:19:17 +00:00
fleet . DiskEncryptionVerifying ,
2023-03-08 20:42:23 +00:00
"" ,
2024-02-29 17:19:17 +00:00
& fleet . MDMDeliveryVerifying ,
2023-03-08 20:42:23 +00:00
} ,
{
"installed profile, not decryptable" ,
ptr . Int ( 0 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryVerifying ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionActionRequired ,
fleet . ActionRequiredRotateKey ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryPending ,
2023-03-08 20:42:23 +00:00
} ,
{
"installed profile, decryptable" ,
ptr . Int ( 1 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryVerifying ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-03-08 20:42:23 +00:00
} ,
2023-04-24 21:27:15 +00:00
fleet . DiskEncryptionVerifying ,
2023-03-08 20:42:23 +00:00
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryVerifying ,
2023-03-08 20:42:23 +00:00
} ,
2023-06-05 17:05:28 +00:00
{
"installed profile, decryptable, verified" ,
ptr . Int ( 1 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryVerified ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-06-05 17:05:28 +00:00
} ,
fleet . DiskEncryptionVerified ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryVerified ,
2023-06-05 17:05:28 +00:00
} ,
2023-03-08 20:42:23 +00:00
{
"pending install, decryptable" ,
ptr . Int ( 1 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryPending ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionEnforcing ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryPending ,
2023-03-08 20:42:23 +00:00
} ,
{
"pending install, unknown decryptable" ,
nil ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryPending ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionEnforcing ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryPending ,
2023-03-08 20:42:23 +00:00
} ,
{
"pending install, no key" ,
ptr . Int ( - 1 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryPending ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionEnforcing ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryPending ,
2023-03-08 20:42:23 +00:00
} ,
{
"failed install, no key" ,
ptr . Int ( - 1 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryFailed ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-10-18 20:39:23 +00:00
Detail : "some mdm profile install error" ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionFailed ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryFailed ,
2023-03-08 20:42:23 +00:00
} ,
{
"failed install, not decryptable" ,
ptr . Int ( 0 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryFailed ,
OperationType : fleet . MDMOperationTypeInstall ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionFailed ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryFailed ,
2023-03-08 20:42:23 +00:00
} ,
{
"pending remove, decryptable" ,
ptr . Int ( 1 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryPending ,
OperationType : fleet . MDMOperationTypeRemove ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionRemovingEnforcement ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryPending ,
2023-03-08 20:42:23 +00:00
} ,
{
"pending remove, no key" ,
ptr . Int ( - 1 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryPending ,
OperationType : fleet . MDMOperationTypeRemove ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionRemovingEnforcement ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryPending ,
2023-03-08 20:42:23 +00:00
} ,
{
"failed remove, unknown decryptable" ,
nil ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryFailed ,
OperationType : fleet . MDMOperationTypeRemove ,
2023-10-18 20:39:23 +00:00
Detail : "some mdm profile removal error" ,
2023-03-08 20:42:23 +00:00
} ,
fleet . DiskEncryptionFailed ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryFailed ,
2023-03-08 20:42:23 +00:00
} ,
{
"removed profile, not decryptable" ,
ptr . Int ( 0 ) ,
& fleet . HostMDMAppleProfile {
HostUUID : "abc" ,
2023-03-17 21:52:30 +00:00
Identifier : mobileconfig . FleetFileVaultPayloadIdentifier ,
2023-11-07 21:03:03 +00:00
Status : & fleet . MDMDeliveryVerifying ,
OperationType : fleet . MDMOperationTypeRemove ,
2023-03-08 20:42:23 +00:00
} ,
"" ,
"" ,
2023-11-07 21:03:03 +00:00
& fleet . MDMDeliveryVerifying ,
2023-03-08 20:42:23 +00:00
} ,
}
for _ , c := range cases {
t . Run ( c . name , func ( t * testing . T ) {
var mdmData fleet . MDMHostData
rawDecrypt := "null"
if c . rawDecrypt != nil {
rawDecrypt = strconv . Itoa ( * c . rawDecrypt )
}
require . NoError ( t , mdmData . Scan ( [ ] byte ( fmt . Sprintf ( ` { "raw_decryptable": %s} ` , rawDecrypt ) ) ) )
2023-10-06 22:04:33 +00:00
host := & fleet . Host { ID : 3 , MDM : mdmData , UUID : "abc" , Platform : "darwin" }
2023-03-08 20:42:23 +00:00
opts := fleet . HostDetailOptions {
IncludeCVEScores : false ,
IncludePolicies : false ,
}
2023-11-20 20:34:57 +00:00
ds . GetHostMDMAppleProfilesFunc = func ( ctx context . Context , uuid string ) ( [ ] fleet . HostMDMAppleProfile , error ) {
2023-03-08 20:42:23 +00:00
if c . fvProf == nil {
return nil , nil
}
return [ ] fleet . HostMDMAppleProfile { * c . fvProf } , nil
}
hostDetail , err := svc . getHostDetails ( test . UserContext ( context . Background ( ) , test . UserAdmin ) , host , opts )
require . NoError ( t , err )
2023-10-06 22:04:33 +00:00
require . NotNil ( t , hostDetail . MDM . MacOSSettings )
2023-03-08 20:42:23 +00:00
2023-03-20 20:22:57 +00:00
if c . wantState == "" {
2023-03-08 20:42:23 +00:00
require . Nil ( t , hostDetail . MDM . MacOSSettings . DiskEncryption )
2023-10-06 22:04:33 +00:00
require . Nil ( t , hostDetail . MDM . OSSettings . DiskEncryption . Status )
2023-10-18 20:39:23 +00:00
require . Empty ( t , hostDetail . MDM . OSSettings . DiskEncryption . Detail )
2023-03-08 20:42:23 +00:00
} else {
require . NotNil ( t , hostDetail . MDM . MacOSSettings . DiskEncryption )
2023-03-20 20:22:57 +00:00
require . Equal ( t , c . wantState , * hostDetail . MDM . MacOSSettings . DiskEncryption )
2023-10-06 22:04:33 +00:00
require . NotNil ( t , hostDetail . MDM . OSSettings . DiskEncryption . Status )
require . Equal ( t , c . wantState , * hostDetail . MDM . OSSettings . DiskEncryption . Status )
2023-10-18 20:39:23 +00:00
require . Equal ( t , c . fvProf . Detail , hostDetail . MDM . OSSettings . DiskEncryption . Detail )
2023-03-08 20:42:23 +00:00
}
if c . wantAction == "" {
require . Nil ( t , hostDetail . MDM . MacOSSettings . ActionRequired )
} else {
require . NotNil ( t , hostDetail . MDM . MacOSSettings . ActionRequired )
require . Equal ( t , c . wantAction , * hostDetail . MDM . MacOSSettings . ActionRequired )
}
2023-03-20 20:22:57 +00:00
if c . wantStatus != nil {
require . NotNil ( t , hostDetail . MDM . Profiles )
profs := * hostDetail . MDM . Profiles
require . Equal ( t , c . wantStatus , profs [ 0 ] . Status )
2023-10-18 20:39:23 +00:00
require . Equal ( t , c . fvProf . Detail , profs [ 0 ] . Detail )
2023-03-20 20:22:57 +00:00
} else {
require . Nil ( t , * hostDetail . MDM . Profiles )
}
2023-03-08 20:42:23 +00:00
} )
}
2021-12-14 21:34:11 +00:00
}
2025-05-09 18:18:48 +00:00
func TestHostDetailsMDMTimestamps ( t * testing . T ) {
ds := new ( mock . Store )
svc := & Service { ds : ds }
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { MDM : fleet . MDM { EnabledAndConfigured : true , WindowsEnabledAndConfigured : true } } , nil
}
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return nil , nil
}
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
return nil
}
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , hostID uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
return & fleet . HostLockWipeStatus { } , nil
}
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
ds . GetHostMDMAppleProfilesFunc = func ( ctx context . Context , uuid string ) ( [ ] fleet . HostMDMAppleProfile , error ) {
return nil , nil
}
ds . GetHostMDMWindowsProfilesFunc = func ( ctx context . Context , uuid string ) ( [ ] fleet . HostMDMWindowsProfile , error ) {
return nil , nil
}
Implement BitLocker "action required" status (#31451)
for #31182
# Details
This PR implements the "Action Required" state for Windows host disk
encryption. This includes updates to reporting for:
* disk encryption summary (`GET /fleet/disk_encryption`)
* config profiles summary (`GET /configuration_profiles/summary`)
* config profile status ( `GET
/configuration_profiles/{profile_uuid}/status`)
For disk encryption summary, the statuses are now determined according
to [the rules in the
Figma](https://www.figma.com/design/XbhlPuEJxQtOgTZW9EOJZp/-28133-Enforce-BitLocker-PIN?node-id=5484-928&t=JB13g8zQ2QDVEmPB-0).
TL;DR if the criteria for "verified" or "verifying" are set, but a
required PIN is not set, we report a host as "action required".
For profiles, I followed what seems to be the existing pattern and set
the profile status to "pending" if the disk encryption status is "action
required". This is what we do for hosts with the "enforcing" or
"removing enforcement" statuses.
A lot of the changes in these files are due to the creation of the
`fleet.DiskEncryptionConfig` struct to hold info about disk encryption
config, and passing variables of that type to various functions instead
of passing a `bool` to indicate whether encryption is enabled. Other
than that, the functional changes are constrained to a few files.
> Note: to get the "require bitlocker pin" UI, compile the front end
with:
```
SHOW_BITLOCKER_PIN_OPTION=true NODE_ENV=development yarn run webpack --progress --watch
```
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
Changelog will be added when feature is complete.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
Could use some help testing this end-to-end. I was able to test the
banners showing up correctly, but testing the Disk Encryption table
requires some Windows-MDM-fu (I just get all zeroes).
## Database migrations
- [X] Checked table schema to confirm autoupdate
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [X] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
2025-08-05 16:23:27 +00:00
ds . GetConfigEnableDiskEncryptionFunc = func ( ctx context . Context , teamID * uint ) ( fleet . DiskEncryptionConfig , error ) {
return fleet . DiskEncryptionConfig { } , nil
2025-05-09 18:18:48 +00:00
}
2025-07-15 18:21:39 +00:00
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
2025-05-09 18:18:48 +00:00
ts1 := time . Now ( ) . Add ( - 1 * time . Hour ) . UTC ( )
ts2 := time . Now ( ) . Add ( - 2 * time . Hour ) . UTC ( )
ds . GetNanoMDMEnrollmentTimesFunc = func ( ctx context . Context , hostUUID string ) ( * time . Time , * time . Time , error ) {
return & ts1 , & ts2 , nil
}
cases := [ ] struct {
platform string
platformIsApple bool
} {
{ "darwin" , true } ,
{ "ios" , true } ,
{ "ipados" , true } ,
{ "windows" , false } ,
{ "ubuntu" , false } ,
{ "centos" , false } ,
{ "rhel" , false } ,
{ "debian" , false } ,
}
for _ , testcase := range cases {
t . Run ( "test MDM timestamps on platform " + testcase . platform , func ( t * testing . T ) {
ds . GetNanoMDMEnrollmentTimesFuncInvoked = false
host := & fleet . Host { ID : 3 , MDM : fleet . MDMHostData { } , Platform : testcase . platform , UUID : "abc123" }
opts := fleet . HostDetailOptions {
IncludeCVEScores : false ,
IncludePolicies : false ,
ExcludeSoftware : true ,
IncludeCriticalVulnerabilitiesCount : false ,
}
hostDetail , err := svc . getHostDetails ( test . UserContext ( context . Background ( ) , test . UserAdmin ) , host , opts )
require . NoError ( t , err )
if testcase . platformIsApple {
assert . True ( t , ds . GetNanoMDMEnrollmentTimesFuncInvoked )
2025-05-13 12:17:11 +00:00
require . NotNil ( t , hostDetail . LastMDMEnrolledAt )
assert . Equal ( t , * hostDetail . LastMDMEnrolledAt , ts1 )
require . NotNil ( t , hostDetail . LastMDMCheckedInAt )
assert . Equal ( t , * hostDetail . LastMDMCheckedInAt , ts2 )
2025-05-09 18:18:48 +00:00
} else {
assert . False ( t , ds . GetNanoMDMEnrollmentTimesFuncInvoked )
2025-05-13 12:17:11 +00:00
assert . Nil ( t , hostDetail . LastMDMEnrolledAt )
assert . Nil ( t , hostDetail . LastMDMCheckedInAt )
2025-05-09 18:18:48 +00:00
}
} )
}
}
2025-04-08 14:35:06 +00:00
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
2023-10-06 22:04:33 +00:00
func TestHostDetailsOSSettings ( t * testing . T ) {
ds := new ( mock . Store )
svc := & Service { ds : ds }
ctx := context . Background ( )
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return nil , nil
}
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
return nil
}
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , hostID uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820)
## Addresses full stack for #18554
- Add new `timezone` column to `calendar_events` table
- When fetched from Google's API, save calendar user's timezone in this
new column along with rest of event data
- Implement datastore method to retrieve the start time and timezone for
a host's next calendar event as a `HostMaintenanceWindow`
- Localize and add UTC offset to the `HostMaintenanceWindow`'s start
time according to its `timezone`
- Include the processed `HostMaintenanceWindow`, if present, in the
response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}`
endpoints
- Implement UI on the host details page to display this data
- Add new and update existing UI, core integration, datastore, and
`fleetctl` tests
- Update `date-fns` package to the latest version
<img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed">
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified tables for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
2023-10-06 22:04:33 +00:00
ds . GetHostMDMMacOSSetupFunc = func ( ctx context . Context , hid uint ) ( * fleet . HostMDMMacOSSetup , error ) {
return nil , nil
}
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2023-10-06 22:04:33 +00:00
2024-11-19 20:11:59 +00:00
ds . GetHostDiskEncryptionKeyFunc = func ( ctx context . Context , hostID uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey { } , nil
}
2025-07-02 19:57:25 +00:00
ds . GetHostArchivedDiskEncryptionKeyFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostArchivedDiskEncryptionKey , error ) {
return & fleet . HostArchivedDiskEncryptionKey { } , nil
}
2025-04-08 14:35:06 +00:00
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
2025-05-09 18:18:48 +00:00
ds . GetNanoMDMEnrollmentTimesFunc = func ( ctx context . Context , hostUUID string ) ( * time . Time , * time . Time , error ) {
return nil , nil , nil
}
2025-07-15 18:21:39 +00:00
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
2024-11-19 20:11:59 +00:00
2023-10-06 22:04:33 +00:00
type testCase struct {
name string
host * fleet . Host
licenseTier string
wantStatus fleet . DiskEncryptionStatus
}
cases := [ ] testCase {
{ "windows" , & fleet . Host { ID : 42 , Platform : "windows" } , fleet . TierPremium , fleet . DiskEncryptionEnforcing } ,
{ "darwin" , & fleet . Host { ID : 42 , Platform : "darwin" } , fleet . TierPremium , "" } ,
2024-12-17 17:43:35 +00:00
// TeamID necessary to check whether disk encryption is enabled for Linux hosts, in lieu of
// MDM-related logic which doesn't apply to Linux hosts
{ "ubuntu" , & fleet . Host { ID : 42 , Platform : "ubuntu" , TeamID : ptr . Uint ( 1 ) } , fleet . TierPremium , "" } ,
2023-10-06 22:04:33 +00:00
{ "not premium" , & fleet . Host { ID : 42 , Platform : "windows" } , fleet . TierFree , "" } ,
}
setupDS := func ( c testCase ) {
ds . AppConfigFuncInvoked = false
ds . GetMDMWindowsBitLockerStatusFuncInvoked = false
2023-11-20 20:34:57 +00:00
ds . GetHostMDMAppleProfilesFuncInvoked = false
ds . GetHostMDMWindowsProfilesFuncInvoked = false
2024-03-29 14:48:31 +00:00
ds . GetHostMDMFuncInvoked = false
2024-12-17 17:43:35 +00:00
ds . GetConfigEnableDiskEncryptionFuncInvoked = false
2023-10-06 22:04:33 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
2023-10-12 12:31:10 +00:00
return & fleet . AppConfig { MDM : fleet . MDM { EnabledAndConfigured : true , WindowsEnabledAndConfigured : true } } , nil
2023-10-06 22:04:33 +00:00
}
2023-10-18 20:39:23 +00:00
ds . GetMDMWindowsBitLockerStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostMDMDiskEncryption , error ) {
2023-10-06 22:04:33 +00:00
if c . wantStatus == "" {
return nil , nil
}
2023-10-18 20:39:23 +00:00
return & fleet . HostMDMDiskEncryption { Status : & c . wantStatus , Detail : "" } , nil
2023-10-06 22:04:33 +00:00
}
2023-11-20 20:34:57 +00:00
ds . GetHostMDMAppleProfilesFunc = func ( ctx context . Context , uuid string ) ( [ ] fleet . HostMDMAppleProfile , error ) {
return nil , nil
}
ds . GetHostMDMWindowsProfilesFunc = func ( ctx context . Context , uuid string ) ( [ ] fleet . HostMDMWindowsProfile , error ) {
2023-10-06 22:04:33 +00:00
return nil , nil
}
2024-03-29 14:48:31 +00:00
ds . GetHostMDMFunc = func ( ctx context . Context , hostID uint ) ( * fleet . HostMDM , error ) {
hmdm := fleet . HostMDM { Enrolled : true , IsServer : false }
return & hmdm , nil
}
Implement BitLocker "action required" status (#31451)
for #31182
# Details
This PR implements the "Action Required" state for Windows host disk
encryption. This includes updates to reporting for:
* disk encryption summary (`GET /fleet/disk_encryption`)
* config profiles summary (`GET /configuration_profiles/summary`)
* config profile status ( `GET
/configuration_profiles/{profile_uuid}/status`)
For disk encryption summary, the statuses are now determined according
to [the rules in the
Figma](https://www.figma.com/design/XbhlPuEJxQtOgTZW9EOJZp/-28133-Enforce-BitLocker-PIN?node-id=5484-928&t=JB13g8zQ2QDVEmPB-0).
TL;DR if the criteria for "verified" or "verifying" are set, but a
required PIN is not set, we report a host as "action required".
For profiles, I followed what seems to be the existing pattern and set
the profile status to "pending" if the disk encryption status is "action
required". This is what we do for hosts with the "enforcing" or
"removing enforcement" statuses.
A lot of the changes in these files are due to the creation of the
`fleet.DiskEncryptionConfig` struct to hold info about disk encryption
config, and passing variables of that type to various functions instead
of passing a `bool` to indicate whether encryption is enabled. Other
than that, the functional changes are constrained to a few files.
> Note: to get the "require bitlocker pin" UI, compile the front end
with:
```
SHOW_BITLOCKER_PIN_OPTION=true NODE_ENV=development yarn run webpack --progress --watch
```
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
Changelog will be added when feature is complete.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
Could use some help testing this end-to-end. I was able to test the
banners showing up correctly, but testing the Disk Encryption table
requires some Windows-MDM-fu (I just get all zeroes).
## Database migrations
- [X] Checked table schema to confirm autoupdate
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [X] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
2025-08-05 16:23:27 +00:00
ds . GetConfigEnableDiskEncryptionFunc = func ( ctx context . Context , teamID * uint ) ( fleet . DiskEncryptionConfig , error ) {
2024-12-17 17:43:35 +00:00
// testing API response when not enabled
Implement BitLocker "action required" status (#31451)
for #31182
# Details
This PR implements the "Action Required" state for Windows host disk
encryption. This includes updates to reporting for:
* disk encryption summary (`GET /fleet/disk_encryption`)
* config profiles summary (`GET /configuration_profiles/summary`)
* config profile status ( `GET
/configuration_profiles/{profile_uuid}/status`)
For disk encryption summary, the statuses are now determined according
to [the rules in the
Figma](https://www.figma.com/design/XbhlPuEJxQtOgTZW9EOJZp/-28133-Enforce-BitLocker-PIN?node-id=5484-928&t=JB13g8zQ2QDVEmPB-0).
TL;DR if the criteria for "verified" or "verifying" are set, but a
required PIN is not set, we report a host as "action required".
For profiles, I followed what seems to be the existing pattern and set
the profile status to "pending" if the disk encryption status is "action
required". This is what we do for hosts with the "enforcing" or
"removing enforcement" statuses.
A lot of the changes in these files are due to the creation of the
`fleet.DiskEncryptionConfig` struct to hold info about disk encryption
config, and passing variables of that type to various functions instead
of passing a `bool` to indicate whether encryption is enabled. Other
than that, the functional changes are constrained to a few files.
> Note: to get the "require bitlocker pin" UI, compile the front end
with:
```
SHOW_BITLOCKER_PIN_OPTION=true NODE_ENV=development yarn run webpack --progress --watch
```
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
Changelog will be added when feature is complete.
- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
## Testing
- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
Could use some help testing this end-to-end. I was able to test the
banners showing up correctly, but testing the Disk Encryption table
requires some Windows-MDM-fu (I just get all zeroes).
## Database migrations
- [X] Checked table schema to confirm autoupdate
- [X] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [X] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [X] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
2025-08-05 16:23:27 +00:00
return fleet . DiskEncryptionConfig { } , nil
2024-12-17 17:43:35 +00:00
}
2025-07-15 18:21:39 +00:00
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
2023-10-06 22:04:33 +00:00
}
for _ , c := range cases {
t . Run ( c . name , func ( t * testing . T ) {
setupDS ( c )
ctx = license . NewContext ( ctx , & fleet . LicenseInfo { Tier : c . licenseTier } )
hostDetail , err := svc . getHostDetails ( test . UserContext ( ctx , test . UserAdmin ) , c . host , fleet . HostDetailOptions {
IncludeCVEScores : false ,
IncludePolicies : false ,
} )
require . NoError ( t , err )
require . NotNil ( t , hostDetail )
require . True ( t , ds . AppConfigFuncInvoked )
switch c . host . Platform {
case "windows" :
2023-11-20 20:34:57 +00:00
require . False ( t , ds . GetHostMDMAppleProfilesFuncInvoked )
2024-03-29 14:48:31 +00:00
if c . licenseTier == fleet . TierPremium {
require . True ( t , ds . GetHostMDMFuncInvoked )
} else {
require . False ( t , ds . GetHostMDMFuncInvoked )
}
2023-10-06 22:04:33 +00:00
if c . wantStatus != "" {
require . True ( t , ds . GetMDMWindowsBitLockerStatusFuncInvoked )
require . NotNil ( t , hostDetail . MDM . OSSettings . DiskEncryption . Status )
require . Equal ( t , c . wantStatus , * hostDetail . MDM . OSSettings . DiskEncryption . Status )
} else {
require . False ( t , ds . GetMDMWindowsBitLockerStatusFuncInvoked )
require . Nil ( t , hostDetail . MDM . OSSettings . DiskEncryption . Status )
}
2024-12-17 17:43:35 +00:00
case "ubuntu" :
require . False ( t , ds . GetHostMDMAppleProfilesFuncInvoked )
require . False ( t , ds . GetMDMWindowsBitLockerStatusFuncInvoked )
// service should call this function to check whether disk encryption is enabled for a Linux host
require . True ( t , ds . GetConfigEnableDiskEncryptionFuncInvoked )
// `hostDetail.MDM.OSSettings` and `hostDetail.MDM.OSSettings.DiskEncryption` will actually not
// be `nil` here due to the way those fields are initialized by `svc.ds.Host`, so we can't
// expect them to be `nil` in these tests. However, since the relevant struct tags are set to
// `omitempty`, the resulting API response WILL omit these fields/subfields when empty,
// which is confirmed at the integration layer.
require . Nil ( t , hostDetail . MDM . OSSettings . DiskEncryption . Status )
2023-10-06 22:04:33 +00:00
case "darwin" :
2023-11-20 20:34:57 +00:00
require . True ( t , ds . GetHostMDMAppleProfilesFuncInvoked )
2023-10-06 22:04:33 +00:00
require . False ( t , ds . GetMDMWindowsBitLockerStatusFuncInvoked )
require . Nil ( t , hostDetail . MDM . OSSettings . DiskEncryption . Status )
default :
2023-11-20 20:34:57 +00:00
require . False ( t , ds . GetHostMDMAppleProfilesFuncInvoked )
2023-10-06 22:04:33 +00:00
require . False ( t , ds . GetMDMWindowsBitLockerStatusFuncInvoked )
}
} )
}
}
2025-04-08 14:35:06 +00:00
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
2023-10-12 12:31:10 +00:00
func TestHostDetailsOSSettingsWindowsOnly ( t * testing . T ) {
ds := new ( mock . Store )
svc := & Service { ds : ds }
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return nil , nil
}
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
return nil
}
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , hostID uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820)
## Addresses full stack for #18554
- Add new `timezone` column to `calendar_events` table
- When fetched from Google's API, save calendar user's timezone in this
new column along with rest of event data
- Implement datastore method to retrieve the start time and timezone for
a host's next calendar event as a `HostMaintenanceWindow`
- Localize and add UTC offset to the `HostMaintenanceWindow`'s start
time according to its `timezone`
- Include the processed `HostMaintenanceWindow`, if present, in the
response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}`
endpoints
- Implement UI on the host details page to display this data
- Add new and update existing UI, core integration, datastore, and
`fleetctl` tests
- Update `date-fns` package to the latest version
<img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed">
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified tables for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
2023-10-12 12:31:10 +00:00
ds . GetHostMDMMacOSSetupFunc = func ( ctx context . Context , hid uint ) ( * fleet . HostMDMMacOSSetup , error ) {
return nil , nil
}
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { MDM : fleet . MDM { WindowsEnabledAndConfigured : true } } , nil
}
2023-10-18 20:39:23 +00:00
ds . GetMDMWindowsBitLockerStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostMDMDiskEncryption , error ) {
2023-10-12 12:31:10 +00:00
verified := fleet . DiskEncryptionVerified
2023-10-18 20:39:23 +00:00
return & fleet . HostMDMDiskEncryption { Status : & verified , Detail : "" } , nil
2023-10-12 12:31:10 +00:00
}
2023-11-20 20:34:57 +00:00
ds . GetHostMDMAppleProfilesFunc = func ( ctx context . Context , uuid string ) ( [ ] fleet . HostMDMAppleProfile , error ) {
return nil , nil
}
ds . GetHostMDMWindowsProfilesFunc = func ( ctx context . Context , uuid string ) ( [ ] fleet . HostMDMWindowsProfile , error ) {
2023-10-12 12:31:10 +00:00
return nil , nil
}
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2024-03-29 14:48:31 +00:00
ds . GetHostMDMFunc = func ( ctx context . Context , hostID uint ) ( * fleet . HostMDM , error ) {
hmdm := fleet . HostMDM { Enrolled : true , IsServer : false }
return & hmdm , nil
}
2025-04-08 14:35:06 +00:00
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
2025-07-15 18:21:39 +00:00
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
2023-10-12 12:31:10 +00:00
ctx := license . NewContext ( context . Background ( ) , & fleet . LicenseInfo { Tier : fleet . TierPremium } )
hostDetail , err := svc . getHostDetails ( test . UserContext ( ctx , test . UserAdmin ) , & fleet . Host { ID : 42 , Platform : "windows" } , fleet . HostDetailOptions {
IncludeCVEScores : false ,
IncludePolicies : false ,
} )
require . NoError ( t , err )
require . NotNil ( t , hostDetail )
require . True ( t , ds . AppConfigFuncInvoked )
2023-11-20 20:34:57 +00:00
require . False ( t , ds . GetHostMDMAppleProfilesFuncInvoked )
2023-10-12 12:31:10 +00:00
require . True ( t , ds . GetMDMWindowsBitLockerStatusFuncInvoked )
require . NotNil ( t , hostDetail . MDM . OSSettings . DiskEncryption . Status )
require . Equal ( t , fleet . DiskEncryptionVerified , * hostDetail . MDM . OSSettings . DiskEncryption . Status )
}
2025-04-08 14:35:06 +00:00
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
2021-12-14 21:34:11 +00:00
func TestHostAuth ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-12-14 21:34:11 +00:00
teamHost := & fleet . Host { TeamID : ptr . Uint ( 1 ) }
globalHost := & fleet . Host { }
2023-02-22 22:26:06 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
2021-12-14 21:34:11 +00:00
ds . DeleteHostFunc = func ( ctx context . Context , hid uint ) error {
return nil
}
2022-01-18 01:52:09 +00:00
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
if id == 1 {
return teamHost , nil
}
return globalHost , nil
}
2022-05-25 16:30:03 +00:00
ds . HostFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
2021-12-14 21:34:11 +00:00
if id == 1 {
return teamHost , nil
}
return globalHost , nil
}
ds . HostByIdentifierFunc = func ( ctx context . Context , identifier string ) ( * fleet . Host , error ) {
if identifier == "1" {
return teamHost , nil
}
return globalHost , nil
}
ds . ListHostsFunc = func ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
return nil , nil
}
2022-06-01 16:06:57 +00:00
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
2021-12-14 21:34:11 +00:00
return nil
}
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( packs [ ] * fleet . Pack , err error ) {
return nil , nil
}
2025-07-17 14:20:49 +00:00
ds . AddHostsToTeamFunc = func ( ctx context . Context , params * fleet . AddHostsToTeamParams ) error {
2021-12-14 21:34:11 +00:00
return nil
}
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
2022-06-28 18:11:49 +00:00
ds . ListHostBatteriesFunc = func ( ctx context . Context , hostID uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820)
## Addresses full stack for #18554
- Add new `timezone` column to `calendar_events` table
- When fetched from Google's API, save calendar user's timezone in this
new column along with rest of event data
- Implement datastore method to retrieve the start time and timezone for
a host's next calendar event as a `HostMaintenanceWindow`
- Localize and add UTC offset to the `HostMaintenanceWindow`'s start
time according to its `timezone`
- Include the processed `HostMaintenanceWindow`, if present, in the
response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}`
endpoints
- Implement UI on the host details page to display this data
- Add new and update existing UI, core integration, datastore, and
`fleetctl` tests
- Update `date-fns` package to the latest version
<img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed">
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified tables for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
2021-12-14 21:34:11 +00:00
ds . DeleteHostsFunc = func ( ctx context . Context , ids [ ] uint ) error {
return nil
}
2022-01-18 01:52:09 +00:00
ds . UpdateHostRefetchRequestedFunc = func ( ctx context . Context , id uint , value bool ) error {
if id == 1 {
teamHost . RefetchRequested = true
} else {
globalHost . RefetchRequested = true
}
return nil
}
2024-08-30 21:00:35 +00:00
ds . BulkSetPendingMDMHostProfilesFunc = func ( ctx context . Context , hids , tids [ ] uint , puuids , uuids [ ] string ,
) ( updates fleet . MDMProfilesUpdates , err error ) {
return fleet . MDMProfilesUpdates { } , nil
2023-03-27 18:43:01 +00:00
}
2023-05-15 18:06:09 +00:00
ds . ListMDMAppleDEPSerialsInHostIDsFunc = func ( ctx context . Context , hids [ ] uint ) ( [ ] string , error ) {
return nil , nil
}
2023-06-14 12:15:05 +00:00
ds . TeamFunc = func ( ctx context . Context , id uint ) ( * fleet . Team , error ) {
return & fleet . Team { ID : id } , nil
}
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func ( ctx context . Context , u * fleet . User , a fleet . ActivityDetails , details [ ] byte , createdAt time . Time ) error {
2023-06-14 12:15:05 +00:00
return nil
}
ds . ListHostsLiteByIDsFunc = func ( ctx context . Context , ids [ ] uint ) ( [ ] * fleet . Host , error ) {
return nil , nil
}
2023-12-21 17:21:39 +00:00
ds . SetOrUpdateCustomHostDeviceMappingFunc = func ( ctx context . Context , hostID uint , email , source string ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
2025-02-11 19:53:11 +00:00
ds . ListHostUpcomingActivitiesFunc = func ( ctx context . Context , hostID uint , opt fleet . ListOptions ) ( [ ] * fleet . UpcomingActivity , * fleet . PaginationMetadata , error ) {
2024-01-29 14:37:54 +00:00
return nil , nil , nil
}
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2024-05-27 14:53:41 +00:00
ds . ListHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , opts fleet . HostSoftwareTitleListOptions ) ( [ ] * fleet . HostSoftwareWithInstaller , * fleet . PaginationMetadata , error ) {
2024-05-01 18:37:52 +00:00
return nil , nil , nil
}
2024-10-28 19:48:54 +00:00
ds . IsHostConnectedToFleetMDMFunc = func ( ctx context . Context , host * fleet . Host ) ( bool , error ) {
return true , nil
}
2025-02-24 17:52:39 +00:00
ds . ListHostCertificatesFunc = func ( ctx context . Context , hostID uint , opts fleet . ListOptions ) ( [ ] * fleet . HostCertificateRecord , * fleet . PaginationMetadata , error ) {
return nil , nil , nil
}
2025-04-08 14:35:06 +00:00
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
2021-12-14 21:34:11 +00:00
2025-05-02 15:41:26 +00:00
ds . GetCategoriesForSoftwareTitlesFunc = func ( ctx context . Context , softwareTitleIDs [ ] uint , team_id * uint ) ( map [ uint ] [ ] string , error ) {
return map [ uint ] [ ] string { } , nil
}
2025-05-20 20:41:38 +00:00
ds . UpdateHostIssuesFailingPoliciesFunc = func ( ctx context . Context , hostIDs [ ] uint ) error {
return nil
}
2025-08-22 17:36:03 +00:00
ds . UpdateHostIssuesFailingPoliciesForSingleHostFunc = func ( ctx context . Context , hostID uint ) error {
return nil
}
2025-05-20 20:41:38 +00:00
ds . GetHostIssuesLastUpdatedFunc = func ( ctx context . Context , hostId uint ) ( time . Time , error ) {
return time . Time { } , nil
}
2025-07-15 18:21:39 +00:00
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
2025-05-02 15:41:26 +00:00
2021-12-14 21:34:11 +00:00
testCases := [ ] struct {
name string
user * fleet . User
shouldFailGlobalWrite bool
shouldFailGlobalRead bool
shouldFailTeamWrite bool
shouldFailTeamRead bool
} {
{
"global admin" ,
& fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } ,
false ,
false ,
false ,
false ,
} ,
{
"global maintainer" ,
& fleet . User { GlobalRole : ptr . String ( fleet . RoleMaintainer ) } ,
false ,
false ,
false ,
false ,
} ,
{
"global observer" ,
& fleet . User { GlobalRole : ptr . String ( fleet . RoleObserver ) } ,
true ,
false ,
true ,
false ,
} ,
2023-12-21 17:21:39 +00:00
{
"team admin, belongs to team" ,
& fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 1 } , Role : fleet . RoleAdmin } } } ,
true ,
true ,
false ,
false ,
} ,
2021-12-14 21:34:11 +00:00
{
"team maintainer, belongs to team" ,
& fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 1 } , Role : fleet . RoleMaintainer } } } ,
true ,
true ,
false ,
false ,
} ,
{
"team observer, belongs to team" ,
& fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 1 } , Role : fleet . RoleObserver } } } ,
true ,
true ,
true ,
false ,
} ,
2023-12-21 17:21:39 +00:00
{
"team admin, DOES NOT belong to team" ,
& fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 2 } , Role : fleet . RoleAdmin } } } ,
true ,
true ,
true ,
true ,
} ,
2021-12-14 21:34:11 +00:00
{
"team maintainer, DOES NOT belong to team" ,
& fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 2 } , Role : fleet . RoleMaintainer } } } ,
true ,
true ,
true ,
true ,
} ,
{
"team observer, DOES NOT belong to team" ,
& fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 2 } , Role : fleet . RoleObserver } } } ,
true ,
true ,
true ,
true ,
} ,
}
for _ , tt := range testCases {
t . Run ( tt . name , func ( t * testing . T ) {
2022-11-15 14:08:05 +00:00
ctx := viewer . NewContext ( ctx , viewer . Viewer { User : tt . user } )
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
opts := fleet . HostDetailOptions {
IncludeCVEScores : false ,
IncludePolicies : false ,
}
2021-12-14 21:34:11 +00:00
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
_ , err := svc . GetHost ( ctx , 1 , opts )
2021-12-14 21:34:11 +00:00
checkAuthErr ( t , tt . shouldFailTeamRead , err )
2023-12-11 22:33:31 +00:00
_ , err = svc . GetHostLite ( ctx , 1 )
checkAuthErr ( t , tt . shouldFailTeamRead , err )
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
_ , err = svc . HostByIdentifier ( ctx , "1" , opts )
2021-12-14 21:34:11 +00:00
checkAuthErr ( t , tt . shouldFailTeamRead , err )
2024-01-29 14:37:54 +00:00
_ , _ , err = svc . ListHostUpcomingActivities ( ctx , 1 , fleet . ListOptions { } )
checkAuthErr ( t , tt . shouldFailTeamRead , err )
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
_ , err = svc . GetHost ( ctx , 2 , opts )
2021-12-14 21:34:11 +00:00
checkAuthErr ( t , tt . shouldFailGlobalRead , err )
2023-12-11 22:33:31 +00:00
_ , err = svc . GetHostLite ( ctx , 2 )
checkAuthErr ( t , tt . shouldFailGlobalRead , err )
only include policies in device endpoints for premium users (#6077)
This removes policy information from `GET /api/_version_/fleet/device/{token}` from non-premium Fleet instances.
Starting the server with `./build/fleet serve --dev --dev_license`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
[
{
"id": 3,
"name": "Antivirus healthy (Linux)",
"query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;",
"description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.",
"author_id": 1,
"author_name": "Roberto",
"author_email": "test@example.com",
"team_id": null,
"resolution": "Ensure ClamAV and Freshclam are installed and running.",
"platform": "darwin,linux",
"created_at": "2022-05-23T20:53:36Z",
"updated_at": "2022-06-03T13:17:42Z",
"response": ""
}
]
```
Starting the server with `./build/fleet serve --dev`
```bash
$ curl -s https://localhost:8080/api/latest/fleet/device/1804e808-171f-4dda-9bec-f695b2f2371a | jq '.host.policies // "not present"'
"not present"
```
2022-06-07 16:27:13 +00:00
_ , err = svc . HostByIdentifier ( ctx , "2" , opts )
2021-12-14 21:34:11 +00:00
checkAuthErr ( t , tt . shouldFailGlobalRead , err )
2024-01-29 14:37:54 +00:00
_ , _ , err = svc . ListHostUpcomingActivities ( ctx , 2 , fleet . ListOptions { } )
checkAuthErr ( t , tt . shouldFailGlobalRead , err )
2021-12-14 21:34:11 +00:00
err = svc . DeleteHost ( ctx , 1 )
checkAuthErr ( t , tt . shouldFailTeamWrite , err )
err = svc . DeleteHost ( ctx , 2 )
checkAuthErr ( t , tt . shouldFailGlobalWrite , err )
2024-03-11 16:02:51 +00:00
err = svc . DeleteHosts ( ctx , [ ] uint { 1 } , nil )
2021-12-14 21:34:11 +00:00
checkAuthErr ( t , tt . shouldFailTeamWrite , err )
2024-03-11 16:02:51 +00:00
err = svc . DeleteHosts ( ctx , [ ] uint { 2 } , nil )
2021-12-14 21:34:11 +00:00
checkAuthErr ( t , tt . shouldFailGlobalWrite , err )
2023-10-13 11:49:11 +00:00
err = svc . AddHostsToTeam ( ctx , ptr . Uint ( 1 ) , [ ] uint { 1 } , false )
2021-12-14 21:34:11 +00:00
checkAuthErr ( t , tt . shouldFailTeamWrite , err )
2024-03-11 16:02:51 +00:00
emptyFilter := make ( map [ string ] interface { } )
err = svc . AddHostsToTeamByFilter ( ctx , ptr . Uint ( 1 ) , & emptyFilter )
2021-12-14 21:34:11 +00:00
checkAuthErr ( t , tt . shouldFailTeamWrite , err )
err = svc . RefetchHost ( ctx , 1 )
checkAuthErr ( t , tt . shouldFailTeamRead , err )
2023-12-21 17:21:39 +00:00
_ , err = svc . SetCustomHostDeviceMapping ( ctx , 1 , "a@b.c" )
checkAuthErr ( t , tt . shouldFailTeamWrite , err )
_ , err = svc . SetCustomHostDeviceMapping ( ctx , 2 , "a@b.c" )
checkAuthErr ( t , tt . shouldFailGlobalWrite , err )
2024-05-01 18:37:52 +00:00
2024-05-27 14:53:41 +00:00
_ , _ , err = svc . ListHostSoftware ( ctx , 1 , fleet . HostSoftwareTitleListOptions { } )
2024-05-01 18:37:52 +00:00
checkAuthErr ( t , tt . shouldFailTeamRead , err )
2024-05-27 14:53:41 +00:00
_ , _ , err = svc . ListHostSoftware ( ctx , 2 , fleet . HostSoftwareTitleListOptions { } )
2024-05-01 18:37:52 +00:00
checkAuthErr ( t , tt . shouldFailGlobalRead , err )
2025-02-24 17:52:39 +00:00
_ , _ , err = svc . ListHostCertificates ( ctx , 1 , fleet . ListOptions { } )
checkAuthErr ( t , tt . shouldFailTeamRead , err )
_ , _ , err = svc . ListHostCertificates ( ctx , 2 , fleet . ListOptions { } )
checkAuthErr ( t , tt . shouldFailGlobalRead , err )
2021-12-14 21:34:11 +00:00
} )
}
2022-05-10 15:29:17 +00:00
// List, GetHostSummary work for all
2021-12-14 21:34:11 +00:00
}
2022-01-18 01:52:09 +00:00
2021-10-11 14:37:48 +00:00
func TestListHosts ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-10-11 14:37:48 +00:00
ds . ListHostsFunc = func ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
return [ ] * fleet . Host {
{ ID : 1 } ,
} , nil
}
2024-11-14 17:09:51 +00:00
userContext := test . UserContext ( ctx , test . UserAdmin )
hosts , err := svc . ListHosts ( userContext , fleet . HostListOptions { } )
2021-10-11 14:37:48 +00:00
require . NoError ( t , err )
require . Len ( t , hosts , 1 )
// a user is required
2022-11-15 14:08:05 +00:00
_ , err = svc . ListHosts ( ctx , fleet . HostListOptions { } )
2021-10-11 14:37:48 +00:00
require . Error ( t , err )
require . Contains ( t , err . Error ( ) , authz . ForbiddenErrorMessage )
2024-11-14 17:09:51 +00:00
var shouldIncludeCVEScores bool
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
require . Equal ( t , shouldIncludeCVEScores , includeCVEScores )
return nil
}
// free license disallows getting vuln details
hosts , err = svc . ListHosts ( userContext , fleet . HostListOptions { PopulateSoftware : true , PopulateSoftwareVulnerabilityDetails : true } )
require . NoError ( t , err )
require . Len ( t , hosts , 1 )
require . True ( t , ds . LoadHostSoftwareFuncInvoked )
ds . LoadHostSoftwareFuncInvoked = false
// you're allowed to skip vuln details on Premium
userContext = license . NewContext ( userContext , & fleet . LicenseInfo { Tier : fleet . TierPremium } )
hosts , err = svc . ListHosts ( userContext , fleet . HostListOptions { PopulateSoftware : true , PopulateSoftwareVulnerabilityDetails : false } )
require . NoError ( t , err )
require . Len ( t , hosts , 1 )
require . True ( t , ds . LoadHostSoftwareFuncInvoked )
ds . LoadHostSoftwareFuncInvoked = false
// you're allowed to retrieve vuln details on Premium
shouldIncludeCVEScores = true
hosts , err = svc . ListHosts ( userContext , fleet . HostListOptions { PopulateSoftware : true , PopulateSoftwareVulnerabilityDetails : true } )
require . NoError ( t , err )
require . Len ( t , hosts , 1 )
require . True ( t , ds . LoadHostSoftwareFuncInvoked )
2021-10-11 14:37:48 +00:00
}
2021-11-09 14:35:36 +00:00
func TestGetHostSummary ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-11-09 14:35:36 +00:00
2022-09-21 19:56:17 +00:00
ds . GenerateHostStatusStatisticsFunc = func ( ctx context . Context , filter fleet . TeamFilter , now time . Time , platform * string , lowDiskSpace * int ) ( * fleet . HostSummary , error ) {
2021-11-09 14:35:36 +00:00
return & fleet . HostSummary {
OnlineCount : 1 ,
2022-05-23 19:11:02 +00:00
OfflineCount : 5 , // offline hosts also includes mia hosts as of Fleet 4.15
2021-11-09 14:35:36 +00:00
MIACount : 3 ,
NewCount : 4 ,
TotalsHostsCount : 5 ,
2022-05-10 15:32:55 +00:00
Platforms : [ ] * fleet . HostSummaryPlatform { { Platform : "darwin" , HostsCount : 1 } , { Platform : "debian" , HostsCount : 2 } , { Platform : "centos" , HostsCount : 3 } , { Platform : "ubuntu" , HostsCount : 4 } } ,
2021-11-09 14:35:36 +00:00
} , nil
}
2022-05-10 15:32:55 +00:00
ds . LabelsSummaryFunc = func ( ctx context . Context ) ( [ ] * fleet . LabelSummary , error ) {
return [ ] * fleet . LabelSummary { { ID : 1 , Name : "All hosts" , Description : "All hosts enrolled in Fleet" , LabelType : fleet . LabelTypeBuiltIn } , { ID : 10 , Name : "Other label" , Description : "Not a builtin label" , LabelType : fleet . LabelTypeRegular } } , nil
}
2021-11-09 14:35:36 +00:00
2022-11-15 14:08:05 +00:00
summary , err := svc . GetHostSummary ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil )
2021-11-09 14:35:36 +00:00
require . NoError ( t , err )
2021-11-15 14:56:13 +00:00
require . Nil ( t , summary . TeamID )
2021-11-09 14:35:36 +00:00
require . Equal ( t , uint ( 1 ) , summary . OnlineCount )
2022-05-23 19:11:02 +00:00
require . Equal ( t , uint ( 5 ) , summary . OfflineCount )
2021-11-09 14:35:36 +00:00
require . Equal ( t , uint ( 3 ) , summary . MIACount )
require . Equal ( t , uint ( 4 ) , summary . NewCount )
require . Equal ( t , uint ( 5 ) , summary . TotalsHostsCount )
2022-05-10 15:32:55 +00:00
require . Len ( t , summary . Platforms , 4 )
require . Equal ( t , uint ( 9 ) , summary . AllLinuxCount )
2022-09-21 19:56:17 +00:00
require . Nil ( t , summary . LowDiskSpaceCount )
2022-05-10 15:32:55 +00:00
require . Len ( t , summary . BuiltinLabels , 1 )
require . Equal ( t , "All hosts" , summary . BuiltinLabels [ 0 ] . Name )
2021-11-09 14:35:36 +00:00
// a user is required
2022-11-15 14:08:05 +00:00
_ , err = svc . GetHostSummary ( ctx , nil , nil , nil )
2021-11-09 14:35:36 +00:00
require . Error ( t , err )
require . Contains ( t , err . Error ( ) , authz . ForbiddenErrorMessage )
}
2021-12-14 21:34:11 +00:00
func TestDeleteHost ( t * testing . T ) {
ds := mysql . CreateMySQLDS ( t )
defer ds . Close ( )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-12-14 21:34:11 +00:00
mockClock := clock . NewMockClock ( )
host := test . NewHost ( t , ds , "foo" , "192.168.1.10" , "1" , "1" , mockClock . Now ( ) )
assert . NotZero ( t , host . ID )
2022-11-15 14:08:05 +00:00
err := svc . DeleteHost ( test . UserContext ( ctx , test . UserAdmin ) , host . ID )
2021-12-14 21:34:11 +00:00
assert . Nil ( t , err )
filter := fleet . TeamFilter { User : test . UserAdmin }
2022-11-15 14:08:05 +00:00
hosts , err := ds . ListHosts ( ctx , filter , fleet . HostListOptions { } )
2021-12-14 21:34:11 +00:00
assert . Nil ( t , err )
assert . Len ( t , hosts , 0 )
}
func TestAddHostsToTeamByFilter ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-12-14 21:34:11 +00:00
expectedHostIDs := [ ] uint { 1 , 2 , 4 }
expectedTeam := ( * uint ) ( nil )
2024-05-24 16:25:27 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
2021-12-14 21:34:11 +00:00
ds . ListHostsFunc = func ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
var hosts [ ] * fleet . Host
for _ , id := range expectedHostIDs {
hosts = append ( hosts , & fleet . Host { ID : id } )
}
return hosts , nil
}
2025-07-17 14:20:49 +00:00
ds . AddHostsToTeamFunc = func ( ctx context . Context , params * fleet . AddHostsToTeamParams ) error {
assert . Equal ( t , expectedTeam , params . TeamID )
assert . Equal ( t , expectedHostIDs , params . HostIDs )
2021-12-14 21:34:11 +00:00
return nil
}
2024-08-30 21:00:35 +00:00
ds . BulkSetPendingMDMHostProfilesFunc = func ( ctx context . Context , hids , tids [ ] uint , puuids , uuids [ ] string ,
) ( updates fleet . MDMProfilesUpdates , err error ) {
return fleet . MDMProfilesUpdates { } , nil
2023-03-27 18:43:01 +00:00
}
2023-05-15 18:06:09 +00:00
ds . ListMDMAppleDEPSerialsInHostIDsFunc = func ( ctx context . Context , hids [ ] uint ) ( [ ] string , error ) {
return nil , nil
}
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2023-06-14 12:15:05 +00:00
return nil
}
2021-12-14 21:34:11 +00:00
2024-03-11 16:02:51 +00:00
emptyRequest := & map [ string ] interface { } { }
require . NoError ( t , svc . AddHostsToTeamByFilter ( test . UserContext ( ctx , test . UserAdmin ) , expectedTeam , emptyRequest ) )
2021-12-14 21:34:11 +00:00
assert . True ( t , ds . ListHostsFuncInvoked )
assert . True ( t , ds . AddHostsToTeamFuncInvoked )
}
func TestAddHostsToTeamByFilterLabel ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-12-14 21:34:11 +00:00
expectedHostIDs := [ ] uint { 6 }
expectedTeam := ptr . Uint ( 1 )
2024-03-11 16:02:51 +00:00
expectedLabel := float64 ( 2 )
2021-12-14 21:34:11 +00:00
2024-05-24 16:25:27 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
2021-12-14 21:34:11 +00:00
ds . ListHostsInLabelFunc = func ( ctx context . Context , filter fleet . TeamFilter , lid uint , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
2024-03-11 16:02:51 +00:00
assert . Equal ( t , uint ( expectedLabel ) , lid )
2021-12-14 21:34:11 +00:00
var hosts [ ] * fleet . Host
for _ , id := range expectedHostIDs {
hosts = append ( hosts , & fleet . Host { ID : id } )
}
return hosts , nil
}
2025-07-17 14:20:49 +00:00
ds . AddHostsToTeamFunc = func ( ctx context . Context , params * fleet . AddHostsToTeamParams ) error {
assert . Equal ( t , expectedHostIDs , params . HostIDs )
2021-12-14 21:34:11 +00:00
return nil
}
2024-08-30 21:00:35 +00:00
ds . BulkSetPendingMDMHostProfilesFunc = func ( ctx context . Context , hids , tids [ ] uint , puuids , uuids [ ] string ,
) ( updates fleet . MDMProfilesUpdates , err error ) {
return fleet . MDMProfilesUpdates { } , nil
2023-03-27 18:43:01 +00:00
}
2023-05-15 18:06:09 +00:00
ds . ListMDMAppleDEPSerialsInHostIDsFunc = func ( ctx context . Context , hids [ ] uint ) ( [ ] string , error ) {
return nil , nil
}
2023-06-14 12:15:05 +00:00
ds . TeamFunc = func ( ctx context . Context , id uint ) ( * fleet . Team , error ) {
return & fleet . Team { ID : id } , nil
}
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2023-06-14 12:15:05 +00:00
return nil
}
2021-12-14 21:34:11 +00:00
2024-03-11 16:02:51 +00:00
filter := & map [ string ] interface { } { "label_id" : expectedLabel }
require . NoError ( t , svc . AddHostsToTeamByFilter ( test . UserContext ( ctx , test . UserAdmin ) , expectedTeam , filter ) )
2021-12-14 21:34:11 +00:00
assert . True ( t , ds . ListHostsInLabelFuncInvoked )
assert . True ( t , ds . AddHostsToTeamFuncInvoked )
}
func TestAddHostsToTeamByFilterEmptyHosts ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-12-14 21:34:11 +00:00
ds . ListHostsFunc = func ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
return [ ] * fleet . Host { } , nil
}
2025-07-17 14:20:49 +00:00
ds . AddHostsToTeamFunc = func ( ctx context . Context , params * fleet . AddHostsToTeamParams ) error {
2021-12-14 21:34:11 +00:00
return nil
}
2024-08-30 21:00:35 +00:00
ds . BulkSetPendingMDMHostProfilesFunc = func ( ctx context . Context , hids , tids [ ] uint , puuids , uuids [ ] string ,
) ( updates fleet . MDMProfilesUpdates , err error ) {
return fleet . MDMProfilesUpdates { } , nil
2023-03-27 18:43:01 +00:00
}
2021-12-14 21:34:11 +00:00
2024-03-11 16:02:51 +00:00
emptyFilter := & map [ string ] interface { } { }
require . NoError ( t , svc . AddHostsToTeamByFilter ( test . UserContext ( ctx , test . UserAdmin ) , nil , emptyFilter ) )
2021-12-14 21:34:11 +00:00
assert . True ( t , ds . ListHostsFuncInvoked )
assert . False ( t , ds . AddHostsToTeamFuncInvoked )
}
func TestRefetchHost ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-12-14 21:34:11 +00:00
host := & fleet . Host { ID : 3 }
2022-01-18 01:52:09 +00:00
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
2021-12-14 21:34:11 +00:00
return host , nil
}
2022-01-18 01:52:09 +00:00
ds . UpdateHostRefetchRequestedFunc = func ( ctx context . Context , id uint , value bool ) error {
assert . Equal ( t , host . ID , id )
assert . True ( t , value )
2021-12-14 21:34:11 +00:00
return nil
}
2022-11-15 14:08:05 +00:00
require . NoError ( t , svc . RefetchHost ( test . UserContext ( ctx , test . UserAdmin ) , host . ID ) )
require . NoError ( t , svc . RefetchHost ( test . UserContext ( ctx , test . UserObserver ) , host . ID ) )
2023-04-05 18:23:49 +00:00
require . NoError ( t , svc . RefetchHost ( test . UserContext ( ctx , test . UserObserverPlus ) , host . ID ) )
2022-11-15 14:08:05 +00:00
require . NoError ( t , svc . RefetchHost ( test . UserContext ( ctx , test . UserMaintainer ) , host . ID ) )
2022-01-18 01:52:09 +00:00
assert . True ( t , ds . HostLiteFuncInvoked )
assert . True ( t , ds . UpdateHostRefetchRequestedFuncInvoked )
2021-12-14 21:34:11 +00:00
}
func TestRefetchHostUserInTeams ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2021-12-14 21:34:11 +00:00
host := & fleet . Host { ID : 3 , TeamID : ptr . Uint ( 4 ) }
2022-01-18 01:52:09 +00:00
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
2021-12-14 21:34:11 +00:00
return host , nil
}
2022-01-18 01:52:09 +00:00
ds . UpdateHostRefetchRequestedFunc = func ( ctx context . Context , id uint , value bool ) error {
assert . Equal ( t , host . ID , id )
assert . True ( t , value )
2021-12-14 21:34:11 +00:00
return nil
}
maintainer := & fleet . User {
Teams : [ ] fleet . UserTeam {
{
Team : fleet . Team { ID : 4 } ,
Role : fleet . RoleMaintainer ,
} ,
} ,
}
2022-11-15 14:08:05 +00:00
require . NoError ( t , svc . RefetchHost ( test . UserContext ( ctx , maintainer ) , host . ID ) )
2022-01-18 01:52:09 +00:00
assert . True ( t , ds . HostLiteFuncInvoked )
assert . True ( t , ds . UpdateHostRefetchRequestedFuncInvoked )
ds . HostLiteFuncInvoked , ds . UpdateHostRefetchRequestedFuncInvoked = false , false
2021-12-14 21:34:11 +00:00
observer := & fleet . User {
Teams : [ ] fleet . UserTeam {
{
Team : fleet . Team { ID : 4 } ,
Role : fleet . RoleObserver ,
} ,
} ,
}
2022-11-15 14:08:05 +00:00
require . NoError ( t , svc . RefetchHost ( test . UserContext ( ctx , observer ) , host . ID ) )
2022-01-18 01:52:09 +00:00
assert . True ( t , ds . HostLiteFuncInvoked )
assert . True ( t , ds . UpdateHostRefetchRequestedFuncInvoked )
2021-12-14 21:34:11 +00:00
}
2022-04-18 21:19:58 +00:00
func TestEmptyTeamOSVersions ( t * testing . T ) {
ds := new ( mock . Store )
2022-11-15 14:08:05 +00:00
svc , ctx := newTestService ( t , ds , nil , nil )
2022-04-18 21:19:58 +00:00
testVersions := [ ] fleet . OSVersion { { HostsCount : 1 , Name : "macOS 12.1" , Platform : "darwin" } }
2024-03-13 13:20:00 +00:00
ds . TeamExistsFunc = func ( ctx context . Context , teamID uint ) ( bool , error ) {
if teamID == 3 {
return false , nil
2022-04-18 21:19:58 +00:00
}
2024-03-13 13:20:00 +00:00
return true , nil
2022-04-18 21:19:58 +00:00
}
2024-03-13 13:20:00 +00:00
ds . OSVersionsFunc = func (
ctx context . Context , teamFilter * fleet . TeamFilter , platform * string , name * string , version * string ,
) ( * fleet . OSVersions , error ) {
if * teamFilter . TeamID == 1 {
2022-04-18 21:19:58 +00:00
return & fleet . OSVersions { CountsUpdatedAt : time . Now ( ) , OSVersions : testVersions } , nil
}
2024-03-13 13:20:00 +00:00
if * teamFilter . TeamID == 4 {
2022-06-10 21:52:24 +00:00
return nil , errors . New ( "some unknown error" )
2022-04-18 21:19:58 +00:00
}
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
return nil , newNotFoundError ( )
2022-04-18 21:19:58 +00:00
}
2025-09-19 20:35:05 +00:00
ds . ListVulnsByMultipleOSVersionsFunc = func ( ctx context . Context , osVersions [ ] fleet . OSVersion , includeCVSS bool ,
2025-10-01 18:11:27 +00:00
teamID * uint ,
) ( map [ string ] fleet . Vulnerabilities , error ) {
2025-09-19 20:35:05 +00:00
return nil , nil
2024-01-24 19:18:57 +00:00
}
2022-04-18 21:19:58 +00:00
// team exists with stats
2024-01-24 19:18:57 +00:00
vers , _ , _ , err := svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , ptr . Uint ( 1 ) , ptr . String ( "darwin" ) , nil , nil , fleet . ListOptions { } , false )
2022-04-18 21:19:58 +00:00
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 1 )
// team exists but no stats
2024-01-24 19:18:57 +00:00
vers , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , ptr . Uint ( 2 ) , ptr . String ( "darwin" ) , nil , nil , fleet . ListOptions { } , false )
2022-04-18 21:19:58 +00:00
require . NoError ( t , err )
assert . Empty ( t , vers . OSVersions )
// team does not exist
2024-01-24 19:18:57 +00:00
_ , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , ptr . Uint ( 3 ) , ptr . String ( "darwin" ) , nil , nil , fleet . ListOptions { } , false )
2022-04-18 21:19:58 +00:00
require . Error ( t , err )
2024-03-13 13:20:00 +00:00
require . Contains ( t , fmt . Sprint ( err ) , "does not exist" )
2022-04-18 21:19:58 +00:00
// some unknown error
2024-01-24 19:18:57 +00:00
_ , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , ptr . Uint ( 4 ) , ptr . String ( "darwin" ) , nil , nil , fleet . ListOptions { } , false )
2022-04-18 21:19:58 +00:00
require . Error ( t , err )
require . Equal ( t , "some unknown error" , fmt . Sprint ( err ) )
}
2023-02-08 23:20:23 +00:00
2024-01-24 19:18:57 +00:00
func TestOSVersionsListOptions ( t * testing . T ) {
ds := new ( mock . Store )
svc , ctx := newTestService ( t , ds , nil , nil )
testVersions := [ ] fleet . OSVersion {
{ HostsCount : 4 , NameOnly : "Windows 11 Pro 22H2" , Platform : "windows" } ,
{ HostsCount : 1 , NameOnly : "macOS 12.1" , Platform : "darwin" } ,
{ HostsCount : 2 , NameOnly : "macOS 12.2" , Platform : "darwin" } ,
{ HostsCount : 3 , NameOnly : "Windows 11 Pro 21H2" , Platform : "windows" } ,
{ HostsCount : 5 , NameOnly : "Ubuntu 20.04" , Platform : "ubuntu" } ,
{ HostsCount : 6 , NameOnly : "Ubuntu 21.04" , Platform : "ubuntu" } ,
}
2025-10-01 18:11:27 +00:00
now := time . Now ( )
2024-03-13 13:20:00 +00:00
ds . OSVersionsFunc = func (
ctx context . Context , teamFilter * fleet . TeamFilter , platform * string , name * string , version * string ,
) ( * fleet . OSVersions , error ) {
2025-10-01 18:11:27 +00:00
return & fleet . OSVersions { CountsUpdatedAt : now , OSVersions : testVersions } , nil
2024-01-24 19:18:57 +00:00
}
2025-09-19 20:35:05 +00:00
ds . ListVulnsByMultipleOSVersionsFunc = func ( ctx context . Context , osVersions [ ] fleet . OSVersion , includeCVSS bool ,
2025-10-01 18:11:27 +00:00
teamID * uint ,
) ( map [ string ] fleet . Vulnerabilities , error ) {
2025-09-19 20:35:05 +00:00
return nil , nil
2024-01-24 19:18:57 +00:00
}
// test default descending count sort
opts := fleet . ListOptions { }
vers , _ , _ , err := svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil , nil , opts , false )
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 6 )
assert . Equal ( t , "Ubuntu 21.04" , vers . OSVersions [ 0 ] . NameOnly )
assert . Equal ( t , "Ubuntu 20.04" , vers . OSVersions [ 1 ] . NameOnly )
assert . Equal ( t , "Windows 11 Pro 22H2" , vers . OSVersions [ 2 ] . NameOnly )
assert . Equal ( t , "Windows 11 Pro 21H2" , vers . OSVersions [ 3 ] . NameOnly )
assert . Equal ( t , "macOS 12.2" , vers . OSVersions [ 4 ] . NameOnly )
assert . Equal ( t , "macOS 12.1" , vers . OSVersions [ 5 ] . NameOnly )
2025-10-01 18:11:27 +00:00
assert . Equal ( t , now , vers . CountsUpdatedAt )
2024-01-24 19:18:57 +00:00
// test ascending count sort
opts = fleet . ListOptions { OrderKey : "hosts_count" , OrderDirection : fleet . OrderAscending }
vers , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil , nil , opts , false )
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 6 )
assert . Equal ( t , "macOS 12.1" , vers . OSVersions [ 0 ] . NameOnly )
assert . Equal ( t , "macOS 12.2" , vers . OSVersions [ 1 ] . NameOnly )
assert . Equal ( t , "Windows 11 Pro 21H2" , vers . OSVersions [ 2 ] . NameOnly )
assert . Equal ( t , "Windows 11 Pro 22H2" , vers . OSVersions [ 3 ] . NameOnly )
assert . Equal ( t , "Ubuntu 20.04" , vers . OSVersions [ 4 ] . NameOnly )
assert . Equal ( t , "Ubuntu 21.04" , vers . OSVersions [ 5 ] . NameOnly )
2025-10-01 18:11:27 +00:00
assert . Equal ( t , now , vers . CountsUpdatedAt )
2024-01-24 19:18:57 +00:00
// pagination
opts = fleet . ListOptions { Page : 0 , PerPage : 2 }
vers , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil , nil , opts , false )
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 2 )
assert . Equal ( t , "Ubuntu 21.04" , vers . OSVersions [ 0 ] . NameOnly )
assert . Equal ( t , "Ubuntu 20.04" , vers . OSVersions [ 1 ] . NameOnly )
2025-10-01 18:11:27 +00:00
assert . Equal ( t , now , vers . CountsUpdatedAt )
2024-01-24 19:18:57 +00:00
opts = fleet . ListOptions { Page : 1 , PerPage : 2 }
vers , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil , nil , opts , false )
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 2 )
assert . Equal ( t , "Windows 11 Pro 22H2" , vers . OSVersions [ 0 ] . NameOnly )
assert . Equal ( t , "Windows 11 Pro 21H2" , vers . OSVersions [ 1 ] . NameOnly )
2025-10-01 18:11:27 +00:00
assert . Equal ( t , now , vers . CountsUpdatedAt )
2024-01-24 19:18:57 +00:00
// pagination + ascending hosts_count sort
opts = fleet . ListOptions { Page : 0 , PerPage : 2 , OrderKey : "hosts_count" , OrderDirection : fleet . OrderAscending }
vers , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil , nil , opts , false )
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 2 )
assert . Equal ( t , "macOS 12.1" , vers . OSVersions [ 0 ] . NameOnly )
assert . Equal ( t , "macOS 12.2" , vers . OSVersions [ 1 ] . NameOnly )
2025-10-01 18:11:27 +00:00
assert . Equal ( t , now , vers . CountsUpdatedAt )
2024-01-24 19:18:57 +00:00
// per page too high
opts = fleet . ListOptions { Page : 0 , PerPage : 1000 }
vers , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil , nil , opts , false )
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 6 )
2025-10-01 18:11:27 +00:00
assert . Equal ( t , now , vers . CountsUpdatedAt )
2024-01-24 19:18:57 +00:00
// Page number too high
opts = fleet . ListOptions { Page : 1000 , PerPage : 2 }
vers , _ , _ , err = svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil , nil , opts , false )
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 0 )
2025-10-01 18:11:27 +00:00
assert . Equal ( t , now , vers . CountsUpdatedAt )
}
func TestOSVersionsDefaultPagination ( t * testing . T ) {
ds := new ( mock . Store )
svc , ctx := newTestService ( t , ds , nil , nil )
testVersions := [ ] fleet . OSVersion { }
for i := range 50 {
testVersions = append ( testVersions , fleet . OSVersion { NameOnly : fmt . Sprintf ( "Version %02d" , i ) , HostsCount : i , Platform : "windows" } )
}
ds . OSVersionsFunc = func (
ctx context . Context , teamFilter * fleet . TeamFilter , platform * string , name * string , version * string ,
) ( * fleet . OSVersions , error ) {
return & fleet . OSVersions { CountsUpdatedAt : time . Now ( ) , OSVersions : testVersions } , nil
}
ds . ListVulnsByMultipleOSVersionsFunc = func ( ctx context . Context , osVersions [ ] fleet . OSVersion , includeCVSS bool ,
teamID * uint ,
) ( map [ string ] fleet . Vulnerabilities , error ) {
return nil , nil
}
// test default descending count sort + default pagination (page 0, per_page 20)
opts := fleet . ListOptions { }
vers , _ , _ , err := svc . OSVersions ( test . UserContext ( ctx , test . UserAdmin ) , nil , nil , nil , nil , opts , false )
require . NoError ( t , err )
assert . Len ( t , vers . OSVersions , 20 )
assert . Equal ( t , "Version 49" , vers . OSVersions [ 0 ] . NameOnly )
assert . Equal ( t , "Version 30" , vers . OSVersions [ 19 ] . NameOnly )
2024-01-24 19:18:57 +00:00
}
2023-02-08 23:20:23 +00:00
func TestHostEncryptionKey ( t * testing . T ) {
cases := [ ] struct {
name string
host * fleet . Host
allowedUsers [ ] * fleet . User
disallowedUsers [ ] * fleet . User
} {
{
name : "global host" ,
host : & fleet . Host {
ID : 1 ,
Platform : "darwin" ,
NodeKey : ptr . String ( "test_key" ) ,
Hostname : "test_hostname" ,
UUID : "test_uuid" ,
TeamID : nil ,
} ,
allowedUsers : [ ] * fleet . User {
test . UserAdmin ,
test . UserMaintainer ,
test . UserObserver ,
2023-04-05 18:23:49 +00:00
test . UserObserverPlus ,
2023-02-08 23:20:23 +00:00
} ,
disallowedUsers : [ ] * fleet . User {
test . UserTeamAdminTeam1 ,
test . UserTeamMaintainerTeam1 ,
test . UserTeamObserverTeam1 ,
test . UserNoRoles ,
} ,
} ,
{
name : "team host" ,
host : & fleet . Host {
ID : 2 ,
Platform : "darwin" ,
NodeKey : ptr . String ( "test_key_2" ) ,
Hostname : "test_hostname_2" ,
UUID : "test_uuid_2" ,
TeamID : ptr . Uint ( 1 ) ,
} ,
allowedUsers : [ ] * fleet . User {
test . UserAdmin ,
test . UserMaintainer ,
test . UserObserver ,
2023-04-05 18:23:49 +00:00
test . UserObserverPlus ,
2023-02-08 23:20:23 +00:00
test . UserTeamAdminTeam1 ,
test . UserTeamMaintainerTeam1 ,
test . UserTeamObserverTeam1 ,
2023-04-05 18:23:49 +00:00
test . UserTeamObserverPlusTeam1 ,
2023-02-08 23:20:23 +00:00
} ,
disallowedUsers : [ ] * fleet . User {
test . UserTeamAdminTeam2 ,
test . UserTeamMaintainerTeam2 ,
test . UserTeamObserverTeam2 ,
2023-04-05 18:23:49 +00:00
test . UserTeamObserverPlusTeam2 ,
2023-02-08 23:20:23 +00:00
test . UserNoRoles ,
} ,
} ,
}
testCert , testKey , err := apple_mdm . NewSCEPCACertKey ( )
require . NoError ( t , err )
testCertPEM := tokenpki . PEMCertificate ( testCert . Raw )
testKeyPEM := tokenpki . PEMRSAPrivateKey ( testKey )
fleetCfg := config . TestConfig ( )
2024-05-30 21:18:42 +00:00
config . SetTestMDMConfig ( t , & fleetCfg , testCertPEM , testKeyPEM , "" )
2023-02-08 23:20:23 +00:00
recoveryKey := "AAA-BBB-CCC"
encryptedKey , err := pkcs7 . Encrypt ( [ ] byte ( recoveryKey ) , [ ] * x509 . Certificate { testCert } )
require . NoError ( t , err )
base64EncryptedKey := base64 . StdEncoding . EncodeToString ( encryptedKey )
2023-10-06 22:04:33 +00:00
wstep , _ , _ , err := fleetCfg . MDM . MicrosoftWSTEP ( )
require . NoError ( t , err )
winEncryptedKey , err := pkcs7 . Encrypt ( [ ] byte ( recoveryKey ) , [ ] * x509 . Certificate { wstep . Leaf } )
require . NoError ( t , err )
winBase64EncryptedKey := base64 . StdEncoding . EncodeToString ( winEncryptedKey )
2023-02-08 23:20:23 +00:00
for _ , tt := range cases {
t . Run ( tt . name , func ( t * testing . T ) {
ds := new ( mock . Store )
2023-10-06 22:04:33 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { MDM : fleet . MDM { EnabledAndConfigured : true } } , nil
}
2023-02-08 23:20:23 +00:00
svc , ctx := newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
require . Equal ( t , tt . host . ID , id )
return tt . host , nil
}
ds . GetHostDiskEncryptionKeyFunc = func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey {
Base64Encrypted : base64EncryptedKey ,
Decryptable : ptr . Bool ( true ) ,
} , nil
}
2025-07-02 19:57:25 +00:00
ds . GetHostArchivedDiskEncryptionKeyFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostArchivedDiskEncryptionKey , error ) {
return & fleet . HostArchivedDiskEncryptionKey { } , nil
}
2023-02-08 23:20:23 +00:00
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2023-02-08 23:20:23 +00:00
act := activity . ( fleet . ActivityTypeReadHostDiskEncryptionKey )
require . Equal ( t , tt . host . ID , act . HostID )
2025-05-21 20:47:11 +00:00
require . Equal ( t , [ ] uint { tt . host . ID } , act . HostIDs ( ) )
2023-02-08 23:20:23 +00:00
require . EqualValues ( t , act . HostDisplayName , tt . host . DisplayName ( ) )
return nil
}
2024-10-09 18:47:27 +00:00
ds . GetAllMDMConfigAssetsByNameFunc = func ( ctx context . Context , assetNames [ ] fleet . MDMAssetName ,
2024-11-19 20:11:59 +00:00
_ sqlx . QueryerContext ,
) ( map [ fleet . MDMAssetName ] fleet . MDMConfigAsset , error ) {
2024-05-30 21:18:42 +00:00
return map [ fleet . MDMAssetName ] fleet . MDMConfigAsset {
fleet . MDMAssetCACert : { Name : fleet . MDMAssetCACert , Value : testCertPEM } ,
fleet . MDMAssetCAKey : { Name : fleet . MDMAssetCAKey , Value : testKeyPEM } ,
} , nil
}
2023-02-08 23:20:23 +00:00
t . Run ( "allowed users" , func ( t * testing . T ) {
for _ , u := range tt . allowedUsers {
_ , err := svc . HostEncryptionKey ( test . UserContext ( ctx , u ) , tt . host . ID )
require . NoError ( t , err )
}
} )
t . Run ( "disallowed users" , func ( t * testing . T ) {
for _ , u := range tt . disallowedUsers {
_ , err := svc . HostEncryptionKey ( test . UserContext ( ctx , u ) , tt . host . ID )
require . Error ( t , err )
require . Contains ( t , authz . ForbiddenErrorMessage , err . Error ( ) )
}
} )
t . Run ( "no user in context" , func ( t * testing . T ) {
_ , err := svc . HostEncryptionKey ( ctx , tt . host . ID )
require . Error ( t , err )
require . Contains ( t , authz . ForbiddenErrorMessage , err . Error ( ) )
} )
} )
}
t . Run ( "test error cases" , func ( t * testing . T ) {
ds := new ( mock . Store )
2023-10-06 22:04:33 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { MDM : fleet . MDM { EnabledAndConfigured : true } } , nil
}
svc , ctx := newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
2023-02-08 23:20:23 +00:00
ctx = test . UserContext ( ctx , test . UserAdmin )
hostErr := errors . New ( "host error" )
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
return nil , hostErr
}
_ , err := svc . HostEncryptionKey ( ctx , 1 )
require . ErrorIs ( t , err , hostErr )
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
return & fleet . Host { } , nil
}
keyErr := errors . New ( "key error" )
ds . GetHostDiskEncryptionKeyFunc = func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return nil , keyErr
}
2025-07-02 19:57:25 +00:00
ds . GetHostArchivedDiskEncryptionKeyFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostArchivedDiskEncryptionKey , error ) {
return & fleet . HostArchivedDiskEncryptionKey { } , nil
}
ds . GetHostArchivedDiskEncryptionKeyFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostArchivedDiskEncryptionKey , error ) {
return & fleet . HostArchivedDiskEncryptionKey { } , nil
}
2024-10-09 18:47:27 +00:00
ds . GetAllMDMConfigAssetsByNameFunc = func ( ctx context . Context , assetNames [ ] fleet . MDMAssetName ,
2024-11-19 20:11:59 +00:00
_ sqlx . QueryerContext ,
) ( map [ fleet . MDMAssetName ] fleet . MDMConfigAsset , error ) {
2024-05-30 21:18:42 +00:00
return map [ fleet . MDMAssetName ] fleet . MDMConfigAsset {
fleet . MDMAssetCACert : { Name : fleet . MDMAssetCACert , Value : testCertPEM } ,
fleet . MDMAssetCAKey : { Name : fleet . MDMAssetCAKey , Value : testKeyPEM } ,
} , nil
}
2023-02-08 23:20:23 +00:00
_ , err = svc . HostEncryptionKey ( ctx , 1 )
require . ErrorIs ( t , err , keyErr )
ds . GetHostDiskEncryptionKeyFunc = func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey { Base64Encrypted : "key" } , nil
}
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2023-02-08 23:20:23 +00:00
return errors . New ( "activity error" )
}
_ , err = svc . HostEncryptionKey ( ctx , 1 )
require . Error ( t , err )
} )
2023-10-06 22:04:33 +00:00
t . Run ( "host platform mdm enabled" , func ( t * testing . T ) {
cases := [ ] struct {
hostPlatform string
macMDMEnabled bool
winMDMEnabled bool
shouldFail bool
} {
{ "windows" , true , false , true } ,
{ "windows" , false , true , false } ,
{ "windows" , true , true , false } ,
{ "darwin" , true , false , false } ,
{ "darwin" , false , true , true } ,
{ "darwin" , true , true , false } ,
}
for _ , c := range cases {
t . Run ( fmt . Sprintf ( "%s: mac mdm: %t; win mdm: %t" , c . hostPlatform , c . macMDMEnabled , c . winMDMEnabled ) , func ( t * testing . T ) {
ds := new ( mock . Store )
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { MDM : fleet . MDM { EnabledAndConfigured : c . macMDMEnabled , WindowsEnabledAndConfigured : c . winMDMEnabled } } , nil
}
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
return & fleet . Host { Platform : c . hostPlatform } , nil
}
ds . GetHostDiskEncryptionKeyFunc = func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
key := base64EncryptedKey
if c . hostPlatform == "windows" {
key = winBase64EncryptedKey
}
return & fleet . HostDiskEncryptionKey {
Base64Encrypted : key ,
Decryptable : ptr . Bool ( true ) ,
} , nil
}
2025-07-02 19:57:25 +00:00
ds . GetHostArchivedDiskEncryptionKeyFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostArchivedDiskEncryptionKey , error ) {
return & fleet . HostArchivedDiskEncryptionKey { } , nil
}
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2023-10-06 22:04:33 +00:00
return nil
}
2024-10-09 18:47:27 +00:00
ds . GetAllMDMConfigAssetsByNameFunc = func ( ctx context . Context , assetNames [ ] fleet . MDMAssetName ,
2024-11-19 20:11:59 +00:00
_ sqlx . QueryerContext ,
) ( map [ fleet . MDMAssetName ] fleet . MDMConfigAsset , error ) {
2024-05-30 21:18:42 +00:00
return map [ fleet . MDMAssetName ] fleet . MDMConfigAsset {
fleet . MDMAssetCACert : { Name : fleet . MDMAssetCACert , Value : testCertPEM } ,
fleet . MDMAssetCAKey : { Name : fleet . MDMAssetCAKey , Value : testKeyPEM } ,
} , nil
}
2023-10-06 22:04:33 +00:00
svc , ctx := newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
ctx = test . UserContext ( ctx , test . UserAdmin )
_ , err := svc . HostEncryptionKey ( ctx , 1 )
if c . shouldFail {
require . Error ( t , err )
2025-04-29 19:11:31 +00:00
if c . macMDMEnabled && ! c . winMDMEnabled && c . hostPlatform == "windows" {
require . ErrorContains ( t , err , fleet . ErrWindowsMDMNotConfigured . Error ( ) )
} else {
require . ErrorContains ( t , err , fleet . ErrMDMNotConfigured . Error ( ) )
}
2023-10-06 22:04:33 +00:00
} else {
require . NoError ( t , err )
}
} )
}
} )
2024-11-18 22:44:25 +00:00
t . Run ( "Linux encryption" , func ( t * testing . T ) {
ds := new ( mock . Store )
host := & fleet . Host { ID : 1 , Platform : "ubuntu" }
symmetricKey := "this_is_a_32_byte_symmetric_key!"
passphrase := "this_is_a_passphrase"
base64EncryptedKey , err := mdm . EncryptAndEncode ( passphrase , symmetricKey )
require . NoError ( t , err )
ds . HostLiteFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
return host , nil
}
2025-07-02 19:57:25 +00:00
ds . GetHostArchivedDiskEncryptionKeyFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostArchivedDiskEncryptionKey , error ) {
return & fleet . HostArchivedDiskEncryptionKey { } , nil
}
2024-11-18 22:44:25 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
return nil
}
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) { // needed for new activity
return & fleet . AppConfig { } , nil
}
// error when no server private key
fleetCfg . Server . PrivateKey = ""
svc , ctx := newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
ctx = test . UserContext ( ctx , test . UserAdmin )
key , err := svc . HostEncryptionKey ( ctx , 1 )
require . Error ( t , err , "private key is unavailable" )
require . Nil ( t , key )
// error when key is not set
ds . GetHostDiskEncryptionKeyFunc = func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey { } , nil
}
fleetCfg . Server . PrivateKey = symmetricKey
svc , ctx = newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
ctx = test . UserContext ( ctx , test . UserAdmin )
key , err = svc . HostEncryptionKey ( ctx , 1 )
require . Error ( t , err , "host encryption key is not set" )
require . Nil ( t , key )
// error when key is not set
ds . GetHostDiskEncryptionKeyFunc = func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey {
Base64Encrypted : "thisIsWrong" ,
Decryptable : ptr . Bool ( true ) ,
} , nil
}
svc , ctx = newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
ctx = test . UserContext ( ctx , test . UserAdmin )
key , err = svc . HostEncryptionKey ( ctx , 1 )
require . Error ( t , err , "decrypt host encryption key" )
require . Nil ( t , key )
// happy path
ds . GetHostDiskEncryptionKeyFunc = func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey {
Base64Encrypted : base64EncryptedKey ,
Decryptable : ptr . Bool ( true ) ,
} , nil
}
svc , ctx = newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
ctx = test . UserContext ( ctx , test . UserAdmin )
key , err = svc . HostEncryptionKey ( ctx , 1 )
require . NoError ( t , err )
require . Equal ( t , passphrase , key . DecryptedValue )
} )
2023-02-08 23:20:23 +00:00
}
2023-06-21 18:00:49 +00:00
2025-04-08 14:35:06 +00:00
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
2023-06-21 18:00:49 +00:00
func TestHostMDMProfileDetail ( t * testing . T ) {
ds := new ( mock . Store )
testCert , testKey , err := apple_mdm . NewSCEPCACertKey ( )
require . NoError ( t , err )
testCertPEM := tokenpki . PEMCertificate ( testCert . Raw )
testKeyPEM := tokenpki . PEMRSAPrivateKey ( testKey )
fleetCfg := config . TestConfig ( )
2024-05-30 21:18:42 +00:00
config . SetTestMDMConfig ( t , & fleetCfg , testCertPEM , testKeyPEM , "" )
2023-06-21 18:00:49 +00:00
svc , ctx := newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
ctx = test . UserContext ( ctx , test . UserAdmin )
ds . HostFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
return & fleet . Host {
2023-10-06 22:04:33 +00:00
ID : 1 ,
Platform : "darwin" ,
2023-06-21 18:00:49 +00:00
} , nil
}
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
return nil
}
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820)
## Addresses full stack for #18554
- Add new `timezone` column to `calendar_events` table
- When fetched from Google's API, save calendar user's timezone in this
new column along with rest of event data
- Implement datastore method to retrieve the start time and timezone for
a host's next calendar event as a `HostMaintenanceWindow`
- Localize and add UTC offset to the `HostMaintenanceWindow`'s start
time according to its `timezone`
- Include the processed `HostMaintenanceWindow`, if present, in the
response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}`
endpoints
- Implement UI on the host details page to display this data
- Add new and update existing UI, core integration, datastore, and
`fleetctl` tests
- Update `date-fns` package to the latest version
<img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed">
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified tables for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
2023-06-21 18:00:49 +00:00
ds . GetHostMDMMacOSSetupFunc = func ( ctx context . Context , hid uint ) ( * fleet . HostMDMMacOSSetup , error ) {
return nil , nil
}
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2023-06-21 18:00:49 +00:00
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig {
MDM : fleet . MDM {
EnabledAndConfigured : true ,
} ,
} , nil
}
2025-04-08 14:35:06 +00:00
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
2025-05-09 18:18:48 +00:00
ds . GetNanoMDMEnrollmentTimesFunc = func ( ctx context . Context , hostUUID string ) ( * time . Time , * time . Time , error ) {
return nil , nil , nil
}
2025-05-20 20:41:38 +00:00
ds . UpdateHostIssuesFailingPoliciesFunc = func ( ctx context . Context , hostIDs [ ] uint ) error {
return nil
}
2025-08-22 17:36:03 +00:00
ds . UpdateHostIssuesFailingPoliciesForSingleHostFunc = func ( ctx context . Context , hostID uint ) error {
return nil
}
2025-05-20 20:41:38 +00:00
ds . GetHostIssuesLastUpdatedFunc = func ( ctx context . Context , hostId uint ) ( time . Time , error ) {
return time . Time { } , nil
}
2025-07-15 18:21:39 +00:00
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
2023-06-21 18:00:49 +00:00
cases := [ ] struct {
name string
storedDetail string
expectedDetail string
} {
{
name : "no detail" ,
storedDetail : "" ,
expectedDetail : "" ,
} ,
{
name : "other detail" ,
storedDetail : "other detail" ,
expectedDetail : "other detail" ,
} ,
{
name : "failed was verifying" ,
storedDetail : string ( fleet . HostMDMProfileDetailFailedWasVerifying ) ,
expectedDetail : fleet . HostMDMProfileDetailFailedWasVerifying . Message ( ) ,
} ,
{
name : "failed was verified" ,
storedDetail : string ( fleet . HostMDMProfileDetailFailedWasVerified ) ,
expectedDetail : fleet . HostMDMProfileDetailFailedWasVerified . Message ( ) ,
} ,
}
for _ , tt := range cases {
t . Run ( tt . name , func ( t * testing . T ) {
2023-11-20 20:34:57 +00:00
ds . GetHostMDMAppleProfilesFunc = func ( ctx context . Context , host_uuid string ) ( [ ] fleet . HostMDMAppleProfile , error ) {
2023-06-21 18:00:49 +00:00
return [ ] fleet . HostMDMAppleProfile {
{
Name : "test" ,
Identifier : "test" ,
2023-11-07 21:03:03 +00:00
OperationType : fleet . MDMOperationTypeInstall ,
Status : & fleet . MDMDeliveryFailed ,
2023-06-21 18:00:49 +00:00
Detail : tt . storedDetail ,
} ,
} , nil
}
h , err := svc . GetHost ( ctx , uint ( 1 ) , fleet . HostDetailOptions { } )
require . NoError ( t , err )
require . NotNil ( t , h . MDM . Profiles )
profs := * h . MDM . Profiles
require . Len ( t , profs , 1 )
require . Equal ( t , tt . expectedDetail , profs [ 0 ] . Detail )
} )
}
}
2024-02-13 18:03:53 +00:00
2025-08-26 15:31:06 +00:00
// Fragile test: This test is fragile because of the large reliance on Datastore mocks. Consider refactoring test/logic or removing the test. It may be slowing us down more than helping us.
func TestHostMDMProfileScopes ( t * testing . T ) {
ds := new ( mock . Store )
testCert , testKey , err := apple_mdm . NewSCEPCACertKey ( )
require . NoError ( t , err )
testCertPEM := tokenpki . PEMCertificate ( testCert . Raw )
testKeyPEM := tokenpki . PEMRSAPrivateKey ( testKey )
fleetCfg := config . TestConfig ( )
config . SetTestMDMConfig ( t , & fleetCfg , testCertPEM , testKeyPEM , "" )
svc , ctx := newTestServiceWithConfig ( t , ds , fleetCfg , nil , nil )
ctx = test . UserContext ( ctx , test . UserAdmin )
appleHost := & fleet . Host {
ID : 1 ,
UUID : "apple-host-uuid" ,
Platform : "darwin" ,
}
windowsHost := & fleet . Host {
ID : 2 ,
UUID : "windows-host-uuid" ,
Platform : "windows" ,
}
ds . HostFunc = func ( ctx context . Context , id uint ) ( * fleet . Host , error ) {
if id == appleHost . ID {
return appleHost , nil
}
require . Equal ( t , id , windowsHost . ID , "Host should only be called with Apple or Windows host IDs" )
return windowsHost , nil
}
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
return nil
}
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
ds . GetHostMDMMacOSSetupFunc = func ( ctx context . Context , hid uint ) ( * fleet . HostMDMMacOSSetup , error ) {
return nil , nil
}
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
return & fleet . HostLockWipeStatus { } , nil
}
ds . UpdateHostIssuesFailingPoliciesForSingleHostFunc = func ( ctx context . Context , hostID uint ) error {
return nil
}
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig {
MDM : fleet . MDM {
EnabledAndConfigured : true ,
WindowsEnabledAndConfigured : true ,
} ,
} , nil
}
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
ds . GetNanoMDMEnrollmentTimesFunc = func ( ctx context . Context , hostUUID string ) ( * time . Time , * time . Time , error ) {
return nil , nil , nil
}
ds . UpdateHostIssuesFailingPoliciesFunc = func ( ctx context . Context , hostIDs [ ] uint ) error {
return nil
}
ds . GetHostIssuesLastUpdatedFunc = func ( ctx context . Context , hostId uint ) ( time . Time , error ) {
return time . Time { } , nil
}
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
appleCases := [ ] struct {
name string
storedProfiles [ ] fleet . HostMDMAppleProfile
expectedProfiles [ ] fleet . HostMDMProfile
} {
{
name : "no profiles" ,
storedProfiles : nil ,
expectedProfiles : nil ,
} ,
{
name : "system scoped profile" ,
storedProfiles : [ ] fleet . HostMDMAppleProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified , Scope : fleet . PayloadScopeSystem } } ,
expectedProfiles : [ ] fleet . HostMDMProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified , Scope : ptr . String ( "device" ) , ManagedLocalAccount : ptr . String ( "" ) } } ,
} ,
{
name : "User scoped profile with username" ,
storedProfiles : [ ] fleet . HostMDMAppleProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified , Scope : fleet . PayloadScopeUser , ManagedLocalAccount : "fleetie" } } ,
expectedProfiles : [ ] fleet . HostMDMProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified , Scope : ptr . String ( "user" ) , ManagedLocalAccount : ptr . String ( "fleetie" ) } } ,
} ,
{
name : "User scoped profile without username for some reason" ,
storedProfiles : [ ] fleet . HostMDMAppleProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified , Scope : fleet . PayloadScopeUser } } ,
expectedProfiles : [ ] fleet . HostMDMProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified , Scope : ptr . String ( "user" ) , ManagedLocalAccount : ptr . String ( "" ) } } ,
} ,
{
name : "system + user scoped profiles" ,
storedProfiles : [ ] fleet . HostMDMAppleProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified , Scope : fleet . PayloadScopeSystem } , { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid2" , Name : "Profile2" , Status : & fleet . MDMDeliveryVerified , Scope : fleet . PayloadScopeUser , ManagedLocalAccount : "fleetie" } } ,
expectedProfiles : [ ] fleet . HostMDMProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified , Scope : ptr . String ( "device" ) , ManagedLocalAccount : ptr . String ( "" ) } , { OperationType : fleet . MDMOperationTypeInstall , HostUUID : appleHost . UUID , ProfileUUID : "profile-uuid2" , Name : "Profile2" , Status : & fleet . MDMDeliveryVerified , Scope : ptr . String ( "user" ) , ManagedLocalAccount : ptr . String ( "fleetie" ) } } ,
} ,
}
windowsCases := [ ] struct {
name string
storedProfiles [ ] fleet . HostMDMWindowsProfile
expectedProfiles [ ] fleet . HostMDMProfile
} {
{
name : "no profiles" ,
storedProfiles : nil ,
expectedProfiles : nil ,
} ,
// Windows does not support scopes or managed local accounts yet but we should not error and
// should set these to nil which is checked below
{
name : "example profile" ,
storedProfiles : [ ] fleet . HostMDMWindowsProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : windowsHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified } } ,
expectedProfiles : [ ] fleet . HostMDMProfile { { OperationType : fleet . MDMOperationTypeInstall , HostUUID : windowsHost . UUID , ProfileUUID : "profile-uuid1" , Name : "Profile1" , Status : & fleet . MDMDeliveryVerified } } ,
} ,
}
for _ , tt := range appleCases {
t . Run ( tt . name , func ( t * testing . T ) {
ds . GetHostMDMAppleProfilesFunc = func ( ctx context . Context , host_uuid string ) ( [ ] fleet . HostMDMAppleProfile , error ) {
return tt . storedProfiles , nil
}
h , err := svc . GetHost ( ctx , appleHost . ID , fleet . HostDetailOptions { } )
require . NoError ( t , err )
if tt . storedProfiles == nil {
require . NotNil ( t , h . MDM . Profiles )
require . Empty ( t , * h . MDM . Profiles )
return
}
profs := * h . MDM . Profiles
require . Len ( t , profs , len ( tt . expectedProfiles ) )
for i := range profs {
require . Equal ( t , tt . expectedProfiles [ i ] . OperationType , profs [ i ] . OperationType )
require . Equal ( t , tt . expectedProfiles [ i ] . HostUUID , profs [ i ] . HostUUID )
require . Equal ( t , tt . expectedProfiles [ i ] . ProfileUUID , profs [ i ] . ProfileUUID )
require . Equal ( t , tt . expectedProfiles [ i ] . Name , profs [ i ] . Name )
require . Equal ( t , tt . expectedProfiles [ i ] . Status , profs [ i ] . Status )
require . NotNil ( t , profs [ i ] . Scope )
require . Equal ( t , * tt . expectedProfiles [ i ] . Scope , * profs [ i ] . Scope )
require . NotNil ( t , profs [ i ] . ManagedLocalAccount )
require . Equal ( t , * tt . expectedProfiles [ i ] . ManagedLocalAccount , * profs [ i ] . ManagedLocalAccount )
}
} )
}
for _ , tt := range windowsCases {
t . Run ( tt . name , func ( t * testing . T ) {
ds . GetHostMDMWindowsProfilesFunc = func ( ctx context . Context , host_uuid string ) ( [ ] fleet . HostMDMWindowsProfile , error ) {
return tt . storedProfiles , nil
}
h , err := svc . GetHost ( ctx , windowsHost . ID , fleet . HostDetailOptions { } )
require . NoError ( t , err )
if tt . storedProfiles == nil {
require . NotNil ( t , h . MDM . Profiles )
require . Empty ( t , * h . MDM . Profiles )
return
}
profs := * h . MDM . Profiles
require . Len ( t , profs , len ( tt . expectedProfiles ) )
for i := range profs {
require . Equal ( t , tt . expectedProfiles [ i ] . OperationType , profs [ i ] . OperationType )
require . Equal ( t , tt . expectedProfiles [ i ] . HostUUID , profs [ i ] . HostUUID )
require . Equal ( t , tt . expectedProfiles [ i ] . ProfileUUID , profs [ i ] . ProfileUUID )
require . Equal ( t , tt . expectedProfiles [ i ] . Name , profs [ i ] . Name )
require . Equal ( t , tt . expectedProfiles [ i ] . Status , profs [ i ] . Status )
require . Nil ( t , profs [ i ] . Scope )
require . Nil ( t , profs [ i ] . ManagedLocalAccount )
}
} )
}
}
2024-02-26 16:31:00 +00:00
func TestLockUnlockWipeHostAuth ( t * testing . T ) {
2024-02-13 18:03:53 +00:00
ds := new ( mock . Store )
svc , ctx := newTestService ( t , ds , nil , nil , & TestServerOpts { License : & fleet . LicenseInfo { Tier : fleet . TierPremium } } )
2024-02-26 16:31:00 +00:00
const (
teamHostID = 1
globalHostID = 2
)
2024-02-13 18:03:53 +00:00
teamHost := & fleet . Host { TeamID : ptr . Uint ( 1 ) , Platform : "darwin" }
globalHost := & fleet . Host { Platform : "darwin" }
ds . HostByIdentifierFunc = func ( ctx context . Context , identifier string ) ( * fleet . Host , error ) {
2024-02-26 16:31:00 +00:00
if identifier == fmt . Sprint ( teamHostID ) {
2024-02-13 18:03:53 +00:00
return teamHost , nil
}
return globalHost , nil
}
ds . LoadHostSoftwareFunc = func ( ctx context . Context , host * fleet . Host , includeCVEScores bool ) error {
return nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( packs [ ] * fleet . Pack , err error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
Add host's next maintenance window to the `hosts/{id}` and `hosts/identifier/{identifier}` endpoints, and render that data on the host details page (#19820)
## Addresses full stack for #18554
- Add new `timezone` column to `calendar_events` table
- When fetched from Google's API, save calendar user's timezone in this
new column along with rest of event data
- Implement datastore method to retrieve the start time and timezone for
a host's next calendar event as a `HostMaintenanceWindow`
- Localize and add UTC offset to the `HostMaintenanceWindow`'s start
time according to its `timezone`
- Include the processed `HostMaintenanceWindow`, if present, in the
response to the `GET` `hosts/{id}` and `hosts/identifier/{identifier}`
endpoints
- Implement UI on the host details page to display this data
- Add new and update existing UI, core integration, datastore, and
`fleetctl` tests
- Update `date-fns` package to the latest version
<img width="1062" alt="Screenshot 2024-06-26 at 1 02 34 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/c3ddad97-23da-42c1-b4ed-b7615ec88aed">
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified tables for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-06-28 17:51:13 +00:00
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
2024-02-13 18:03:53 +00:00
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . GetHostMDMAppleProfilesFunc = func ( ctx context . Context , hostUUID string ) ( [ ] fleet . HostMDMAppleProfile , error ) {
return nil , nil
}
ds . GetHostMDMWindowsProfilesFunc = func ( ctx context . Context , hostUUID string ) ( [ ] fleet . HostMDMWindowsProfile , error ) {
return nil , nil
}
ds . GetHostMDMMacOSSetupFunc = func ( ctx context . Context , hostID uint ) ( * fleet . HostMDMMacOSSetup , error ) {
return nil , nil
}
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2024-02-26 21:52:23 +00:00
ds . LockHostViaScriptFunc = func ( ctx context . Context , request * fleet . HostScriptRequestPayload , platform string ) error {
2024-02-13 18:03:53 +00:00
return nil
}
2025-08-01 14:03:37 +00:00
// Some functions use Host, others HostLite. For our purposes either is fine
ds . HostFunc = func ( ctx context . Context , hostID uint ) ( * fleet . Host , error ) {
2024-02-26 16:31:00 +00:00
if hostID == teamHostID {
2024-02-13 18:03:53 +00:00
return teamHost , nil
}
return globalHost , nil
}
2025-08-01 14:03:37 +00:00
ds . HostLiteFunc = mock . HostLiteFunc ( ds . HostFunc )
2024-02-13 18:03:53 +00:00
ds . GetMDMWindowsBitLockerStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostMDMDiskEncryption , error ) {
return nil , nil
}
ds . GetHostMDMFunc = func ( ctx context . Context , hostID uint ) ( * fleet . HostMDM , error ) {
return & fleet . HostMDM { Enrolled : true , Name : fleet . WellKnownMDMFleet } , nil
}
2024-05-24 16:25:27 +00:00
ds . NewActivityFunc = func (
ctx context . Context , user * fleet . User , activity fleet . ActivityDetails , details [ ] byte , createdAt time . Time ,
) error {
2024-02-13 18:03:53 +00:00
return nil
}
2024-02-26 21:52:23 +00:00
ds . UnlockHostManuallyFunc = func ( ctx context . Context , hostID uint , platform string , ts time . Time ) error {
2024-02-13 18:03:53 +00:00
return nil
}
2024-06-14 18:01:12 +00:00
ds . IsHostConnectedToFleetMDMFunc = func ( ctx context . Context , host * fleet . Host ) ( bool , error ) {
return true , nil
}
2025-05-09 18:18:48 +00:00
ds . GetNanoMDMEnrollmentTimesFunc = func ( ctx context . Context , hostUUID string ) ( * time . Time , * time . Time , error ) {
return nil , nil , nil
}
2024-02-13 18:03:53 +00:00
cases := [ ] struct {
name string
user * fleet . User
shouldFailGlobalWrite bool
shouldFailTeamWrite bool
} {
{
name : "global observer" ,
user : & fleet . User { GlobalRole : ptr . String ( fleet . RoleObserver ) } ,
2024-02-16 15:52:14 +00:00
shouldFailGlobalWrite : true ,
shouldFailTeamWrite : true ,
2024-02-13 18:03:53 +00:00
} ,
{
name : "team observer" ,
user : & fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 1 } , Role : fleet . RoleObserver } } } ,
shouldFailGlobalWrite : true ,
2024-02-16 15:52:14 +00:00
shouldFailTeamWrite : true ,
2024-02-13 18:03:53 +00:00
} ,
{
name : "global observer plus" ,
user : & fleet . User { GlobalRole : ptr . String ( fleet . RoleObserverPlus ) } ,
2024-02-16 15:52:14 +00:00
shouldFailGlobalWrite : true ,
shouldFailTeamWrite : true ,
2024-02-13 18:03:53 +00:00
} ,
{
name : "team observer plus" ,
user : & fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 1 } , Role : fleet . RoleObserverPlus } } } ,
shouldFailGlobalWrite : true ,
2024-02-16 15:52:14 +00:00
shouldFailTeamWrite : true ,
2024-02-13 18:03:53 +00:00
} ,
{
name : "global admin" ,
user : & fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } ,
shouldFailGlobalWrite : false ,
shouldFailTeamWrite : false ,
} ,
{
name : "team admin" ,
user : & fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 1 } , Role : fleet . RoleAdmin } } } ,
shouldFailGlobalWrite : true ,
shouldFailTeamWrite : false ,
} ,
{
name : "global maintainer" ,
user : & fleet . User { GlobalRole : ptr . String ( fleet . RoleMaintainer ) } ,
shouldFailGlobalWrite : false ,
shouldFailTeamWrite : false ,
} ,
{
name : "team maintainer" ,
user : & fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 1 } , Role : fleet . RoleMaintainer } } } ,
shouldFailGlobalWrite : true ,
shouldFailTeamWrite : false ,
} ,
{
name : "team admin wrong team" ,
user : & fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 42 } , Role : fleet . RoleAdmin } } } ,
shouldFailGlobalWrite : true ,
shouldFailTeamWrite : true ,
} ,
{
name : "team maintainer wrong team" ,
user : & fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 42 } , Role : fleet . RoleMaintainer } } } ,
shouldFailGlobalWrite : true ,
shouldFailTeamWrite : true ,
} ,
{
name : "global gitops" ,
user : & fleet . User { GlobalRole : ptr . String ( fleet . RoleGitOps ) } ,
shouldFailGlobalWrite : true ,
shouldFailTeamWrite : true ,
} ,
{
name : "team gitops" ,
user : & fleet . User { Teams : [ ] fleet . UserTeam { { Team : fleet . Team { ID : 1 } , Role : fleet . RoleGitOps } } } ,
shouldFailGlobalWrite : true ,
shouldFailTeamWrite : true ,
} ,
}
for _ , tt := range cases {
t . Run ( tt . name , func ( t * testing . T ) {
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
2024-12-30 14:32:48 +00:00
return & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true , WindowsEnabledAndConfigured : true } ,
ServerSettings : fleet . ServerSettings { ScriptsDisabled : true } , // scripts being disabled shouldn't stop lock/unlock/wipe
} , nil
2024-02-13 18:03:53 +00:00
}
ctx := viewer . NewContext ( ctx , viewer . Viewer { User : tt . user } )
2024-06-17 16:30:53 +00:00
_ , err := svc . LockHost ( ctx , globalHostID , false )
2024-02-13 18:03:53 +00:00
checkAuthErr ( t , tt . shouldFailGlobalWrite , err )
2024-06-17 16:30:53 +00:00
_ , err = svc . LockHost ( ctx , teamHostID , false )
2024-02-13 18:03:53 +00:00
checkAuthErr ( t , tt . shouldFailTeamWrite , err )
// Pretend we locked the host
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
return & fleet . HostLockWipeStatus { HostFleetPlatform : host . FleetPlatform ( ) , LockMDMCommand : & fleet . MDMCommand { } , LockMDMCommandResult : & fleet . MDMCommandResult { Status : fleet . MDMAppleStatusAcknowledged } } , nil
2024-02-13 18:03:53 +00:00
}
2024-02-26 16:31:00 +00:00
_ , err = svc . UnlockHost ( ctx , globalHostID )
2024-02-13 18:03:53 +00:00
checkAuthErr ( t , tt . shouldFailGlobalWrite , err )
2024-02-26 16:31:00 +00:00
_ , err = svc . UnlockHost ( ctx , teamHostID )
2024-02-13 18:03:53 +00:00
checkAuthErr ( t , tt . shouldFailTeamWrite , err )
// Reset so we're now pretending host is unlocked
2024-02-26 16:31:00 +00:00
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
2024-02-13 18:03:53 +00:00
return & fleet . HostLockWipeStatus { } , nil
}
2024-02-26 16:31:00 +00:00
2025-06-11 17:56:07 +00:00
err = svc . WipeHost ( ctx , globalHostID , nil )
2024-02-26 16:31:00 +00:00
checkAuthErr ( t , tt . shouldFailGlobalWrite , err )
2025-06-11 17:56:07 +00:00
err = svc . WipeHost ( ctx , teamHostID , nil )
2024-02-26 16:31:00 +00:00
checkAuthErr ( t , tt . shouldFailTeamWrite , err )
2024-02-13 18:03:53 +00:00
} )
}
}
2024-03-11 16:02:51 +00:00
func TestBulkOperationFilterValidation ( t * testing . T ) {
ds := new ( mock . Store )
svc , ctx := newTestService ( t , ds , nil , nil )
viewerCtx := test . UserContext ( ctx , test . UserAdmin )
ds . ListHostsFunc = func ( ctx context . Context , filter fleet . TeamFilter , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
return [ ] * fleet . Host { } , nil
}
ds . ListHostsInLabelFunc = func ( ctx context . Context , filter fleet . TeamFilter , lid uint , opt fleet . HostListOptions ) ( [ ] * fleet . Host , error ) {
return [ ] * fleet . Host { } , nil
}
2024-05-08 20:52:35 +00:00
// TODO(sarah): Future improvement to auto-generate a list of all possible filter values
// from `fleet.HostListOptions` and iterate to test that only a limited subset of filter (i.e.
// label_id, team_id, status, query) are allowed for bulk operations.
2024-03-11 16:02:51 +00:00
tc := [ ] struct {
name string
filters * map [ string ] interface { }
has400Err bool
} {
{
name : "valid status filter" ,
filters : & map [ string ] interface { } {
"status" : "new" ,
} ,
} ,
{
name : "invalid status" ,
filters : & map [ string ] interface { } {
"status" : "invalid" ,
} ,
has400Err : true ,
} ,
{
name : "empty status is invalid" ,
filters : & map [ string ] interface { } {
"status" : "" ,
} ,
has400Err : true ,
} ,
{
name : "valid team filter" ,
filters : & map [ string ] interface { } {
"team_id" : float64 ( 1 ) , // json unmarshals to float64
} ,
} ,
{
name : "invalid team_id type" ,
filters : & map [ string ] interface { } {
"team_id" : "invalid" ,
} ,
has400Err : true ,
} ,
{
name : "valid label_id filter" ,
filters : & map [ string ] interface { } {
"label_id" : float64 ( 1 ) ,
} ,
} ,
{
name : "invalid label_id type" ,
filters : & map [ string ] interface { } {
"label_id" : "invalid" ,
} ,
has400Err : true ,
} ,
{
name : "invalid status type" ,
filters : & map [ string ] interface { } {
"status" : float64 ( 1 ) ,
} ,
has400Err : true ,
} ,
{
name : "empty filter" ,
filters : & map [ string ] interface { } { } ,
} ,
{
name : "valid query filter" ,
filters : & map [ string ] interface { } {
"query" : "test" ,
} ,
} ,
{
name : "invalid query type" ,
filters : & map [ string ] interface { } {
"query" : float64 ( 1 ) ,
} ,
has400Err : true ,
} ,
{
name : "empty query is invalid" ,
filters : & map [ string ] interface { } {
"query" : "" ,
} ,
has400Err : true ,
} ,
{
name : "multiple valid filters" ,
filters : & map [ string ] interface { } {
"status" : "new" ,
"team_id" : float64 ( 1 ) ,
"query" : "test" ,
} ,
} ,
{
name : "mixed valid and invalid filters" ,
filters : & map [ string ] interface { } {
"status" : "new" ,
"team_id" : "invalid" ,
} ,
has400Err : true ,
} ,
{
name : "mixed invalid filters and valid filters (different order)" ,
filters : & map [ string ] interface { } {
"status" : "invalid" ,
"team_id" : 1 ,
} ,
has400Err : true ,
} ,
{
name : "mixed valid and unknown filters" ,
filters : & map [ string ] interface { } {
"status" : "new" ,
"unknown" : "filter" ,
} ,
has400Err : true ,
} ,
{
name : "unknown filter" ,
filters : & map [ string ] interface { } {
"unknown" : "filter" ,
} ,
has400Err : true ,
} ,
}
checkErr := func ( t * testing . T , err error , has400Err bool ) {
if has400Err {
require . Error ( t , err )
var be * fleet . BadRequestError
require . ErrorAs ( t , err , & be )
} else {
require . NoError ( t , err )
}
}
for _ , tt := range tc {
t . Run ( tt . name , func ( t * testing . T ) {
checkErr ( t , svc . AddHostsToTeamByFilter ( viewerCtx , nil , tt . filters ) , tt . has400Err )
checkErr ( t , svc . DeleteHosts ( viewerCtx , nil , tt . filters ) , tt . has400Err )
} )
}
}
2024-07-31 19:59:30 +00:00
func TestSetDiskEncryptionNotifications ( t * testing . T ) {
ds := new ( mock . Store )
ctx := context . Background ( )
2024-08-02 19:06:21 +00:00
svc := & Service { ds : ds , logger : kitlog . NewNopLogger ( ) }
2024-07-31 19:59:30 +00:00
tests := [ ] struct {
name string
host * fleet . Host
appConfig * fleet . AppConfig
diskEncryptionConfigured bool
isConnectedToFleetMDM bool
mdmInfo * fleet . HostMDM
getHostDiskEncryptionKey func ( context . Context , uint ) ( * fleet . HostDiskEncryptionKey , error )
expectedNotifications * fleet . OrbitConfigNotifications
expectedError bool
2024-08-02 19:06:21 +00:00
disableCapability bool
2024-07-31 19:59:30 +00:00
} {
{
name : "no MDM configured" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "darwin" , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : false } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : nil ,
getHostDiskEncryptionKey : nil ,
expectedNotifications : & fleet . OrbitConfigNotifications { } ,
expectedError : false ,
} ,
{
name : "not connected to Fleet MDM" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "darwin" , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : false ,
mdmInfo : nil ,
getHostDiskEncryptionKey : nil ,
expectedNotifications : & fleet . OrbitConfigNotifications { } ,
expectedError : false ,
} ,
{
name : "host not enrolled in osquery" ,
host : & fleet . Host { ID : 1 , Platform : "darwin" , OsqueryHostID : nil } ,
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : nil ,
getHostDiskEncryptionKey : nil ,
expectedNotifications : & fleet . OrbitConfigNotifications { } ,
expectedError : false ,
} ,
{
name : "disk encryption not configured" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "darwin" , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : false ,
isConnectedToFleetMDM : true ,
mdmInfo : nil ,
getHostDiskEncryptionKey : nil ,
expectedNotifications : & fleet . OrbitConfigNotifications { } ,
expectedError : false ,
} ,
{
name : "darwin with decryptable key" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "darwin" , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : nil ,
getHostDiskEncryptionKey : func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey { Decryptable : ptr . Bool ( true ) } , nil
} ,
expectedNotifications : & fleet . OrbitConfigNotifications {
RotateDiskEncryptionKey : false ,
} ,
expectedError : false ,
} ,
2024-08-02 19:06:21 +00:00
{
name : "darwin needs rotation but client is old" ,
host : & fleet . Host { ID : 1 , Platform : "darwin" , OsqueryHostID : ptr . String ( "foo" ) } ,
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : nil ,
getHostDiskEncryptionKey : func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey { Decryptable : ptr . Bool ( false ) } , nil
} ,
expectedNotifications : & fleet . OrbitConfigNotifications {
RotateDiskEncryptionKey : true ,
} ,
expectedError : false ,
disableCapability : true ,
} ,
2024-08-01 17:04:54 +00:00
{
name : "darwin needs rotation" ,
host : & fleet . Host { ID : 1 , Platform : "darwin" , OsqueryHostID : ptr . String ( "foo" ) } ,
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : nil ,
getHostDiskEncryptionKey : func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey { Decryptable : ptr . Bool ( false ) } , nil
} ,
expectedNotifications : & fleet . OrbitConfigNotifications {
RotateDiskEncryptionKey : true ,
} ,
expectedError : false ,
} ,
2024-07-31 19:59:30 +00:00
{
name : "windows server with no encryption needed" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "windows" , DiskEncryptionEnabled : ptr . Bool ( true ) , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : & fleet . HostMDM { IsServer : true } ,
getHostDiskEncryptionKey : func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return nil , newNotFoundError ( )
} ,
expectedNotifications : & fleet . OrbitConfigNotifications {
EnforceBitLockerEncryption : false ,
} ,
expectedError : false ,
} ,
{
name : "windows with encryption enabled but key missing" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "windows" , DiskEncryptionEnabled : ptr . Bool ( true ) , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : & fleet . HostMDM { IsServer : false } ,
getHostDiskEncryptionKey : func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return nil , newNotFoundError ( )
} ,
expectedNotifications : & fleet . OrbitConfigNotifications {
EnforceBitLockerEncryption : true ,
} ,
expectedError : false ,
} ,
{
name : "darwin with missing encryption key" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "darwin" , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : nil ,
getHostDiskEncryptionKey : func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return nil , newNotFoundError ( )
} ,
expectedNotifications : & fleet . OrbitConfigNotifications {
RotateDiskEncryptionKey : false ,
} ,
expectedError : false ,
} ,
{
name : "windows with encryption key and not decryptable" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "windows" , DiskEncryptionEnabled : ptr . Bool ( true ) , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : & fleet . HostMDM { IsServer : false } ,
getHostDiskEncryptionKey : func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return & fleet . HostDiskEncryptionKey { Decryptable : ptr . Bool ( false ) } , nil
} ,
expectedNotifications : & fleet . OrbitConfigNotifications {
EnforceBitLockerEncryption : true ,
} ,
expectedError : false ,
} ,
{
name : "windows with enforce BitLocker" ,
2024-08-01 17:04:54 +00:00
host : & fleet . Host { ID : 1 , Platform : "windows" , DiskEncryptionEnabled : ptr . Bool ( false ) , OsqueryHostID : ptr . String ( "foo" ) } ,
2024-07-31 19:59:30 +00:00
appConfig : & fleet . AppConfig {
MDM : fleet . MDM { EnabledAndConfigured : true } ,
} ,
diskEncryptionConfigured : true ,
isConnectedToFleetMDM : true ,
mdmInfo : & fleet . HostMDM { IsServer : false } ,
getHostDiskEncryptionKey : func ( ctx context . Context , id uint ) ( * fleet . HostDiskEncryptionKey , error ) {
return nil , newNotFoundError ( )
} ,
expectedNotifications : & fleet . OrbitConfigNotifications {
EnforceBitLockerEncryption : true ,
} ,
expectedError : false ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
if tt . getHostDiskEncryptionKey != nil {
ds . GetHostDiskEncryptionKeyFunc = tt . getHostDiskEncryptionKey
}
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return tt . appConfig , nil
}
2025-07-02 19:57:25 +00:00
ds . GetHostArchivedDiskEncryptionKeyFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostArchivedDiskEncryptionKey , error ) {
return & fleet . HostArchivedDiskEncryptionKey { } , nil
}
2024-07-31 19:59:30 +00:00
2024-08-02 19:06:21 +00:00
if ! tt . disableCapability {
r := http . Request {
Header : http . Header { fleet . CapabilitiesHeader : [ ] string { string ( fleet . CapabilityEscrowBuddy ) } } ,
}
ctx = capabilities . NewContext ( ctx , & r )
}
2024-07-31 19:59:30 +00:00
notifs := & fleet . OrbitConfigNotifications { }
2025-07-29 23:22:36 +00:00
err := svc . setDiskEncryptionNotifications (
ctx ,
notifs ,
tt . host ,
tt . appConfig ,
tt . diskEncryptionConfigured ,
tt . isConnectedToFleetMDM ,
tt . mdmInfo ,
)
2024-07-31 19:59:30 +00:00
if tt . expectedError {
require . Error ( t , err )
} else {
require . NoError ( t , err )
}
require . Equal ( t , tt . expectedNotifications . RotateDiskEncryptionKey , notifs . RotateDiskEncryptionKey )
} )
}
}
2025-08-19 14:38:53 +00:00
func TestGetHostDetailsExcludeSoftwareFlag ( t * testing . T ) {
ds := new ( mock . Store )
svc := & Service { ds : ds }
baseHost := & fleet . Host { ID : 42 }
// common DS mocks
ds . AppConfigFunc = func ( ctx context . Context ) ( * fleet . AppConfig , error ) {
return & fleet . AppConfig { } , nil
}
ds . ListLabelsForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Label , error ) {
return nil , nil
}
ds . ListPacksForHostFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . Pack , error ) {
return nil , nil
}
ds . ListPoliciesForHostFunc = func ( ctx context . Context , host * fleet . Host ) ( [ ] * fleet . HostPolicy , error ) {
return nil , nil
}
ds . ListHostBatteriesFunc = func ( ctx context . Context , hostID uint ) ( [ ] * fleet . HostBattery , error ) {
return nil , nil
}
ds . ListUpcomingHostMaintenanceWindowsFunc = func ( ctx context . Context , hid uint ) ( [ ] * fleet . HostMaintenanceWindow , error ) {
return nil , nil
}
ds . GetHostLockWipeStatusFunc = func ( ctx context . Context , host * fleet . Host ) ( * fleet . HostLockWipeStatus , error ) {
return & fleet . HostLockWipeStatus { } , nil
}
ds . ScimUserByHostIDFunc = func ( ctx context . Context , hostID uint ) ( * fleet . ScimUser , error ) {
return nil , nil
}
ds . ListHostDeviceMappingFunc = func ( ctx context . Context , id uint ) ( [ ] * fleet . HostDeviceMapping , error ) {
return nil , nil
}
ds . IsHostDiskEncryptionKeyArchivedFunc = func ( ctx context . Context , hostID uint ) ( bool , error ) {
return false , nil
}
t . Run ( "ExcludeSoftware=true returns empty slice" , func ( t * testing . T ) {
ds . LoadHostSoftwareFuncInvoked = false
ds . LoadHostSoftwareFunc = func ( ctx context . Context , h * fleet . Host , includeCVEScores bool ) error {
t . Fatalf ( "LoadHostSoftwareFunc should not be called when ExcludeSoftware is true" )
return nil
}
opts := fleet . HostDetailOptions { ExcludeSoftware : true }
hostDetail , err := svc . getHostDetails ( test . UserContext ( context . Background ( ) , test . UserAdmin ) , baseHost , opts )
require . NoError ( t , err )
require . NotNil ( t , hostDetail . Software , "Software slice should not be nil" )
assert . Len ( t , hostDetail . Software , 0 , "Software slice should be empty when excluded" )
} )
t . Run ( "ExcludeSoftware=false returns filled slice" , func ( t * testing . T ) {
expectedSoftware := [ ] fleet . HostSoftwareEntry {
{
Software : fleet . Software {
ID : 1 ,
Name : "test-app" ,
Version : "1.0.0" ,
Source : "apps" ,
} ,
InstalledPaths : [ ] string { "/Applications/test-app.app" } ,
} ,
{
Software : fleet . Software {
ID : 2 ,
Name : "another-app" ,
Version : "2.3.4" ,
Source : "apps" ,
} ,
InstalledPaths : [ ] string { "/Applications/another-app.app" } ,
} ,
}
ds . LoadHostSoftwareFuncInvoked = false
ds . LoadHostSoftwareFunc = func ( ctx context . Context , h * fleet . Host , includeCVEScores bool ) error {
h . HostSoftware . Software = expectedSoftware
return nil
}
opts := fleet . HostDetailOptions { ExcludeSoftware : false }
hostDetail , err := svc . getHostDetails ( test . UserContext ( context . Background ( ) , test . UserAdmin ) , baseHost , opts )
require . NoError ( t , err )
require . NotNil ( t , hostDetail . Software )
assert . Equal ( t , expectedSoftware , hostDetail . Software )
assert . True ( t , ds . LoadHostSoftwareFuncInvoked , "LoadHostSoftwareFunc should have been called" )
} )
}