add API endpoint to see disk encryption key (#9713)

https://github.com/fleetdm/fleet/issues/8708
This commit is contained in:
Roberto Dip
2023-02-08 20:20:23 -03:00
committed by GitHub
parent 275a5993a8
commit 7cd581866a
23 changed files with 661 additions and 124 deletions
+1
View File
@@ -0,0 +1 @@
* Added an API endpoint to retrieve a host disk encryption key for macOS if Fleet's MDM is enabled.
+4 -4
View File
@@ -1007,8 +1007,8 @@ func TestVerifyDiskEncryptionKeysJob(t *testing.T) {
now := time.Now()
t.Run("able to decrypt", func(t *testing.T) {
ds.GetUnverifiedDiskEncryptionKeysFunc = func(ctx context.Context) ([]fleet.DiskEncryptionKey, error) {
return []fleet.DiskEncryptionKey{
ds.GetUnverifiedDiskEncryptionKeysFunc = func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
return []fleet.HostDiskEncryptionKey{
{HostID: 1, Base64Encrypted: base64EncryptedKey, UpdatedAt: now},
{HostID: 2, Base64Encrypted: base64EncryptedKey, UpdatedAt: now.Add(time.Hour)},
{HostID: 3, Base64Encrypted: "BAD-KEY", UpdatedAt: now.Add(-time.Hour)},
@@ -1039,8 +1039,8 @@ func TestVerifyDiskEncryptionKeysJob(t *testing.T) {
})
t.Run("unable to decrypt", func(t *testing.T) {
ds.GetUnverifiedDiskEncryptionKeysFunc = func(ctx context.Context) ([]fleet.DiskEncryptionKey, error) {
return []fleet.DiskEncryptionKey{{HostID: 1, Base64Encrypted: "RANDOM"}}, nil
ds.GetUnverifiedDiskEncryptionKeysFunc = func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
return []fleet.HostDiskEncryptionKey{{HostID: 1, Base64Encrypted: "RANDOM"}}, nil
}
calls := 0
@@ -39,6 +39,7 @@
"config_tls_refresh": 0,
"logger_tls_period": 0,
"mdm": {
"encryption_key_available": false,
"enrollment_status": null,
"server_url": null
},
@@ -30,6 +30,7 @@ spec:
last_enrolled_at: "0001-01-01T00:00:00Z"
logger_tls_period: 0
mdm:
encryption_key_available: false
enrollment_status: null
server_url: null
memory: 0
+2
View File
@@ -40,6 +40,7 @@
"config_tls_refresh": 0,
"logger_tls_period": 0,
"mdm": {
"encryption_key_available": false,
"enrollment_status": null,
"server_url": null
},
@@ -106,6 +107,7 @@
"config_tls_refresh": 0,
"logger_tls_period": 0,
"mdm": {
"encryption_key_available": false,
"enrollment_status": null,
"server_url": null
},
+2
View File
@@ -34,6 +34,7 @@ spec:
last_enrolled_at: "0001-01-01T00:00:00Z"
logger_tls_period: 0
mdm:
encryption_key_available: false
enrollment_status: null
server_url: null
memory: 0
@@ -87,6 +88,7 @@ spec:
last_enrolled_at: "0001-01-01T00:00:00Z"
logger_tls_period: 0
mdm:
encryption_key_available: false
enrollment_status: null
server_url: null
memory: 0
+17
View File
@@ -583,6 +583,23 @@ This activity contains the following fields:
}
```
### Type `read_host_disk_encryption_key`
Generated when a user reads the disk encryption key for a host.
This activity contains the following fields:
- "host_id": ID of the host.
- "host_display_name": Display name of the host.
#### Example
```json
{
"host_id": 1,
"host_display_name": "Anna's MacBook Pro",
}
```
<meta name="pageOrderInSection" value="1400">
+62 -65
View File
@@ -6,48 +6,45 @@ Users with the Admin role receive all permissions.
## User permissions
| **Action** | Observer | Maintainer | Admin |
| ---------------------------------------------------- | -------- | ---------- | ----- |
| View all [activity](https://fleetdm.com/docs/using-fleet/rest-api#activities) | ✅ | ✅ | ✅ |
| View all hosts | ✅ | ✅ | ✅ |
| **Action** | Observer | Maintainer | Admin |
| ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ---------- | ----- |
| View all [activity](https://fleetdm.com/docs/using-fleet/rest-api#activities) | ✅ | ✅ | ✅ |
| View all hosts | ✅ | ✅ | ✅ |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ |
| Target hosts using labels | ✅ | ✅ | ✅ |
| Add and delete hosts | | ✅ | ✅ |
| Transfer hosts between teams\* | | ✅ | ✅ |
| Create, edit, and delete labels | | ✅ | ✅ |
| View all software | ✅ | ✅ | ✅ |
| Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ |
| Filter hosts by software | ✅ | ✅ | ✅ |
| Filter software by team\* | ✅ | ✅ | ✅ |
| Manage [vulnerability automations](https://fleetdm.com/docs/using-fleet/automations#vulnerability-automations) | | | ✅ |
| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ |
| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ |
| Create, edit, and delete queries | | ✅ | ✅ |
| View all queries | ✅ | ✅ | ✅ |
| Add, edit, and remove queries from all schedules | | ✅ | ✅ |
| Create, edit, view, and delete packs | | ✅ | ✅ |
| View all policies | ✅ | ✅ | ✅ |
| Filter hosts using policies | ✅ | ✅ | ✅ |
| Create, edit, and delete policies for all hosts | | ✅ | ✅ |
| Create, edit, and delete policies for all hosts assigned to team\* | | ✅ | ✅ |
| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | ✅ |
| Create, edit, view, and delete users | | | ✅ |
| Add and remove team members\* | | | ✅ |
| Create, edit, and delete teams\* | | | ✅ |
| Create, edit, and delete [enroll secrets](https://fleetdm.com/docs/deploying/faq#when-do-i-need-to-deploy-a-new-enroll-secret-to-my-hosts) | | ✅ | ✅ |
| Create, edit, and delete [enroll secrets for teams](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team)\* | | ✅ | ✅ |
| Edit [organization settings](https://fleetdm.com/docs/using-fleet/configuration-files#organization-settings) | | | ✅ |
| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | ✅ |
| Edit [agent options for hosts assigned to teams](https://fleetdm.com/docs/using-fleet/configuration-files#team-agent-options)\* | | | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
| Retrieve contents from file carving | | | ✅ |
| View Apple mobile device management (MDM) certificate information | | | ✅ |
| View Apple business manager (BM) information | | | ✅ |
| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | ✅ |
| Target hosts using labels | ✅ | ✅ | ✅ |
| Add and delete hosts | | ✅ | ✅ |
| Transfer hosts between teams\* | | ✅ | ✅ |
| Create, edit, and delete labels | | ✅ | ✅ |
| View all software | ✅ | ✅ | ✅ |
| Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ |
| Filter hosts by software | ✅ | ✅ | ✅ |
| Filter software by team\* | ✅ | ✅ | ✅ |
| Manage [vulnerability automations](https://fleetdm.com/docs/using-fleet/automations#vulnerability-automations) | | | ✅ |
| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ |
| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ |
| Create, edit, and delete queries | | ✅ | ✅ |
| View all queries | ✅ | ✅ | ✅ |
| Add, edit, and remove queries from all schedules | | ✅ | ✅ |
| Create, edit, view, and delete packs | | ✅ | ✅ |
| View all policies | ✅ | ✅ | ✅ |
| Filter hosts using policies | ✅ | ✅ | ✅ |
| Create, edit, and delete policies for all hosts | | ✅ | ✅ |
| Create, edit, and delete policies for all hosts assigned to team\* | | ✅ | ✅ |
| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | ✅ |
| Create, edit, view, and delete users | | | ✅ |
| Add and remove team members\* | | | ✅ |
| Create, edit, and delete teams\* | | | ✅ |
| Create, edit, and delete [enroll secrets](https://fleetdm.com/docs/deploying/faq#when-do-i-need-to-deploy-a-new-enroll-secret-to-my-hosts) | | ✅ | ✅ |
| Create, edit, and delete [enroll secrets for teams](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team)\* | | ✅ | ✅ |
| Edit [organization settings](https://fleetdm.com/docs/using-fleet/configuration-files#organization-settings) | | | ✅ |
| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | ✅ |
| Edit [agent options for hosts assigned to teams](https://fleetdm.com/docs/using-fleet/configuration-files#team-agent-options)\* | | | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
| Retrieve contents from file carving | | | ✅ |
| View Apple mobile device management (MDM) certificate information | | | ✅ |
| View Apple business manager (BM) information | | | ✅ |
| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | ✅ |
| View disk encryption key for macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ |
\*Applies only to Fleet Premium
@@ -68,29 +65,29 @@ Users can be a member of multiple teams in Fleet.
Users that are members of multiple teams can be assigned different roles for each team. For example, a user can be given access to the "Workstations" team and assigned the "Observer" role. This same user can be given access to the "Servers" team and assigned the "Maintainer" role.
| **Action** | Team observer | Team maintainer | Team admin |
| ------------------------------------------------------------ | -------- | ---------- | ------- |
| View hosts | ✅ | ✅ | ✅ |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ |
| Target hosts using labels | ✅ | ✅ | ✅ |
| Add and delete hosts | | ✅ | ✅ |
| Filter software by [vulnerabilities]((https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing)) | ✅ | ✅ | ✅ |
| Filter hosts by software | ✅ | ✅ | ✅ |
| Filter software | ✅ | ✅ | ✅ |
| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ |
| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) | | ✅ | ✅ |
| Create, edit, and delete only **self authored** queries | | ✅ | ✅ |
| Add, edit, and remove queries from the schedule | | ✅ | ✅ |
| View policies | ✅ | ✅ | ✅ |
| View global (inherited) policies | ✅ | ✅ | ✅ |
| Filter hosts using policies | ✅ | ✅ | ✅ |
| Create, edit, and delete policies | | ✅ | ✅ |
| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | ✅ |
| Add and remove team members | | | ✅ |
| Edit team name | | | ✅ |
| Create, edit, and delete [team enroll secrets](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team) | | ✅ | ✅ |
| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
| **Action** | Team observer | Team maintainer | Team admin |
| -------------------------------------------------------------------------------------------------------------------------------- | ------------- | --------------- | ---------- |
| View hosts | ✅ | ✅ | ✅ |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ |
| Target hosts using labels | ✅ | ✅ | ✅ |
| Add and delete hosts | | ✅ | ✅ |
| Filter software by [vulnerabilities](<(https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing)>) | ✅ | ✅ | ✅ |
| Filter hosts by software | ✅ | ✅ | ✅ |
| Filter software | ✅ | ✅ | ✅ |
| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ |
| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) | | ✅ | ✅ |
| Create, edit, and delete only **self authored** queries | | ✅ | ✅ |
| Add, edit, and remove queries from the schedule | | ✅ | ✅ |
| View policies | ✅ | ✅ | ✅ |
| View global (inherited) policies | ✅ | ✅ | ✅ |
| Filter hosts using policies | ✅ | ✅ | ✅ |
| Create, edit, and delete policies | | ✅ | ✅ |
| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | ✅ |
| Add and remove team members | | | ✅ |
| Edit team name | | | ✅ |
| Create, edit, and delete [team enroll secrets](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team) | | ✅ | ✅ |
| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | ✅ |
| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
| View disk encryption key for macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ |
<meta name="pageOrderInSection" value="900">
+40
View File
@@ -1723,6 +1723,7 @@ None.
- [Get aggregated host's mobile device management (MDM) and Munki information](#get-aggregated-hosts-macadmin-mobile-device-management-mdm-and-munki-information)
- [Get host OS versions](#get-host-os-versions)
- [Get hosts report in CSV](#get-hosts-report-in-csv)
- [Get host's disk encryption key](#get-hosts-disk-encryption-key)
### On the different timestamps in the host data structure
@@ -1864,6 +1865,7 @@ If `after` is being used with `created_at` or `updated_at`, the table must be sp
}
},
"mdm": {
"encryption_key_available": false,
"enrollment_status": null,
"server_url": null
}
@@ -2243,6 +2245,7 @@ Returns the information of the specified host.
}
},
"mdm": {
"encryption_key_available": false,
"enrollment_status": null,
"server_url": null
}
@@ -2421,6 +2424,7 @@ Returns the information of the host specified using the `uuid`, `osquery_host_id
"display_text": "dogfood-ubuntu-box",
"display_name": "dogfood-ubuntu-box",
"mdm": {
"encryption_key_available": false,
"enrollment_status": null,
"server_url": null
}
@@ -2989,6 +2993,42 @@ created_at,updated_at,id,detail_updated_at,label_updated_at,policy_updated_at,la
2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,3,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:21:56Z,false,foo.local2,48ebe4b0-39c3-4a74-a67f-308f7b5dd171,linux,,,,,,0s,0,,,,0,0,,,,,,,,,0,0,0,,,0,0,0,,,,
```
### Get host's disk encryption key
Requires the [macadmins osquery extension](https://github.com/macadmins/osquery-extension) which comes bundled
in [Fleet's osquery installers](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
Requires Fleet's MDM properly [enabled and configured](./Mobile-device-management.md).
Retrieves the disk encryption key for a host.
`GET /api/v1/fleet/hosts/:id/encryption_key`
#### Parameters
| Name | Type | In | Description |
| ---- | ------- | ---- | ------------------------------------------------------------------ |
| id | integer | path | **Required** The id of the host to get the disk encryption key for |
#### Example
`GET /api/v1/fleet/hosts/8/encryption_key`
##### Default response
`Status: 200`
```json
{
"host_id": 8,
"encryption_key": {
"key": "5ADZ-HTZ8-LJJ4-B2F8-JWH3-YPBT",
"updated_at": "2022-12-01T05:31:43Z"
}
}
```
---
+36 -3
View File
@@ -429,6 +429,7 @@ FROM
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
LEFT JOIN host_mdm hmdm on hmdm.host_id = h.id
LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
JOIN (
SELECT
count(*) as count
@@ -490,6 +491,16 @@ const hostMDMSelect = `,
CASE
WHEN hmdm.is_server = 1 THEN NULL
ELSE hmdm.server_url
END,
'encryption_key_available',
CASE
/* roberto: this is the only way I have found for MySQL to
* return true and false instead of 0 and 1 in the JSON, the
* unmarshaller was having problems converting int values to
* booleans.
*/
WHEN hdek.decryptable IS NULL OR hdek.decryptable = 0 THEN CAST(FALSE AS JSON)
ELSE CAST(TRUE AS JSON)
END
) mdm_host_data
`
@@ -685,7 +696,8 @@ func (ds *Datastore) applyHostFilters(opt fleet.HostListOptions, sql string, fil
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN teams t ON (h.team_id = t.id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
LEFT JOIN host_mdm hmdm ON hmdm.host_id = h.id
LEFT JOIN host_mdm hmdm ON hmdm.host_id = h.id
LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
%s
%s
%s
@@ -1485,6 +1497,7 @@ func (ds *Datastore) SearchHosts(ctx context.Context, filter fleet.TeamFilter, m
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
LEFT JOIN host_mdm hmdm on hmdm.host_id = h.id
LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
WHERE TRUE`
var args []interface{}
@@ -1592,6 +1605,7 @@ func (ds *Datastore) HostByIdentifier(ctx context.Context, identifier string) (*
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
LEFT JOIN host_mdm hmdm ON hmdm.host_id = h.id
LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
WHERE ? IN (h.hostname, h.osquery_host_id, h.node_key, h.uuid)
LIMIT 1
`
@@ -2348,8 +2362,8 @@ func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostI
}
func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.DiskEncryptionKey, error) {
var keys []fleet.DiskEncryptionKey
func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
var keys []fleet.HostDiskEncryptionKey
err := sqlx.SelectContext(ctx, ds.reader, &keys, `
SELECT
base64_encrypted,
@@ -2384,6 +2398,25 @@ func (ds *Datastore) SetHostsDiskEncryptionKeyStatus(
return err
}
func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) {
var key fleet.HostDiskEncryptionKey
err := sqlx.GetContext(ctx, ds.reader, &key, `
SELECT
host_id, base64_encrypted, decryptable, updated_at
FROM
host_disk_encryption_keys
WHERE host_id = ?`, hostID)
if err != nil {
if err == sql.ErrNoRows {
msg := fmt.Sprintf("for host %d", hostID)
return nil, ctxerr.Wrap(ctx, notFound("HostDiskEncryptionKey").WithMessage(msg))
}
return nil, ctxerr.Wrapf(ctx, err, "getting data from host_mdm for host_id %d", hostID)
}
return &key, nil
}
func (ds *Datastore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {
return ds.updateOrInsert(
ctx,
+3 -14
View File
@@ -6034,20 +6034,9 @@ func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) {
require.NoError(t, err)
checkEncryptionKey := func(hostID uint, expected string) {
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
var actual string
row := tx.QueryRowxContext(
context.Background(),
"SELECT base64_encrypted FROM host_disk_encryption_keys WHERE host_id = ?",
hostID,
)
err := row.Scan(&actual)
require.NoError(t, err)
require.Equal(t, expected, actual)
return nil
})
actual, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID)
require.NoError(t, err)
require.Equal(t, expected, actual.Base64Encrypted)
}
h, err := ds.Host(context.Background(), host.ID)
+21
View File
@@ -47,6 +47,8 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeMDMUnenrolled{},
ActivityTypeEditedMacOSMinVersion{},
ActivityTypeReadHostDiskEncryptionKey{},
}
type ActivityDetails interface {
@@ -708,3 +710,22 @@ func (a ActivityTypeEditedMacOSMinVersion) Documentation() (activity string, det
"deadline": "2023-06-01"
}`
}
type ActivityTypeReadHostDiskEncryptionKey struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
}
func (a ActivityTypeReadHostDiskEncryptionKey) ActivityName() string {
return "read_host_disk_encryption_key"
}
func (a ActivityTypeReadHostDiskEncryptionKey) Documentation() (activity string, details string, detailsExample string) {
return `Generated when a user reads the disk encryption key for a host.`,
`This activity contains the following fields:
- "host_id": ID of the host.
- "host_display_name": Display name of the host.`, `{
"host_id": 1,
"host_display_name": "Anna's MacBook Pro",
}`
}
+4 -2
View File
@@ -642,10 +642,12 @@ type Datastore interface {
// GetUnverifiedDiskEncryptionKeys returns all the encryption keys that
// are collected but their decryptable status is not known yet (ie:
// we're able to decrypt the key using a private key in the server)
GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]DiskEncryptionKey, error)
// SetHostsDiskEncryptionKeyStatus sets the encryptable status for the set
GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]HostDiskEncryptionKey, error)
// SetHostDiskEncryptionKeyStatus sets the encryptable status for the set
// of encription keys provided
SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error
// GetHostDiskEncryptionKey returns the encryption key information for a given host
GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*HostDiskEncryptionKey, error)
// SetOrUpdateHostOrbitInfo inserts of updates the orbit info for a host
SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error
+9 -5
View File
@@ -220,6 +220,9 @@ type MDMHostData struct {
// ServerURL is the server_url stored in the host_mdm table, loaded by
// JOIN in datastore
ServerURL *string `json:"server_url" db:"-" csv:"mdm.server_url"`
// EncryptionKeyAvailable indicates if Fleet was able to retrieve and
// decode an encryption key for the host.
EncryptionKeyAvailable bool `json:"encryption_key_available" db:"-" csv:"-"`
}
// Scan implements the Scanner interface for sqlx, to support unmarshaling a
@@ -640,9 +643,10 @@ type HostMDMCheckinInfo struct {
DisplayName string `json:"display_name" db:"display_name"`
}
type DiskEncryptionKey struct {
HostID uint `db:"host_id"`
Base64Encrypted string `db:"base64_encrypted"`
Decryptable *bool `db:"decryptable"`
UpdatedAt time.Time `db:"updated_at"`
type HostDiskEncryptionKey struct {
HostID uint `json:"-" db:"host_id"`
Base64Encrypted string `json:"-" db:"base64_encrypted"`
Decryptable *bool `json:"-" db:"decryptable"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
DecryptedValue string `json:"key" db:"-"`
}
+2
View File
@@ -324,6 +324,8 @@ type Service interface {
GetMDMSolution(ctx context.Context, mdmID uint) (*MDMSolution, error)
GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*MunkiIssue, error)
HostEncryptionKey(ctx context.Context, id uint) (*HostDiskEncryptionKey, error)
// OSVersions returns a list of operating systems and associated host counts, which may be
// filtered using the following optional criteria: team id, platform, or name and version.
// Name cannot be used without version, and conversely, version cannot be used without name.
+12 -2
View File
@@ -464,10 +464,12 @@ type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, e
type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string) error
type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.DiskEncryptionKey, error)
type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error)
type SetHostsDiskEncryptionKeyStatusFunc func(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error
type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error)
type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string) error
type ReplaceHostDeviceMappingFunc func(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping) error
@@ -1216,6 +1218,9 @@ type DataStore struct {
SetHostsDiskEncryptionKeyStatusFunc SetHostsDiskEncryptionKeyStatusFunc
SetHostsDiskEncryptionKeyStatusFuncInvoked bool
GetHostDiskEncryptionKeyFunc GetHostDiskEncryptionKeyFunc
GetHostDiskEncryptionKeyFuncInvoked bool
SetOrUpdateHostOrbitInfoFunc SetOrUpdateHostOrbitInfoFunc
SetOrUpdateHostOrbitInfoFuncInvoked bool
@@ -2441,7 +2446,7 @@ func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID
return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key)
}
func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.DiskEncryptionKey, error) {
func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
s.GetUnverifiedDiskEncryptionKeysFuncInvoked = true
return s.GetUnverifiedDiskEncryptionKeysFunc(ctx)
}
@@ -2451,6 +2456,11 @@ func (s *DataStore) SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs
return s.SetHostsDiskEncryptionKeyStatusFunc(ctx, hostIDs, encryptable, threshold)
}
func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) {
s.GetHostDiskEncryptionKeyFuncInvoked = true
return s.GetHostDiskEncryptionKeyFunc(ctx, hostID)
}
func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {
s.SetOrUpdateHostOrbitInfoFuncInvoked = true
return s.SetOrUpdateHostOrbitInfoFunc(ctx, hostID, version)
+1
View File
@@ -437,6 +437,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.GET("/api/_version_/fleet/mdm/apple/installers", listMDMAppleInstallersEndpoint, listMDMAppleInstallersRequest{})
ue.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{})
ue.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{})
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
// host-specific mdm commands
ue.PATCH("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/unenroll", mdmAppleCommandRemoveEnrollmentProfileEndpoint, mdmAppleCommandRemoveEnrollmentProfileRequest{})
+85 -6
View File
@@ -11,12 +11,14 @@ import (
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/authz"
authzctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/gocarina/gocsv"
)
@@ -388,7 +390,7 @@ func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service
}
func (svc *Service) GetHost(ctx context.Context, id uint, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken)
alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken)
if !alreadyAuthd {
// First ensure the user has access to list hosts, then check the specific
// host once team_id is loaded.
@@ -723,7 +725,7 @@ func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}
func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return err
}
@@ -871,7 +873,7 @@ func listHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc
}
func (svc *Service) ListHostDeviceMapping(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
@@ -963,7 +965,7 @@ func getMacadminsDataEndpoint(ctx context.Context, request interface{}, svc flee
}
func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.MacadminsData, error) {
if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
@@ -1250,7 +1252,7 @@ func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
// for now, only csv format is allowed
if req.Format != "csv" {
// prevent returning an "unauthorized" error, we want that specific error
if az, ok := authz.FromContext(ctx); ok {
if az, ok := authzctx.FromContext(ctx); ok {
az.SetChecked()
}
err := ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("format", "unsupported or unspecified report format").
@@ -1365,3 +1367,80 @@ func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *stri
return osVersions, nil
}
////////////////////////////////////////////////////////////////////////////////
// Encryption Key
////////////////////////////////////////////////////////////////////////////////
type getHostEncryptionKeyRequest struct {
ID uint `url:"id"`
}
type getHostEncryptionKeyResponse struct {
Err error `json:"error,omitempty"`
EncryptionKey *fleet.HostDiskEncryptionKey `json:"encryption_key,omitempty"`
HostID uint `json:"host_id,omitempty"`
}
func (r getHostEncryptionKeyResponse) error() error { return r.Err }
func getHostEncryptionKey(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*getHostEncryptionKeyRequest)
key, err := svc.HostEncryptionKey(ctx, req.ID)
if err != nil {
return getHostEncryptionKeyResponse{Err: err}, nil
}
return getHostEncryptionKeyResponse{EncryptionKey: key, HostID: req.ID}, nil
}
func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
host, err := svc.ds.HostLite(ctx, id)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
}
// Permissions to read encryption keys are exactly the same
// as the ones required to read hosts.
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return nil, err
}
key, err := svc.ds.GetHostDiskEncryptionKey(ctx, id)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
}
if key.Decryptable == nil || !*key.Decryptable {
return nil, ctxerr.Wrap(ctx, notFoundError{}, "getting host encryption key")
}
cert, _, _, err := svc.config.MDM.AppleSCEP()
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
}
decryptedKey, err := apple_mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
}
key.DecryptedValue = string(decryptedKey)
err = svc.ds.NewActivity(
ctx,
authz.UserFromContext(ctx),
fleet.ActivityTypeReadHostDiskEncryptionKey{
HostID: host.ID,
HostDisplayName: host.DisplayName(),
},
)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "create read host disk encryption key activity")
}
return key, nil
}
+164
View File
@@ -2,6 +2,8 @@ package service
import (
"context"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"testing"
@@ -9,14 +11,19 @@ import (
"github.com/WatchBeam/clock"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
nanodep_client "github.com/micromdm/nanodep/client"
"github.com/micromdm/nanodep/tokenpki"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mozilla.org/pkcs7"
)
func TestHostDetails(t *testing.T) {
@@ -508,3 +515,160 @@ func TestEmptyTeamOSVersions(t *testing.T) {
require.Error(t, err)
require.Equal(t, "some unknown error", fmt.Sprint(err))
}
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,
},
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,
test.UserTeamAdminTeam1,
test.UserTeamMaintainerTeam1,
test.UserTeamObserverTeam1,
},
disallowedUsers: []*fleet.User{
test.UserTeamAdminTeam2,
test.UserTeamMaintainerTeam2,
test.UserTeamObserverTeam2,
test.UserNoRoles,
},
},
}
testBMToken := &nanodep_client.OAuth1Tokens{
ConsumerKey: "test_consumer",
ConsumerSecret: "test_secret",
AccessToken: "test_access_token",
AccessSecret: "test_access_secret",
AccessTokenExpiry: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
}
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, testBMToken)
recoveryKey := "AAA-BBB-CCC"
encryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{testCert})
require.NoError(t, err)
base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey)
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
ds := new(mock.Store)
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
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
act := activity.(fleet.ActivityTypeReadHostDiskEncryptionKey)
require.Equal(t, tt.host.ID, act.HostID)
require.EqualValues(t, act.HostDisplayName, tt.host.DisplayName())
return nil
}
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)
svc, ctx := newTestService(t, ds, nil, nil)
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
}
_, 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
}
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
return errors.New("activity error")
}
_, err = svc.HostEncryptionKey(ctx, 1)
require.Error(t, err)
})
}
@@ -2257,28 +2257,6 @@ func (s *integrationEnterpriseTestSuite) TestOrbitConfigNudgeSettings() {
require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 04:00:00 +0000 UTC")
}
// gets the latest activity and checks that it matches any provided properties.
// empty string or 0 id means do not check that property. It returns the ID of that
// latest activity.
func (s *integrationEnterpriseTestSuite) lastActivityMatches(name, details string, id uint) uint {
var listActivities listActivitiesResponse
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "1")
require.True(s.T(), len(listActivities.Activities) > 0)
act := listActivities.Activities[0]
if name != "" {
assert.Equal(s.T(), name, act.Type)
}
if details != "" {
require.NotNil(s.T(), act.Details)
assert.JSONEq(s.T(), details, string(*act.Details))
}
if id > 0 {
assert.Equal(s.T(), id, act.ID)
}
return act.ID
}
// allEqual compares all fields of a struct.
// If a field is a pointer on one side but not on the other, then it follows that pointer. This is useful for optional
// arguments.
+170
View File
@@ -31,8 +31,10 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/mock"
"github.com/fleetdm/fleet/v4/server/service/schedule"
"github.com/fleetdm/fleet/v4/server/test"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/google/uuid"
@@ -587,6 +589,174 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() {
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusOK)
}
func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() {
t := s.T()
ctx := context.Background()
// create a host
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name()),
NodeKey: ptr.String(t.Name()),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: "darwin",
})
require.NoError(t, err)
// add an encryption key for the host
cert, _, _, err := s.fleetCfg.MDM.AppleSCEP()
require.NoError(t, err)
parsed, err := x509.ParseCertificate(cert.Certificate[0])
require.NoError(t, err)
recoveryKey := "AAA-BBB-CCC"
encryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{parsed})
require.NoError(t, err)
base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey)
err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64EncryptedKey)
require.NoError(t, err)
// request with no token
res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusUnauthorized)
res.Body.Close()
// encryption key not processed yet
resp := getHostEncryptionKeyResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp)
// unable to decrypt encryption key
err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, false, time.Now())
require.NoError(t, err)
resp = getHostEncryptionKeyResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp)
// no activities created so far
activities := listActivitiesResponse{}
s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)
found := false
for _, activity := range activities.Activities {
if activity.Type == "read_host_disk_encryption_key" {
found = true
}
}
require.False(t, found)
// decryptable key
checkDecryptableKey := func(u fleet.User) {
err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, time.Now())
require.NoError(t, err)
resp = getHostEncryptionKeyResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue)
// use the admin token to get the activities
currToken := s.token
defer func() { s.token = currToken }()
s.token = s.getTestAdminToken()
s.lastActivityMatches(
"read_host_disk_encryption_key",
fmt.Sprintf(`{"host_display_name": "%s", "host_id": %d}`, host.DisplayName(), host.ID),
0,
)
}
// we're about to mess up with the token, make sure to set it to the
// default value when the test ends
currToken := s.token
t.Cleanup(func() { s.token = currToken })
// admins are able to see the host encryption key
s.token = s.getTestAdminToken()
checkDecryptableKey(s.users["admin1@example.com"])
// maintainers are able to see the token
u := s.users["user1@example.com"]
s.token = s.getTestToken(u.Email, test.GoodPassword)
checkDecryptableKey(u)
// observers are able to see the token
u = s.users["user2@example.com"]
s.token = s.getTestToken(u.Email, test.GoodPassword)
checkDecryptableKey(u)
// add the host to a team
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 4827,
Name: "team1_" + t.Name(),
Description: "desc team1_" + t.Name(),
})
require.NoError(t, err)
err = s.ds.AddHostsToTeam(ctx, &team.ID, []uint{host.ID})
require.NoError(t, err)
// admins are still able to see the token
s.token = s.getTestAdminToken()
checkDecryptableKey(s.users["admin1@example.com"])
// maintainers are still able to see the token
u = s.users["user1@example.com"]
s.token = s.getTestToken(u.Email, test.GoodPassword)
checkDecryptableKey(u)
// observers are still able to see the token
u = s.users["user2@example.com"]
s.token = s.getTestToken(u.Email, test.GoodPassword)
checkDecryptableKey(u)
// add a team member
u = fleet.User{
Name: "test team user",
Email: "user1+team@example.com",
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: *team,
Role: fleet.RoleMaintainer,
},
},
}
require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
_, err = s.ds.NewUser(ctx, &u)
require.NoError(t, err)
// members are able to see the token
s.token = s.getTestToken(u.Email, test.GoodPassword)
checkDecryptableKey(u)
// create a separate team
team2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
ID: 4828,
Name: "team2_" + t.Name(),
Description: "desc team2_" + t.Name(),
})
require.NoError(t, err)
// add a team member
u = fleet.User{
Name: "test team user",
Email: "user1+team2@example.com",
GlobalRole: nil,
Teams: []fleet.UserTeam{
{
Team: *team2,
Role: fleet.RoleMaintainer,
},
},
}
require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
_, err = s.ds.NewUser(ctx, &u)
require.NoError(t, err)
// non-members aren't able to see the token
s.token = s.getTestToken(u.Email, test.GoodPassword)
resp = getHostEncryptionKeyResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusForbidden, &resp)
}
type device struct {
uuid string
serial string
@@ -23,7 +23,7 @@ func main() {
App: config.AppConfig{
EnableScheduledQueryStats: true,
},
}, &fleet.Features{
}, nil, &fleet.Features{
EnableSoftwareInventory: true,
EnableHostUsers: true,
})
+23
View File
@@ -304,3 +304,26 @@ func (ts *withServer) LoginSSOUser(username, password string) (fleet.Auth, strin
require.NoError(t, err)
return auth, string(body)
}
// gets the latest activity and checks that it matches any provided properties.
// empty string or 0 id means do not check that property. It returns the ID of that
// latest activity.
func (ts *withServer) lastActivityMatches(name, details string, id uint) uint {
t := ts.s.T()
var listActivities listActivitiesResponse
ts.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "1")
require.True(t, len(listActivities.Activities) > 0)
act := listActivities.Activities[0]
if name != "" {
assert.Equal(t, name, act.Type)
}
if details != "" {
require.NotNil(t, act.Details)
assert.JSONEq(t, details, string(*act.Details))
}
if id > 0 {
assert.Equal(t, id, act.ID)
}
return act.ID
}