From 49a7190badfba63c261627453e60ff4bdee90716 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 30 May 2022 16:54:01 -0300 Subject: [PATCH 01/10] add premium, device authed endpoint to retrieve policies --- ee/server/service/devices.go | 28 ++++++++++++++ server/datastore/mysql/hosts_test.go | 55 ++++++++++++++++++++++++++++ server/fleet/service.go | 2 + server/service/devices.go | 42 +++++++++++++++++++++ server/service/devices_test.go | 46 +++++++++++++++++++++++ server/service/handler.go | 1 + 6 files changed, 174 insertions(+) create mode 100644 ee/server/service/devices.go create mode 100644 server/service/devices_test.go diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go new file mode 100644 index 000000000000..40aaac7afa90 --- /dev/null +++ b/ee/server/service/devices.go @@ -0,0 +1,28 @@ +package service + +import ( + "context" + + "github.com/fleetdm/fleet/v4/server/contexts/authz" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" + "github.com/fleetdm/fleet/v4/server/fleet" +) + +func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { + if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) { + if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { + return nil, err + } + + host, err := svc.ds.HostLite(ctx, host.ID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "find host for device policies") + } + + if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil { + return nil, err + } + } + + return svc.ds.ListPoliciesForHost(ctx, host) +} diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index e15c957cc020..13182651d4a7 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -75,6 +75,7 @@ func TestHosts(t *testing.T) { {"ListFilterAdditional", testHostsListFilterAdditional}, {"ListStatus", testHostsListStatus}, {"ListQuery", testHostsListQuery}, + {"ListPoliciesForHost", testListPoliciesForHost}, {"Enroll", testHostsEnroll}, {"LoadHostByNodeKey", testHostsLoadHostByNodeKey}, {"LoadHostByNodeKeyCaseSensitive", testHostsLoadHostByNodeKeyCaseSensitive}, @@ -4206,3 +4207,57 @@ func testShouldCleanTeamPolicies(t *testing.T, ds *Datastore) { require.Equal(t, shouldCleanTeamPolicies(c.currentTeamID, c.newTeamID), c.out) } } + +func testListPoliciesForHost(t *testing.T, ds *Datastore) { + user := test.NewUser(t, ds, "Alice", "alice@example.com", true) + team, err := ds.NewTeam(context.Background(), &fleet.Team{Name: t.Name()}) + require.NoError(t, err) + + host, err := ds.NewHost(context.Background(), &fleet.Host{ + OsqueryHostID: "1234", + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: "1", + UUID: "1", + Hostname: "foo.local", + }) + require.NoError(t, err) + require.NoError(t, ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID})) + host, err = ds.Host(context.Background(), host.ID) + + tq, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query1", + Description: "query1 desc", + Query: "select 1;", + Saved: true, + }) + require.NoError(t, err) + + teamPolicy, err := ds.NewTeamPolicy(context.Background(), team.ID, &user.ID, fleet.PolicyPayload{ + QueryID: &tq.ID, + }) + require.NoError(t, err) + + gq, err := ds.NewQuery(context.Background(), &fleet.Query{ + Name: "query2", + Description: "query2 desc", + Query: "select 2;", + Saved: true, + }) + require.NoError(t, err) + globalPolicy, err := ds.NewGlobalPolicy(context.Background(), &user.ID, fleet.PolicyPayload{ + QueryID: &gq.ID, + }) + require.NoError(t, err) + + require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{teamPolicy.ID: ptr.Bool(false), globalPolicy.ID: ptr.Bool(true)}, time.Now(), false)) + require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{teamPolicy.ID: ptr.Bool(true), globalPolicy.ID: ptr.Bool(false)}, time.Now(), false)) + + hostPolicies, err := ds.ListPoliciesForHost(context.Background(), host) + require.NoError(t, err) + + require.Len(t, hostPolicies, 2) + require.ElementsMatch(t, []*fleet.HostPolicy{&fleet.HostPolicy{teamPolicy.PolicyData, "pass"}, &fleet.HostPolicy{globalPolicy.PolicyData, "fail"}}, hostPolicies) +} diff --git a/server/fleet/service.go b/server/fleet/service.go index ea3e1254c941..d8a0a5857d36 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -257,6 +257,8 @@ type Service interface { // ListHostDeviceMapping returns the list of device-mapping of user's email address // for the host. ListHostDeviceMapping(ctx context.Context, id uint) ([]*HostDeviceMapping, error) + // ListDevicePolicies lists all policies for the given host, including passing / failing summaries + ListDevicePolicies(ctx context.Context, host *Host) ([]*HostPolicy, error) MacadminsData(ctx context.Context, id uint) (*MacadminsData, error) AggregatedMacadminsData(ctx context.Context, teamID *uint) (*AggregatedMacadminsData, error) diff --git a/server/service/devices.go b/server/service/devices.go index 8c133ee20e5a..af42b91d3d56 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -168,3 +168,45 @@ func getDeviceMacadminsDataEndpoint(ctx context.Context, request interface{}, sv } return getMacadminsDataResponse{Macadmins: data}, nil } + +//////////////////////////////////////////////////////////////////////////////// +// Get Current Device's Policies +//////////////////////////////////////////////////////////////////////////////// + +type getDevicePoliciesRequest struct { + Token string `url:"token"` +} + +func (r *getDevicePoliciesRequest) deviceAuthToken() string { + return r.Token +} + +type getDevicePoliciesResponse struct { + Err error `json:"error,omitempty"` + Policies []*fleet.HostPolicy `json:"policies"` +} + +func (r getDevicePoliciesResponse) error() error { return r.Err } + +func getDevicePoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { + host, ok := hostctx.FromContext(ctx) + if !ok { + err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) + return getHostResponse{Err: err}, nil + } + + data, err := svc.ListDevicePolicies(ctx, host) + if err != nil { + return getDevicePoliciesResponse{Err: err}, nil + } + + return getDevicePoliciesResponse{Policies: data}, nil +} + +func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { + // skipauth: No authorization check needed due to implementation returning + // only license error. + svc.authz.SkipAuthorization(ctx) + + return nil, fleet.ErrMissingLicense +} diff --git a/server/service/devices_test.go b/server/service/devices_test.go new file mode 100644 index 000000000000..a463e465e333 --- /dev/null +++ b/server/service/devices_test.go @@ -0,0 +1,46 @@ +package service + +import ( + "context" + "errors" + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mock" + "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/test" + "github.com/stretchr/testify/require" +) + +func TestListDevicePolicies(t *testing.T) { + ds := new(mock.Store) + mockPolicies := []*fleet.HostPolicy{&fleet.HostPolicy{fleet.PolicyData{Name: "test-policy"}, "pass"}} + ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { + return mockPolicies, nil + } + + ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) { + if hostID != uint(1) { + return nil, errors.New("test error") + } + + return &fleet.Host{ID: hostID, TeamID: ptr.Uint(1)}, nil + } + + t.Run("without premium license", func(t *testing.T) { + svc := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierFree}}) + _, error := svc.ListDevicePolicies(test.UserContext(test.UserAdmin), &fleet.Host{ID: 1}) + require.ErrorIs(t, error, fleet.ErrMissingLicense) + }) + + t.Run("with premium license", func(t *testing.T) { + svc := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}) + + _, error := svc.ListDevicePolicies(test.UserContext(test.UserAdmin), &fleet.Host{}) + require.Error(t, error) + + policies, error := svc.ListDevicePolicies(test.UserContext(test.UserAdmin), &fleet.Host{ID: 1}) + require.NoError(t, error) + require.Len(t, policies, len(mockPolicies)) + }) +} diff --git a/server/service/handler.go b/server/service/handler.go index d5b771606619..3047ba7f728c 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -395,6 +395,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC de.POST("/api/_version_/fleet/device/{token}/refetch", refetchDeviceHostEndpoint, refetchDeviceHostRequest{}) de.GET("/api/_version_/fleet/device/{token}/device_mapping", listDeviceHostDeviceMappingEndpoint, listDeviceHostDeviceMappingRequest{}) de.GET("/api/_version_/fleet/device/{token}/macadmins", getDeviceMacadminsDataEndpoint, getDeviceMacadminsDataRequest{}) + de.GET("/api/_version_/fleet/device/{token}/policies", getDevicePoliciesEndpoint, getDevicePoliciesRequest{}) // host-authenticated endpoints he := newHostAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...) From 1fd974d1e41d1e45f9011c97f16dc3f9bcfc64b5 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 30 May 2022 18:39:12 -0300 Subject: [PATCH 02/10] add changes file --- changes/issue-5685-device-policies-endpoint | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/issue-5685-device-policies-endpoint diff --git a/changes/issue-5685-device-policies-endpoint b/changes/issue-5685-device-policies-endpoint new file mode 100644 index 000000000000..9cd06d9c130e --- /dev/null +++ b/changes/issue-5685-device-policies-endpoint @@ -0,0 +1 @@ +* Added `/api/_version_/fleet/device/{token}/policies` to retrieve policies for a device. This endpoint can only be accessed with a premium license. From f00d6bc11daaf055afa907f70437ec5bc87a4cf7 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 30 May 2022 18:56:33 -0300 Subject: [PATCH 03/10] add documentationg --- docs/Using-Fleet/REST-API.md | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index ec66777a1be9..c29e74464a7d 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -17,6 +17,7 @@ - [Teams](#teams) - [Translator](#translator) - [Users](#users) +- [Devices](#devices) ## Overview @@ -6359,6 +6360,51 @@ Deletes the selected user's sessions in Fleet. Also deletes the user's API token `Status: 200` +## Devices + +- [List all software](#list-all-software) +- [List device policies](#list-device-policies) + +### List device policies + +_Available in Fleet Premium_ + +`GET /api/_version_/fleet/device/{token}/policies` + +#### Parameters + +None. + +#### Example + +`GET /api/latest/fleet/device/880e301e-e6c1-4ffa-ab59-1ecfbae34035/policies` + +##### Default response + +`Status: 200` + +```json +{ + "policies": [ + { + "id": 3, + "name": "Antivirus healthy (Linux)", + "query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;", + "description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.", + "author_id": 1, + "author_name": "Admin", + "author_email": "admin@example.com", + "team_id": null, + "resolution": "Ensure ClamAV and Freshclam are installed and running.", + "platform": "linux", + "created_at": "2022-05-23T20:53:36Z", + "updated_at": "2022-05-23T20:53:36Z", + "response": "fail" + } + ] +} +``` + ## Debug - [Get a summary of errors](#get-a-summary-of-errors) From 242cfae838ef6d02cc5c0037d8c02fc7fde67a5d Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 31 May 2022 10:51:12 -0300 Subject: [PATCH 04/10] Revert "add documentationg" This reverts commit f00d6bc11daaf055afa907f70437ec5bc87a4cf7. --- docs/Using-Fleet/REST-API.md | 46 ------------------------------------ 1 file changed, 46 deletions(-) diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md index c29e74464a7d..ec66777a1be9 100644 --- a/docs/Using-Fleet/REST-API.md +++ b/docs/Using-Fleet/REST-API.md @@ -17,7 +17,6 @@ - [Teams](#teams) - [Translator](#translator) - [Users](#users) -- [Devices](#devices) ## Overview @@ -6360,51 +6359,6 @@ Deletes the selected user's sessions in Fleet. Also deletes the user's API token `Status: 200` -## Devices - -- [List all software](#list-all-software) -- [List device policies](#list-device-policies) - -### List device policies - -_Available in Fleet Premium_ - -`GET /api/_version_/fleet/device/{token}/policies` - -#### Parameters - -None. - -#### Example - -`GET /api/latest/fleet/device/880e301e-e6c1-4ffa-ab59-1ecfbae34035/policies` - -##### Default response - -`Status: 200` - -```json -{ - "policies": [ - { - "id": 3, - "name": "Antivirus healthy (Linux)", - "query": "SELECT score FROM (SELECT case when COUNT(*) = 2 then 1 ELSE 0 END AS score FROM processes WHERE (name = 'clamd') OR (name = 'freshclam')) WHERE score == 1;", - "description": "Checks that both ClamAV's daemon and its updater service (freshclam) are running.", - "author_id": 1, - "author_name": "Admin", - "author_email": "admin@example.com", - "team_id": null, - "resolution": "Ensure ClamAV and Freshclam are installed and running.", - "platform": "linux", - "created_at": "2022-05-23T20:53:36Z", - "updated_at": "2022-05-23T20:53:36Z", - "response": "fail" - } - ] -} -``` - ## Debug - [Get a summary of errors](#get-a-summary-of-errors) From e8a58e277e9c13d35ce78c1ff98dc7a0f1da5f1c Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 31 May 2022 10:56:26 -0300 Subject: [PATCH 05/10] remove test --- server/datastore/mysql/hosts_test.go | 55 ---------------------------- 1 file changed, 55 deletions(-) diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 13182651d4a7..e15c957cc020 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -75,7 +75,6 @@ func TestHosts(t *testing.T) { {"ListFilterAdditional", testHostsListFilterAdditional}, {"ListStatus", testHostsListStatus}, {"ListQuery", testHostsListQuery}, - {"ListPoliciesForHost", testListPoliciesForHost}, {"Enroll", testHostsEnroll}, {"LoadHostByNodeKey", testHostsLoadHostByNodeKey}, {"LoadHostByNodeKeyCaseSensitive", testHostsLoadHostByNodeKeyCaseSensitive}, @@ -4207,57 +4206,3 @@ func testShouldCleanTeamPolicies(t *testing.T, ds *Datastore) { require.Equal(t, shouldCleanTeamPolicies(c.currentTeamID, c.newTeamID), c.out) } } - -func testListPoliciesForHost(t *testing.T, ds *Datastore) { - user := test.NewUser(t, ds, "Alice", "alice@example.com", true) - team, err := ds.NewTeam(context.Background(), &fleet.Team{Name: t.Name()}) - require.NoError(t, err) - - host, err := ds.NewHost(context.Background(), &fleet.Host{ - OsqueryHostID: "1234", - DetailUpdatedAt: time.Now(), - LabelUpdatedAt: time.Now(), - PolicyUpdatedAt: time.Now(), - SeenTime: time.Now(), - NodeKey: "1", - UUID: "1", - Hostname: "foo.local", - }) - require.NoError(t, err) - require.NoError(t, ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID})) - host, err = ds.Host(context.Background(), host.ID) - - tq, err := ds.NewQuery(context.Background(), &fleet.Query{ - Name: "query1", - Description: "query1 desc", - Query: "select 1;", - Saved: true, - }) - require.NoError(t, err) - - teamPolicy, err := ds.NewTeamPolicy(context.Background(), team.ID, &user.ID, fleet.PolicyPayload{ - QueryID: &tq.ID, - }) - require.NoError(t, err) - - gq, err := ds.NewQuery(context.Background(), &fleet.Query{ - Name: "query2", - Description: "query2 desc", - Query: "select 2;", - Saved: true, - }) - require.NoError(t, err) - globalPolicy, err := ds.NewGlobalPolicy(context.Background(), &user.ID, fleet.PolicyPayload{ - QueryID: &gq.ID, - }) - require.NoError(t, err) - - require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{teamPolicy.ID: ptr.Bool(false), globalPolicy.ID: ptr.Bool(true)}, time.Now(), false)) - require.NoError(t, ds.RecordPolicyQueryExecutions(context.Background(), host, map[uint]*bool{teamPolicy.ID: ptr.Bool(true), globalPolicy.ID: ptr.Bool(false)}, time.Now(), false)) - - hostPolicies, err := ds.ListPoliciesForHost(context.Background(), host) - require.NoError(t, err) - - require.Len(t, hostPolicies, 2) - require.ElementsMatch(t, []*fleet.HostPolicy{&fleet.HostPolicy{teamPolicy.PolicyData, "pass"}, &fleet.HostPolicy{globalPolicy.PolicyData, "fail"}}, hostPolicies) -} From 4bb35e414c05697f66d190057778a861f54c2fc2 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 31 May 2022 10:57:29 -0300 Subject: [PATCH 06/10] remove unnecessary check --- ee/server/service/devices.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 40aaac7afa90..08406c2e6784 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -3,25 +3,22 @@ package service import ( "context" - "github.com/fleetdm/fleet/v4/server/contexts/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" ) func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { - if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) { - if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { - return nil, err - } + if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { + return nil, err + } - host, err := svc.ds.HostLite(ctx, host.ID) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "find host for device policies") - } + host, err := svc.ds.HostLite(ctx, host.ID) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "find host for device policies") + } - if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil { - return nil, err - } + if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil { + return nil, err } return svc.ds.ListPoliciesForHost(ctx, host) From eb2334f5d1a5bcdbf82b5c5f5b51763e14ca6a63 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 31 May 2022 11:07:07 -0300 Subject: [PATCH 07/10] remove unused code --- ee/server/service/devices.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 08406c2e6784..6b9dc1990883 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -3,23 +3,9 @@ package service import ( "context" - "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" ) func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { - if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil { - return nil, err - } - - host, err := svc.ds.HostLite(ctx, host.ID) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "find host for device policies") - } - - if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil { - return nil, err - } - return svc.ds.ListPoliciesForHost(ctx, host) } From ea2c1c0cb10b16247bb8bacbabd00c0783ca4299 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 31 May 2022 13:13:10 -0300 Subject: [PATCH 08/10] rename methods and interfaces --- server/service/devices.go | 16 ++++++++-------- server/service/handler.go | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server/service/devices.go b/server/service/devices.go index af42b91d3d56..51947455443c 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -170,25 +170,25 @@ func getDeviceMacadminsDataEndpoint(ctx context.Context, request interface{}, sv } //////////////////////////////////////////////////////////////////////////////// -// Get Current Device's Policies +// List Current Device's Policies //////////////////////////////////////////////////////////////////////////////// -type getDevicePoliciesRequest struct { +type listDevicePoliciesRequest struct { Token string `url:"token"` } -func (r *getDevicePoliciesRequest) deviceAuthToken() string { +func (r *listDevicePoliciesRequest) deviceAuthToken() string { return r.Token } -type getDevicePoliciesResponse struct { +type listDevicePoliciesResponse struct { Err error `json:"error,omitempty"` Policies []*fleet.HostPolicy `json:"policies"` } -func (r getDevicePoliciesResponse) error() error { return r.Err } +func (r listDevicePoliciesResponse) error() error { return r.Err } -func getDevicePoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { +func listDevicePoliciesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (interface{}, error) { host, ok := hostctx.FromContext(ctx) if !ok { err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context")) @@ -197,10 +197,10 @@ func getDevicePoliciesEndpoint(ctx context.Context, request interface{}, svc fle data, err := svc.ListDevicePolicies(ctx, host) if err != nil { - return getDevicePoliciesResponse{Err: err}, nil + return listDevicePoliciesResponse{Err: err}, nil } - return getDevicePoliciesResponse{Policies: data}, nil + return listDevicePoliciesResponse{Policies: data}, nil } func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { diff --git a/server/service/handler.go b/server/service/handler.go index 3047ba7f728c..2e9165646ce9 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -395,7 +395,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC de.POST("/api/_version_/fleet/device/{token}/refetch", refetchDeviceHostEndpoint, refetchDeviceHostRequest{}) de.GET("/api/_version_/fleet/device/{token}/device_mapping", listDeviceHostDeviceMappingEndpoint, listDeviceHostDeviceMappingRequest{}) de.GET("/api/_version_/fleet/device/{token}/macadmins", getDeviceMacadminsDataEndpoint, getDeviceMacadminsDataRequest{}) - de.GET("/api/_version_/fleet/device/{token}/policies", getDevicePoliciesEndpoint, getDevicePoliciesRequest{}) + de.GET("/api/_version_/fleet/device/{token}/policies", listDevicePoliciesEndpoint,listDevicePoliciesRequestt{}) // host-authenticated endpoints he := newHostAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...) From 8150c500e9ed3fc6fb2ce1f54feb18269dbb0270 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 31 May 2022 13:13:17 -0300 Subject: [PATCH 09/10] remove test --- server/service/devices_test.go | 46 ---------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 server/service/devices_test.go diff --git a/server/service/devices_test.go b/server/service/devices_test.go deleted file mode 100644 index a463e465e333..000000000000 --- a/server/service/devices_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package service - -import ( - "context" - "errors" - "testing" - - "github.com/fleetdm/fleet/v4/server/fleet" - "github.com/fleetdm/fleet/v4/server/mock" - "github.com/fleetdm/fleet/v4/server/ptr" - "github.com/fleetdm/fleet/v4/server/test" - "github.com/stretchr/testify/require" -) - -func TestListDevicePolicies(t *testing.T) { - ds := new(mock.Store) - mockPolicies := []*fleet.HostPolicy{&fleet.HostPolicy{fleet.PolicyData{Name: "test-policy"}, "pass"}} - ds.ListPoliciesForHostFunc = func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { - return mockPolicies, nil - } - - ds.HostLiteFunc = func(ctx context.Context, hostID uint) (*fleet.Host, error) { - if hostID != uint(1) { - return nil, errors.New("test error") - } - - return &fleet.Host{ID: hostID, TeamID: ptr.Uint(1)}, nil - } - - t.Run("without premium license", func(t *testing.T) { - svc := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierFree}}) - _, error := svc.ListDevicePolicies(test.UserContext(test.UserAdmin), &fleet.Host{ID: 1}) - require.ErrorIs(t, error, fleet.ErrMissingLicense) - }) - - t.Run("with premium license", func(t *testing.T) { - svc := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}}) - - _, error := svc.ListDevicePolicies(test.UserContext(test.UserAdmin), &fleet.Host{}) - require.Error(t, error) - - policies, error := svc.ListDevicePolicies(test.UserContext(test.UserAdmin), &fleet.Host{ID: 1}) - require.NoError(t, error) - require.Len(t, policies, len(mockPolicies)) - }) -} From 934530a2dc81de68cb3dfe591af823845bfcc16c Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Tue, 31 May 2022 14:06:33 -0300 Subject: [PATCH 10/10] add integration tests --- server/service/handler.go | 2 +- server/service/integration_core_test.go | 7 ++ server/service/integration_enterprise_test.go | 101 ++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/server/service/handler.go b/server/service/handler.go index 2e9165646ce9..203293e5da75 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -395,7 +395,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC de.POST("/api/_version_/fleet/device/{token}/refetch", refetchDeviceHostEndpoint, refetchDeviceHostRequest{}) de.GET("/api/_version_/fleet/device/{token}/device_mapping", listDeviceHostDeviceMappingEndpoint, listDeviceHostDeviceMappingRequest{}) de.GET("/api/_version_/fleet/device/{token}/macadmins", getDeviceMacadminsDataEndpoint, getDeviceMacadminsDataRequest{}) - de.GET("/api/_version_/fleet/device/{token}/policies", listDevicePoliciesEndpoint,listDevicePoliciesRequestt{}) + de.GET("/api/_version_/fleet/device/{token}/policies", listDevicePoliciesEndpoint, listDevicePoliciesRequest{}) // host-authenticated endpoints he := newHostAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...) diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index cc3ae3c74648..fe9472bae365 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -4634,6 +4634,13 @@ func (s *integrationTestSuite) TestDeviceAuthenticatedEndpoints() { res.Body.Close() require.NotNil(t, getHostResp.License) require.Equal(t, getHostResp.License.Tier, "free") + + // device policies are not accessible for free endpoints + listPoliciesResp := listDevicePoliciesResponse{} + res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/policies", nil, http.StatusPaymentRequired) + json.NewDecoder(res.Body).Decode(&getHostResp) + res.Body.Close() + require.Nil(t, listPoliciesResp.Policies) } func (s *integrationTestSuite) TestModifyUser() { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 8e155b86020a..82638368a5c5 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -8,10 +8,14 @@ import ( "net/http" "strings" "testing" + "time" + "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -456,3 +460,100 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() { // delete team again, now an unknown team s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/teams/%d", tm1ID), nil, http.StatusNotFound, &delResp) } + +func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() { + t := s.T() + + team, err := s.ds.NewTeam(context.Background(), &fleet.Team{ + ID: 51, + Name: "team1-policies", + Description: "desc team1", + }) + require.NoError(t, err) + + 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: t.Name(), + NodeKey: t.Name(), + UUID: uuid.New().String(), + Hostname: fmt.Sprintf("%sfoo.local", t.Name()), + Platform: "darwin", + }) + require.NoError(t, err) + err = s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID}) + require.NoError(t, err) + + // create an auth token for hosts[0] + token := "much_valid" + mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error { + _, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, host.ID, token) + return err + }) + + qr, err := s.ds.NewQuery(context.Background(), &fleet.Query{ + Name: "TestQueryEnterpriseGlobalPolicy", + Description: "Some description", + Query: "select * from osquery;", + ObserverCanRun: true, + }) + require.NoError(t, err) + + // add a global policy + gpParams := globalPolicyRequest{ + QueryID: &qr.ID, + Resolution: "some global resolution", + } + gpResp := globalPolicyResponse{} + s.DoJSON("POST", "/api/latest/fleet/policies", gpParams, http.StatusOK, &gpResp) + require.NotNil(t, gpResp.Policy) + + // add a policy to team 1 + oldToken := s.token + t.Cleanup(func() { + s.token = oldToken + }) + + password := test.GoodPassword + email := "test_enterprise_policies@user.com" + + u := &fleet.User{ + Name: "test team user", + Email: email, + GlobalRole: nil, + Teams: []fleet.UserTeam{ + { + Team: *team, + Role: fleet.RoleMaintainer, + }, + }, + } + + require.NoError(t, u.SetPassword(password, 10, 10)) + _, err = s.ds.NewUser(context.Background(), u) + require.NoError(t, err) + + s.token = s.getTestToken(email, password) + tpParams := teamPolicyRequest{ + Name: "TestQueryEnterpriseTeamPolicy", + Query: "select * from osquery;", + Description: "Some description", + Resolution: "some team resolution", + Platform: "darwin", + } + tpResp := teamPolicyResponse{} + s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies", team.ID), tpParams, http.StatusOK, &tpResp) + + // try with invalid token + res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/invalid_token/policies", nil, http.StatusUnauthorized) + res.Body.Close() + + listDevicePoliciesResp := listDevicePoliciesResponse{} + res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/policies", nil, http.StatusOK) + json.NewDecoder(res.Body).Decode(&listDevicePoliciesResp) + res.Body.Close() + require.Len(t, listDevicePoliciesResp.Policies, 2) + require.NoError(t, listDevicePoliciesResp.Err) +}