From e31fc889f1ef1993f03ef8ca8d3473d7e1e9741d Mon Sep 17 00:00:00 2001 From: gillespi314 <73313222+gillespi314@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:26:06 -0600 Subject: [PATCH] Add MDM profiles to host detail in API responses (#10034) Issue #9599 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [ ] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md) - [ ] Documented any permissions changes - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [ ] Added/updated tests - [ ] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). --- cmd/fleetctl/get_test.go | 4 + go.mod | 2 + go.sum | 2 + server/datastore/mysql/apple_mdm.go | 19 +++ server/datastore/mysql/apple_mdm_test.go | 120 ++++++++++++++++++- server/datastore/mysql/hosts.go | 4 +- server/fleet/apple_mdm.go | 12 +- server/fleet/datastore.go | 3 + server/fleet/hosts.go | 10 +- server/mock/datastore_mock.go | 10 ++ server/service/apple_mdm.go | 2 - server/service/apple_mdm_test.go | 140 ++++++++++++++++++++++- server/service/hosts.go | 13 +++ server/service/hosts_test.go | 7 ++ 14 files changed, 333 insertions(+), 15 deletions(-) diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 10838b498d..58a710d628 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -265,6 +265,10 @@ func TestGetTeamsByName(t *testing.T) { func TestGetHosts(t *testing.T) { _, ds := runServerWithMockedDS(t) + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{}, nil + } + // this func is called when no host is specified i.e. `fleetctl get hosts --json` ds.ListHostsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) { additional := json.RawMessage(`{"query1": [{"col1": "val", "col2": 42}]}`) diff --git a/go.mod b/go.mod index c062063209..555bb646db 100644 --- a/go.mod +++ b/go.mod @@ -237,6 +237,7 @@ require ( github.com/jonboulle/clockwork v0.2.2 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/klauspost/compress v1.15.0 // indirect + github.com/lib/pq v1.10.6 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-colorable v0.1.11 // indirect @@ -287,6 +288,7 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect + github.com/ziutek/mymysql v1.5.4 // indirect go.elastic.co/apm v1.15.0 // indirect go.elastic.co/fastjson v1.1.0 // indirect go.opencensus.io v0.23.0 // indirect diff --git a/go.sum b/go.sum index 84ed0b809b..c846d2a10d 100644 --- a/go.sum +++ b/go.sum @@ -914,6 +914,7 @@ github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/macadmins/osquery-extension v0.0.7 h1:iEo2eajSbG51pdXg+QH3kM5ZWhidWBBZ/YiNDEXU1lE= @@ -1306,6 +1307,7 @@ github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPR github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c h1:TWQ2UvXPkhPxI2KmApKBOCaV6yD2N4mlvqFQ/DlPtpQ= github.com/zwass/kit v0.0.0-20210625184505-ec5b5c5cce9c/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY= diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go index 81176cc78a..6bbf5d1c39 100644 --- a/server/datastore/mysql/apple_mdm.go +++ b/server/datastore/mysql/apple_mdm.go @@ -140,6 +140,25 @@ func (ds *Datastore) DeleteMDMAppleConfigProfile(ctx context.Context, profileID return nil } +func (ds *Datastore) GetHostMDMProfiles(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) { + stmt := ` +SELECT + profile_id, + status, + operation_type, + detail +FROM + host_mdm_apple_profiles +WHERE + host_uuid = ?` + + var profiles []fleet.HostMDMAppleProfile + if err := sqlx.SelectContext(ctx, ds.reader, &profiles, stmt, hostUUID); err != nil { + return nil, err + } + return profiles, nil +} + func (ds *Datastore) NewMDMAppleEnrollmentProfile( ctx context.Context, payload fleet.MDMAppleEnrollmentProfilePayload, diff --git a/server/datastore/mysql/apple_mdm_test.go b/server/datastore/mysql/apple_mdm_test.go index 38a59d2333..6cdf24dcd2 100644 --- a/server/datastore/mysql/apple_mdm_test.go +++ b/server/datastore/mysql/apple_mdm_test.go @@ -27,6 +27,7 @@ func TestMDMAppleConfigProfile(t *testing.T) { {"TestNewMDMAppleConfigProfileDuplicateIdentifier", testNewMDMAppleConfigProfileDuplicateIdentifier}, {"TestDeleteMDMAppleConfigProfile", testDeleteMDMAppleConfigProfile}, {"TestListMDMAppleConfigProfiles", testListMDMAppleConfigProfiles}, + {"TestHostDetailsMDMProfiles", testHostDetailsMDMProfiles}, {"TestBatchSetMDMAppleProfiles", testBatchSetMDMAppleProfiles}, {"TestMDMAppleProfileManagement", testMDMAppleProfileManagement}, {"TestUpdateHostMDMAppleProfile", testGetMDMAppleProfilesContents}, @@ -192,6 +193,124 @@ func checkConfigProfile(t *testing.T, expected fleet.MDMAppleConfigProfile, actu require.Equal(t, expected.Mobileconfig, actual.Mobileconfig) } +func testHostDetailsMDMProfiles(t *testing.T, ds *Datastore) { + ctx := context.Background() + + p0, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{Name: "Name0", Identifier: "Identifier0", Mobileconfig: []byte("profile0-bytes")}) + require.NoError(t, err) + + p1, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{Name: "Name1", Identifier: "Identifier1", Mobileconfig: []byte("profile1-bytes")}) + require.NoError(t, err) + + p2, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{Name: "Name2", Identifier: "Identifier2", Mobileconfig: []byte("profile2-bytes")}) + require.NoError(t, err) + + profiles, err := ds.ListMDMAppleConfigProfiles(ctx, ptr.Uint(0)) + require.NoError(t, err) + require.Len(t, profiles, 3) + + h0, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: ptr.String("host0-osquery-id"), + NodeKey: ptr.String("host0-node-key"), + UUID: "host0-test-mdm-profiles", + Hostname: "hostname0", + }) + require.NoError(t, err) + + gotHost, err := ds.Host(ctx, h0.ID) + require.NoError(t, err) + require.Nil(t, gotHost.MDM.Profiles) + gotProfs, err := ds.GetHostMDMProfiles(ctx, h0.UUID) + require.NoError(t, err) + require.Nil(t, gotProfs) + + h1, err := ds.NewHost(ctx, &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + OsqueryHostID: ptr.String("host1-osquery-id"), + NodeKey: ptr.String("host1-node-key"), + UUID: "host1-test-mdm-profiles", + Hostname: "hostname1", + }) + require.NoError(t, err) + + gotHost, err = ds.Host(ctx, h1.ID) + require.NoError(t, err) + require.Nil(t, gotHost.MDM.Profiles) + gotProfs, err = ds.GetHostMDMProfiles(ctx, h1.UUID) + require.NoError(t, err) + require.Nil(t, gotProfs) + + expectedProfiles0 := map[uint]fleet.HostMDMAppleProfile{ + p0.ProfileID: {HostUUID: h0.UUID, ProfileID: p0.ProfileID, CommandUUID: "cmd0-uuid", Status: &fleet.MDMAppleDeliveryPending, OperationType: fleet.MDMAppleOperationTypeInstall, Detail: ""}, + p1.ProfileID: {HostUUID: h0.UUID, ProfileID: p1.ProfileID, CommandUUID: "cmd1-uuid", Status: &fleet.MDMAppleDeliveryApplied, OperationType: fleet.MDMAppleOperationTypeInstall, Detail: ""}, + p2.ProfileID: {HostUUID: h0.UUID, ProfileID: p2.ProfileID, CommandUUID: "cmd2-uuid", Status: &fleet.MDMAppleDeliveryFailed, OperationType: fleet.MDMAppleOperationTypeRemove, Detail: "Error removing profile"}, + } + + expectedProfiles1 := map[uint]fleet.HostMDMAppleProfile{ + p0.ProfileID: {HostUUID: h1.UUID, ProfileID: p0.ProfileID, CommandUUID: "cmd0-uuid", Status: &fleet.MDMAppleDeliveryFailed, OperationType: fleet.MDMAppleOperationTypeInstall, Detail: "Error installing profile"}, + p1.ProfileID: {HostUUID: h1.UUID, ProfileID: p1.ProfileID, CommandUUID: "cmd1-uuid", Status: &fleet.MDMAppleDeliveryApplied, OperationType: fleet.MDMAppleOperationTypeInstall, Detail: ""}, + p2.ProfileID: {HostUUID: h1.UUID, ProfileID: p2.ProfileID, CommandUUID: "cmd2-uuid", Status: &fleet.MDMAppleDeliveryFailed, OperationType: fleet.MDMAppleOperationTypeRemove, Detail: "Error removing profile"}, + } + + var args []interface{} + for _, p := range expectedProfiles0 { + args = append(args, p.HostUUID, p.ProfileID, p.CommandUUID, *p.Status, p.OperationType, p.Detail) + } + for _, p := range expectedProfiles1 { + args = append(args, p.HostUUID, p.ProfileID, p.CommandUUID, *p.Status, p.OperationType, p.Detail) + } + + ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { + _, err := q.ExecContext(ctx, ` + INSERT INTO host_mdm_apple_profiles ( + host_uuid, profile_id, command_uuid, status, operation_type, detail) + VALUES (?,?,?,?,?,?),(?,?,?,?,?,?),(?,?,?,?,?,?),(?,?,?,?,?,?),(?,?,?,?,?,?),(?,?,?,?,?,?) + `, args..., + ) + if err != nil { + return err + } + return nil + }) + + gotHost, err = ds.Host(ctx, h1.ID) + require.NoError(t, err) + require.Nil(t, gotHost.MDM.Profiles) // ds.Host never returns MDM profiles + + gotProfs, err = ds.GetHostMDMProfiles(ctx, h0.UUID) + require.NoError(t, err) + require.Len(t, gotProfs, 3) + for _, gp := range gotProfs { + ep, ok := expectedProfiles0[gp.ProfileID] + require.True(t, ok) + require.Equal(t, *ep.Status, *gp.Status) + require.Equal(t, ep.OperationType, gp.OperationType) + require.Equal(t, ep.Detail, gp.Detail) + } + + gotHost, err = ds.Host(ctx, h1.ID) + require.NoError(t, err) + require.Nil(t, gotHost.MDM.Profiles) // ds.Host never returns MDM profiles + + gotProfs, err = ds.GetHostMDMProfiles(ctx, h1.UUID) + require.NoError(t, err) + require.Len(t, gotProfs, 3) + for _, gp := range gotProfs { + ep, ok := expectedProfiles1[gp.ProfileID] + require.True(t, ok) + require.Equal(t, *ep.Status, *gp.Status) + require.Equal(t, ep.OperationType, gp.OperationType) + require.Equal(t, ep.Detail, gp.Detail) + } +} + func TestIngestMDMAppleDevicesFromDEPSync(t *testing.T) { ds := CreateMySQLDS(t) ctx := context.Background() @@ -823,7 +942,6 @@ func testMDMAppleProfileManagement(t *testing.T, ds *Datastore) { {ProfileID: globalPfs[1].ProfileID, ProfileIdentifier: globalPfs[1].Identifier, HostUUID: "test-uuid-1"}, {ProfileID: globalPfs[2].ProfileID, ProfileIdentifier: globalPfs[2].Identifier, HostUUID: "test-uuid-1"}, }, toRemove) - } // checkMDMHostRelatedTables checks that rows are inserted for new MDM hosts in diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index 90a2290820..0b0ae8bdd2 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -476,6 +476,8 @@ LIMIT return &host, nil } +// hostMDMSelect is the SQL fragment used to construct the JSON object +// of MDM host data. const hostMDMSelect = `, JSON_OBJECT( 'enrollment_status', @@ -2359,7 +2361,6 @@ func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostI base64_encrypted = VALUES(base64_encrypted) `, hostID, encryptedBase64Key) return err - } func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { @@ -2406,7 +2407,6 @@ func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) FROM host_disk_encryption_keys WHERE host_id = ?`, hostID) - if err != nil { if err == sql.ErrNoRows { msg := fmt.Sprintf("for host %d", hostID) diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index 6a1da6969e..ad6ff7e4db 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -422,12 +422,12 @@ func (cp MDMAppleConfigProfile) ScreenPayloadTypes() error { // HostMDMAppleProfile represents the status of an Apple MDM profile in a host. type HostMDMAppleProfile struct { - HostUUID string - CommandUUID string - ProfileID uint - Status *MDMAppleDeliveryStatus - OperationType MDMAppleOperationType - Detail string + HostUUID string `db:"host_uuid" json:"-"` + CommandUUID string `db:"command_uuid" json:"-"` + ProfileID uint `db:"profile_id" json:"profile_id"` + Status *MDMAppleDeliveryStatus `db:"status" json:"status"` + OperationType MDMAppleOperationType `db:"operation_type" json:"operation_type"` + Detail string `db:"detail" json:"detail"` } type MDMAppleProfilePayload struct { diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 4d93d7a1fb..6e05d13907 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -716,6 +716,9 @@ type Datastore interface { // profile id. DeleteMDMAppleConfigProfile(ctx context.Context, profileID uint) error + // GetHostMDMProfiles returns the MDM profile information for the specified host UUID. + GetHostMDMProfiles(ctx context.Context, hostUUID string) ([]HostMDMAppleProfile, error) + // NewMDMAppleEnrollmentProfile creates and returns new enrollment profile. // Such enrollment profiles allow devices to enroll to Fleet MDM. NewMDMAppleEnrollmentProfile(ctx context.Context, enrollmentPayload MDMAppleEnrollmentProfilePayload) (*MDMAppleEnrollmentProfile, error) diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index d8bd8174b5..df00eedee8 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -223,6 +223,14 @@ type MDMHostData struct { // EncryptionKeyAvailable indicates if Fleet was able to retrieve and // decode an encryption key for the host. EncryptionKeyAvailable bool `json:"encryption_key_available" db:"-" csv:"-"` + + // Profiles is a list of HostMDMProfiles for the host. Note that as for many + // other host fields, it is not filled in by all host-returning datastore methods. + // + // It is a pointer to a slice so that when set, it gets marhsaled even + // if the slice is empty, but when unset, it doesn't get marshaled + // (e.g. we don't return that information for the List Hosts endpoint). + Profiles *[]HostMDMAppleProfile `json:"profiles,omitempty" db:"profiles" csv:"-"` } // Scan implements the Scanner interface for sqlx, to support unmarshaling a @@ -268,7 +276,7 @@ func (h Host) AuthzType() string { } // HostDetail provides the full host metadata along with associated labels and -// packs. +// packs. It also includes policies, batteries, and MDM profiles, as applicable. type HostDetail struct { Host // Labels is the list of labels the host is a member of. diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 5ec2a96f85..1d69fefaf6 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -513,6 +513,8 @@ type ListMDMAppleConfigProfilesFunc func(ctx context.Context, teamID *uint) ([]* type DeleteMDMAppleConfigProfileFunc func(ctx context.Context, profileID uint) error +type GetHostMDMProfilesFunc func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) + type NewMDMAppleEnrollmentProfileFunc func(ctx context.Context, enrollmentPayload fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) type GetMDMAppleEnrollmentProfileByTokenFunc func(ctx context.Context, token string) (*fleet.MDMAppleEnrollmentProfile, error) @@ -1307,6 +1309,9 @@ type DataStore struct { DeleteMDMAppleConfigProfileFunc DeleteMDMAppleConfigProfileFunc DeleteMDMAppleConfigProfileFuncInvoked bool + GetHostMDMProfilesFunc GetHostMDMProfilesFunc + GetHostMDMProfilesFuncInvoked bool + NewMDMAppleEnrollmentProfileFunc NewMDMAppleEnrollmentProfileFunc NewMDMAppleEnrollmentProfileFuncInvoked bool @@ -3122,6 +3127,11 @@ func (s *DataStore) DeleteMDMAppleConfigProfile(ctx context.Context, profileID u return s.DeleteMDMAppleConfigProfileFunc(ctx, profileID) } +func (s *DataStore) GetHostMDMProfiles(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) { + s.GetHostMDMProfilesFuncInvoked = true + return s.GetHostMDMProfilesFunc(ctx, hostUUID) +} + func (s *DataStore) NewMDMAppleEnrollmentProfile(ctx context.Context, enrollmentPayload fleet.MDMAppleEnrollmentProfilePayload) (*fleet.MDMAppleEnrollmentProfile, error) { s.mu.Lock() s.NewMDMAppleEnrollmentProfileFuncInvoked = true diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index 97ee8f1317..3fcd759400 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -367,7 +367,6 @@ func (svc *Service) ListMDMAppleConfigProfiles(ctx context.Context, teamID uint) return nil, ctxerr.Wrap(ctx, err) } - // TODO: record activitiy return cps, nil } @@ -432,7 +431,6 @@ func (svc *Service) GetMDMAppleConfigProfile(ctx context.Context, profileID uint return nil, err } - // TODO: record activitiy return cp, nil } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 3c77a8913d..084408cef8 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -467,6 +467,143 @@ func mcBytesForTest(name, identifier, uuid string) []byte { `, name, identifier, uuid)) } +func TestHostDetailsMDMProfiles(t *testing.T) { + svc, ctx, ds := setupAppleMDMService(t) + ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}) + + expected := []fleet.HostMDMAppleProfile{ + {HostUUID: "H057-UU1D-1337", ProfileID: uint(5), CommandUUID: "CMD-UU1D-5", Status: &fleet.MDMAppleDeliveryPending, OperationType: fleet.MDMAppleOperationTypeInstall, Detail: ""}, + {HostUUID: "H057-UU1D-1337", ProfileID: uint(8), CommandUUID: "CMD-UU1D-8", Status: &fleet.MDMAppleDeliveryApplied, OperationType: fleet.MDMAppleOperationTypeInstall, Detail: ""}, + {HostUUID: "H057-UU1D-1337", ProfileID: uint(13), CommandUUID: "CMD-UU1D-13", Status: &fleet.MDMAppleDeliveryFailed, OperationType: fleet.MDMAppleOperationTypeRemove, Detail: "Error removing profile"}, + } + expectedByProfileID := make(map[uint]fleet.HostMDMAppleProfile) + for _, ep := range expected { + expectedByProfileID[ep.ProfileID] = ep + } + + ds.GetHostMDMProfilesFunc = func(ctx context.Context, hostUUID string) ([]fleet.HostMDMAppleProfile, error) { + if hostUUID == "H057-UU1D-1337" { + return expected, nil + } + return []fleet.HostMDMAppleProfile{}, nil + } + ds.HostFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) { + if hostID == uint(42) { + return &fleet.Host{ID: uint(42), UUID: "H057-UU1D-1337"}, nil + } + return &fleet.Host{ID: hostID, UUID: "WR0N6-UU1D"}, nil + } + ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) { + if identifier == "h0571d3n71f13r" { + return &fleet.Host{ID: uint(42), UUID: "H057-UU1D-1337"}, nil + } + return &fleet.Host{ID: uint(21), UUID: "WR0N6-UU1D"}, nil + } + ds.LoadHostSoftwareFunc = func(ctx context.Context, host *fleet.Host, includeCVEScores bool) error { + return nil + } + ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { + return nil, nil + } + ds.ListPacksForHostFunc = func(ctx context.Context, hid uint) (packs []*fleet.Pack, err error) { + return nil, nil + } + ds.ListHostBatteriesFunc = func(ctx context.Context, id uint) ([]*fleet.HostBattery, error) { + return nil, nil + } + ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { + return nil, nil + } + + cases := []struct { + name string + mdmEnabled bool + hostID *uint + hostIdentifier *string + expected *[]fleet.HostMDMAppleProfile + }{ + { + name: "TestGetHostMDMProfilesOK", + mdmEnabled: true, + hostID: ptr.Uint(42), + hostIdentifier: nil, + expected: &expected, + }, + { + name: "TestGetHostMDMProfilesEmpty", + mdmEnabled: false, + hostID: ptr.Uint(21), + hostIdentifier: nil, + expected: &[]fleet.HostMDMAppleProfile{}, + }, + { + name: "TestGetHostMDMProfilesNil", + mdmEnabled: false, + hostID: ptr.Uint(42), + hostIdentifier: nil, + expected: nil, + }, + { + name: "TestHostByIdentifierMDMProfilesOK", + mdmEnabled: true, + hostID: nil, + hostIdentifier: ptr.String("h0571d3n71f13r"), + expected: &expected, + }, + { + name: "TestHostByIdentifierMDMProfilesNil", + mdmEnabled: false, + hostID: nil, + hostIdentifier: ptr.String("h0571d3n71f13r"), + expected: nil, + }, + { + name: "TestHostByIdentifierMDMProfilesEmpty", + mdmEnabled: false, + hostID: nil, + hostIdentifier: ptr.String("4n07h3r1d3n71f13r"), + expected: &[]fleet.HostMDMAppleProfile{}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: c.mdmEnabled}}, nil + } + ds.AppConfigFuncInvoked = false + ds.HostFuncInvoked = false + ds.HostByIdentifierFuncInvoked = false + ds.GetHostMDMProfilesFuncInvoked = false + + var gotHost *fleet.HostDetail + if c.hostID != nil { + h, err := svc.GetHost(ctx, *c.hostID, fleet.HostDetailOptions{}) + require.NoError(t, err) + require.True(t, ds.HostFuncInvoked) + gotHost = h + } + if c.hostIdentifier != nil { + h, err := svc.HostByIdentifier(ctx, *c.hostIdentifier, fleet.HostDetailOptions{}) + require.NoError(t, err) + require.True(t, ds.HostByIdentifierFuncInvoked) + gotHost = h + } + require.NotNil(t, gotHost) + require.True(t, ds.AppConfigFuncInvoked) + + if !c.mdmEnabled { + require.Nil(t, gotHost.MDM.Profiles) + return + } + + require.True(t, ds.GetHostMDMProfilesFuncInvoked) + require.NotNil(t, gotHost.MDM.Profiles) + require.ElementsMatch(t, *c.expected, *gotHost.MDM.Profiles) + }) + } +} + func TestAppleMDMEnrollmentProfile(t *testing.T) { svc, ctx, _ := setupAppleMDMService(t) @@ -801,7 +938,6 @@ func TestMDMCommandAndReportResultsProfileHandling(t *testing.T) { require.True(t, ds.GetMDMAppleCommandRequestTypeFuncInvoked) require.True(t, ds.UpdateHostMDMAppleProfileFuncInvoked) } - } func TestMDMBatchSetAppleProfiles(t *testing.T) { @@ -1079,7 +1215,6 @@ func TestMDMAppleCommander(t *testing.T) { pushes := make(map[string]*mdm.Push, len(targetUUIDs)) for _, uuid := range targetUUIDs { pushes[uuid] = &mdm.Push{ - PushMagic: "magic" + uuid, Token: []byte("token" + uuid), Topic: "topic" + uuid, @@ -1240,7 +1375,6 @@ func TestMDMAppleReconcileProfiles(t *testing.T) { require.True(t, ds.ListMDMAppleProfilesToRemoveFuncInvoked) require.True(t, ds.GetMDMAppleProfilesContentsFuncInvoked) require.True(t, ds.BulkUpsertMDMAppleHostProfilesFuncInvoked) - } func mobileconfigForTest(name, identifier string) []byte { diff --git a/server/service/hosts.go b/server/service/hosts.go index 8771bd7f63..78144eda04 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -795,6 +795,19 @@ func (svc *Service) getHostDetails(ctx context.Context, host *fleet.Host, opts f policies = &hp } + // If Fleet MDM is enabled and configured, we want to include MDM profiles. + ac, err := svc.ds.AppConfig(ctx) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get app config for host mdm profiles") + } + if ac.MDM.EnabledAndConfigured { + profiles, err := svc.ds.GetHostMDMProfiles(ctx, host.UUID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "get host mdm profiles") + } + host.MDM.Profiles = &profiles + } + return &fleet.HostDetail{ Host: *host, Labels: labels, diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index 134df616cc..b8b6ad4ed1 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -37,6 +37,9 @@ func TestHostDetails(t *testing.T) { Description: "the foobar label", }, } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{}, nil + } ds.ListLabelsForHostFunc = func(ctx context.Context, hid uint) ([]*fleet.Label, error) { return expectedLabels, nil } @@ -83,6 +86,10 @@ func TestHostAuth(t *testing.T) { teamHost := &fleet.Host{TeamID: ptr.Uint(1)} globalHost := &fleet.Host{} + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{}, nil + } + ds.DeleteHostFunc = func(ctx context.Context, hid uint) error { return nil }