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. diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go new file mode 100644 index 000000000000..6b9dc1990883 --- /dev/null +++ b/ee/server/service/devices.go @@ -0,0 +1,11 @@ +package service + +import ( + "context" + + "github.com/fleetdm/fleet/v4/server/fleet" +) + +func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) { + return svc.ds.ListPoliciesForHost(ctx, host) +} 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..51947455443c 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 } + +//////////////////////////////////////////////////////////////////////////////// +// List Current Device's Policies +//////////////////////////////////////////////////////////////////////////////// + +type listDevicePoliciesRequest struct { + Token string `url:"token"` +} + +func (r *listDevicePoliciesRequest) deviceAuthToken() string { + return r.Token +} + +type listDevicePoliciesResponse struct { + Err error `json:"error,omitempty"` + Policies []*fleet.HostPolicy `json:"policies"` +} + +func (r listDevicePoliciesResponse) error() error { return r.Err } + +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")) + return getHostResponse{Err: err}, nil + } + + data, err := svc.ListDevicePolicies(ctx, host) + if err != nil { + return listDevicePoliciesResponse{Err: err}, nil + } + + return listDevicePoliciesResponse{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/handler.go b/server/service/handler.go index d5b771606619..203293e5da75 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", 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) +}