add API endpoint to see disk encryption key (#9713)
https://github.com/fleetdm/fleet/issues/8708
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added an API endpoint to retrieve a host disk encryption key for macOS if Fleet's MDM is enabled.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
}`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:"-"`
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user