mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
4d7f947529
GET /hosts API endpoint can now populate policies with populate_policies=true query parameter. #16242 # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] Manual QA for all new/changed functionality
510 lines
15 KiB
Go
510 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestListOptionsFromRequest(t *testing.T) {
|
|
listOptionsTests := []struct {
|
|
// url string to parse
|
|
url string
|
|
// expected list options
|
|
listOptions fleet.ListOptions
|
|
// should cause a BadRequest error
|
|
shouldErr400 bool
|
|
}{
|
|
// both params provided
|
|
{
|
|
url: "/foo?page=1&per_page=10",
|
|
listOptions: fleet.ListOptions{Page: 1, PerPage: 10},
|
|
},
|
|
// only per_page (page should default to 0)
|
|
{
|
|
url: "/foo?per_page=10",
|
|
listOptions: fleet.ListOptions{Page: 0, PerPage: 10},
|
|
},
|
|
// only page (per_page should default to defaultPerPage
|
|
{
|
|
url: "/foo?page=10",
|
|
listOptions: fleet.ListOptions{Page: 10, PerPage: defaultPerPage},
|
|
},
|
|
// no params provided (defaults to empty ListOptions indicating
|
|
// unlimited)
|
|
{
|
|
url: "/foo?unrelated=foo",
|
|
listOptions: fleet.ListOptions{},
|
|
},
|
|
|
|
// Both order params provided
|
|
{
|
|
url: "/foo?order_key=foo&order_direction=desc",
|
|
listOptions: fleet.ListOptions{OrderKey: "foo", OrderDirection: fleet.OrderDescending},
|
|
},
|
|
// Both order params provided (asc)
|
|
{
|
|
url: "/foo?order_key=bar&order_direction=asc",
|
|
listOptions: fleet.ListOptions{OrderKey: "bar", OrderDirection: fleet.OrderAscending},
|
|
},
|
|
// Default order direction
|
|
{
|
|
url: "/foo?order_key=foo",
|
|
listOptions: fleet.ListOptions{OrderKey: "foo", OrderDirection: fleet.OrderAscending},
|
|
},
|
|
|
|
// All params defined
|
|
{
|
|
url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100&after=bar",
|
|
listOptions: fleet.ListOptions{
|
|
OrderKey: "foo",
|
|
OrderDirection: fleet.OrderDescending,
|
|
Page: 1,
|
|
PerPage: 100,
|
|
After: "bar",
|
|
},
|
|
},
|
|
|
|
// various 400 error cases
|
|
{
|
|
url: "/foo?page=foo&per_page=10",
|
|
shouldErr400: true,
|
|
},
|
|
{
|
|
url: "/foo?page=1&per_page=foo",
|
|
shouldErr400: true,
|
|
},
|
|
{
|
|
url: "/foo?page=-1",
|
|
shouldErr400: true,
|
|
},
|
|
{
|
|
url: "/foo?page=1&per_page=-10",
|
|
shouldErr400: true,
|
|
},
|
|
// order_direction without order_key
|
|
{
|
|
url: "/foo?page=1&order_direction=desc",
|
|
shouldErr400: true,
|
|
},
|
|
// bad order_direction
|
|
{
|
|
url: "/foo?&order_direction=foo&order_key=foo",
|
|
shouldErr400: true,
|
|
},
|
|
// after without order_key
|
|
{
|
|
url: "/foo?page=1&after=foo",
|
|
shouldErr400: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range listOptionsTests {
|
|
t.Run(
|
|
tt.url, func(t *testing.T) {
|
|
urlStruct, _ := url.Parse(tt.url)
|
|
req := &http.Request{URL: urlStruct}
|
|
opt, err := listOptionsFromRequest(req)
|
|
|
|
if tt.shouldErr400 {
|
|
assert.NotNil(t, err)
|
|
var be *fleet.BadRequestError
|
|
require.ErrorAs(t, err, &be)
|
|
return
|
|
}
|
|
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.listOptions, opt)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestHostListOptionsFromRequest(t *testing.T) {
|
|
hostListOptionsTests := map[string]struct {
|
|
// url string to parse
|
|
url string
|
|
// expected options
|
|
hostListOptions fleet.HostListOptions
|
|
// expected error message, if any
|
|
errorMessage string
|
|
}{
|
|
"no params passed": {
|
|
url: "/foo",
|
|
hostListOptions: fleet.HostListOptions{},
|
|
},
|
|
"embedded list options params defined": {
|
|
url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100",
|
|
hostListOptions: fleet.HostListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "foo",
|
|
OrderDirection: fleet.OrderDescending,
|
|
Page: 1,
|
|
PerPage: 100,
|
|
},
|
|
},
|
|
},
|
|
"all params defined": {
|
|
url: "/foo?order_key=foo&order_direction=asc&page=10&per_page=1&device_mapping=T&additional_info_filters" +
|
|
"=filter1,filter2&status=new&team_id=2&policy_id=3&policy_response=passing&software_id=4&os_id=5" +
|
|
"&os_name=osName&os_version=osVersion&os_version_id=5&disable_failing_policies=1&macos_settings=verified" +
|
|
"&macos_settings_disk_encryption=enforcing&os_settings=pending&os_settings_disk_encryption=failed" +
|
|
"&bootstrap_package=installed&mdm_id=6&mdm_name=mdmName&mdm_enrollment_status=automatic" +
|
|
"&munki_issue_id=7&low_disk_space=99&vulnerability=CVE-2023-42887&populate_policies=true",
|
|
hostListOptions: fleet.HostListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "foo",
|
|
OrderDirection: fleet.OrderAscending,
|
|
Page: 10,
|
|
PerPage: 1,
|
|
},
|
|
DeviceMapping: true,
|
|
AdditionalFilters: []string{"filter1", "filter2"},
|
|
StatusFilter: fleet.StatusNew,
|
|
TeamFilter: ptr.Uint(2),
|
|
PolicyIDFilter: ptr.Uint(3),
|
|
PolicyResponseFilter: ptr.Bool(true),
|
|
SoftwareIDFilter: ptr.Uint(4),
|
|
OSIDFilter: ptr.Uint(5),
|
|
OSVersionIDFilter: ptr.Uint(5),
|
|
OSNameFilter: ptr.String("osName"),
|
|
OSVersionFilter: ptr.String("osVersion"),
|
|
DisableFailingPolicies: true,
|
|
MacOSSettingsFilter: fleet.OSSettingsVerified,
|
|
MacOSSettingsDiskEncryptionFilter: fleet.DiskEncryptionEnforcing,
|
|
OSSettingsFilter: fleet.OSSettingsPending,
|
|
OSSettingsDiskEncryptionFilter: fleet.DiskEncryptionFailed,
|
|
MDMBootstrapPackageFilter: (*fleet.MDMBootstrapPackageStatus)(ptr.String(string(fleet.MDMBootstrapPackageInstalled))),
|
|
MDMIDFilter: ptr.Uint(6),
|
|
MDMNameFilter: ptr.String("mdmName"),
|
|
MDMEnrollmentStatusFilter: fleet.MDMEnrollStatusAutomatic,
|
|
MunkiIssueIDFilter: ptr.Uint(7),
|
|
LowDiskSpaceFilter: ptr.Int(99),
|
|
VulnerabilityFilter: ptr.String("CVE-2023-42887"),
|
|
PopulatePolicies: true,
|
|
},
|
|
},
|
|
"policy_id and policy_response params (for coverage)": {
|
|
url: "/foo?policy_id=100&policy_response=failing",
|
|
hostListOptions: fleet.HostListOptions{
|
|
PolicyIDFilter: ptr.Uint(100),
|
|
PolicyResponseFilter: ptr.Bool(false),
|
|
},
|
|
},
|
|
"error in page (embedded list options)": {
|
|
url: "/foo?page=-1",
|
|
errorMessage: "negative page value",
|
|
},
|
|
"error in status": {
|
|
url: "/foo?status=foo",
|
|
errorMessage: "Invalid status",
|
|
},
|
|
"error in team_id (number too large)": {
|
|
url: "/foo?team_id=9,223,372,036,854,775,808",
|
|
errorMessage: "Invalid team_id",
|
|
},
|
|
"error in team_id (not a number)": {
|
|
url: "/foo?team_id=foo",
|
|
errorMessage: "Invalid team_id",
|
|
},
|
|
"error in policy_id": {
|
|
url: "/foo?policy_id=foo",
|
|
errorMessage: "Invalid policy_id",
|
|
},
|
|
"error when policy_response specified without policy_id": {
|
|
url: "/foo?policy_response=passing",
|
|
errorMessage: "Missing policy_id",
|
|
},
|
|
"error in policy_response (invalid option)": {
|
|
url: "/foo?policy_id=1&policy_response=foo",
|
|
errorMessage: "Invalid policy_response",
|
|
},
|
|
"error in software_id": {
|
|
url: "/foo?software_id=foo",
|
|
errorMessage: "Invalid software_id",
|
|
},
|
|
"error in os_id": {
|
|
url: "/foo?os_id=foo",
|
|
errorMessage: "Invalid os_id",
|
|
},
|
|
"error in disable_failing_policies": {
|
|
url: "/foo?disable_failing_policies=foo",
|
|
errorMessage: "Invalid disable_failing_policies",
|
|
},
|
|
"error in device_mapping": {
|
|
url: "/foo?device_mapping=foo",
|
|
errorMessage: "Invalid device_mapping",
|
|
},
|
|
"error in mdm_id": {
|
|
url: "/foo?mdm_id=foo",
|
|
errorMessage: "Invalid mdm_id",
|
|
},
|
|
"error in mdm_enrollment_status (invalid option)": {
|
|
url: "/foo?mdm_enrollment_status=foo",
|
|
errorMessage: "Invalid mdm_enrollment_status",
|
|
},
|
|
"error in macos_settings (invalid option)": {
|
|
url: "/foo?macos_settings=foo",
|
|
errorMessage: "Invalid macos_settings",
|
|
},
|
|
"error in macos_settings_disk_encryption (invalid option)": {
|
|
url: "/foo?macos_settings_disk_encryption=foo",
|
|
errorMessage: "Invalid macos_settings_disk_encryption",
|
|
},
|
|
"error in os_settings (invalid option)": {
|
|
url: "/foo?os_settings=foo",
|
|
errorMessage: "Invalid os_settings",
|
|
},
|
|
"error in os_settings_disk_encryption (invalid option)": {
|
|
url: "/foo?os_settings_disk_encryption=foo",
|
|
errorMessage: "Invalid os_settings_disk_encryption",
|
|
},
|
|
"error in bootstrap_package (invalid option)": {
|
|
url: "/foo?bootstrap_package=foo",
|
|
errorMessage: "Invalid bootstrap_package",
|
|
},
|
|
"error in munki_issue_id": {
|
|
url: "/foo?munki_issue_id=foo",
|
|
errorMessage: "Invalid munki_issue_id",
|
|
},
|
|
"error in low_disk_space (not a number)": {
|
|
url: "/foo?low_disk_space=foo",
|
|
errorMessage: "Invalid low_disk_space",
|
|
},
|
|
"error in low_disk_space (too low)": {
|
|
url: "/foo?low_disk_space=0",
|
|
errorMessage: "Invalid low_disk_space",
|
|
},
|
|
"error in low_disk_space (too high)": {
|
|
url: "/foo?low_disk_space=101",
|
|
errorMessage: "Invalid low_disk_space",
|
|
},
|
|
"error in os_name/os_version (os_name missing)": {
|
|
url: "/foo?os_version=1.0",
|
|
errorMessage: "Invalid os_name",
|
|
},
|
|
"error in os_name/os_version (os_version missing)": {
|
|
url: "/foo?os_name=foo",
|
|
errorMessage: "Invalid os_version",
|
|
},
|
|
"negative software_id": {
|
|
url: "/foo?software_id=-10",
|
|
errorMessage: "Invalid software_id",
|
|
},
|
|
"negative software_version_id": {
|
|
url: "/foo?software_version_id=-10",
|
|
errorMessage: "Invalid software_version_id",
|
|
},
|
|
"negative software_title_id": {
|
|
url: "/foo?software_title_id=-10",
|
|
errorMessage: "Invalid software_title_id",
|
|
},
|
|
"software_title_id too big": {
|
|
url: "/foo?software_title_id=" + fmt.Sprint(1<<33),
|
|
errorMessage: "Invalid software_title_id",
|
|
},
|
|
"software_version_id can be > 32bits": {
|
|
url: "/foo?software_version_id=" + fmt.Sprint(1<<33),
|
|
hostListOptions: fleet.HostListOptions{
|
|
SoftwareVersionIDFilter: ptr.Uint(1 << 33),
|
|
},
|
|
},
|
|
"good software_version_id": {
|
|
url: "/foo?software_version_id=1",
|
|
hostListOptions: fleet.HostListOptions{
|
|
SoftwareVersionIDFilter: ptr.Uint(1),
|
|
},
|
|
},
|
|
"good software_title_id": {
|
|
url: "/foo?software_title_id=1",
|
|
hostListOptions: fleet.HostListOptions{
|
|
SoftwareTitleIDFilter: ptr.Uint(1),
|
|
},
|
|
},
|
|
"invalid combination software_title_id and software_version_id": {
|
|
url: "/foo?software_title_id=1&software_version_id=2",
|
|
errorMessage: "The combination of software_version_id and software_title_id is not allowed",
|
|
},
|
|
"invalid combination software_id and software_version_id": {
|
|
url: "/foo?software_id=1&software_version_id=2",
|
|
errorMessage: "The combination of software_id and software_version_id is not allowed",
|
|
},
|
|
"invalid populate_policies": {
|
|
url: "/foo?populate_policies=foo",
|
|
errorMessage: "populate_policies",
|
|
},
|
|
}
|
|
|
|
for name, tt := range hostListOptionsTests {
|
|
t.Run(
|
|
name, func(t *testing.T) {
|
|
urlStruct, _ := url.Parse(tt.url)
|
|
req := &http.Request{URL: urlStruct}
|
|
opt, err := hostListOptionsFromRequest(req)
|
|
|
|
if tt.errorMessage != "" {
|
|
assert.NotNil(t, err)
|
|
var be *fleet.BadRequestError
|
|
require.ErrorAs(t, err, &be)
|
|
require.Contains(t, err.Error(), tt.errorMessage)
|
|
return
|
|
}
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.hostListOptions, opt)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestCarveListOptionsFromRequest(t *testing.T) {
|
|
carveListOptionsTests := map[string]struct {
|
|
// url string to parse
|
|
url string
|
|
// expected options
|
|
carveListOptions fleet.CarveListOptions
|
|
// expected error message, if any
|
|
errorMessage string
|
|
}{
|
|
"no params passed": {
|
|
url: "/foo",
|
|
carveListOptions: fleet.CarveListOptions{},
|
|
},
|
|
"embedded list options params defined": {
|
|
url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100",
|
|
carveListOptions: fleet.CarveListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "foo",
|
|
OrderDirection: fleet.OrderDescending,
|
|
Page: 1,
|
|
PerPage: 100,
|
|
},
|
|
},
|
|
},
|
|
"all params defined": {
|
|
url: "/foo?order_key=foo&order_direction=asc&page=10&per_page=1&expired=true",
|
|
carveListOptions: fleet.CarveListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "foo",
|
|
OrderDirection: fleet.OrderAscending,
|
|
Page: 10,
|
|
PerPage: 1,
|
|
},
|
|
Expired: true,
|
|
},
|
|
},
|
|
"error in page (embedded list options)": {
|
|
url: "/foo?page=-1",
|
|
errorMessage: "negative page value",
|
|
},
|
|
"error in expired": {
|
|
url: "/foo?expired=foo",
|
|
errorMessage: "Invalid expired",
|
|
},
|
|
}
|
|
|
|
for name, tt := range carveListOptionsTests {
|
|
t.Run(
|
|
name, func(t *testing.T) {
|
|
urlStruct, _ := url.Parse(tt.url)
|
|
req := &http.Request{URL: urlStruct}
|
|
opt, err := carveListOptionsFromRequest(req)
|
|
|
|
if tt.errorMessage != "" {
|
|
assert.NotNil(t, err)
|
|
var be *fleet.BadRequestError
|
|
require.ErrorAs(t, err, &be)
|
|
assert.True(
|
|
t, strings.Contains(err.Error(), tt.errorMessage),
|
|
"error message '%v' should contain '%v'", err.Error(), tt.errorMessage,
|
|
)
|
|
return
|
|
}
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.carveListOptions, opt)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestUserListOptionsFromRequest(t *testing.T) {
|
|
userListOptionsTests := map[string]struct {
|
|
// url string to parse
|
|
url string
|
|
// expected options
|
|
userListOptions fleet.UserListOptions
|
|
// expected error message, if any
|
|
errorMessage string
|
|
}{
|
|
"no params passed": {
|
|
url: "/foo",
|
|
userListOptions: fleet.UserListOptions{},
|
|
},
|
|
"embedded list options params defined": {
|
|
url: "/foo?order_key=foo&order_direction=desc&page=1&per_page=100",
|
|
userListOptions: fleet.UserListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "foo",
|
|
OrderDirection: fleet.OrderDescending,
|
|
Page: 1,
|
|
PerPage: 100,
|
|
},
|
|
},
|
|
},
|
|
"all params defined": {
|
|
url: "/foo?order_key=foo&order_direction=asc&page=10&per_page=1&team_id=1",
|
|
userListOptions: fleet.UserListOptions{
|
|
ListOptions: fleet.ListOptions{
|
|
OrderKey: "foo",
|
|
OrderDirection: fleet.OrderAscending,
|
|
Page: 10,
|
|
PerPage: 1,
|
|
},
|
|
TeamID: 1,
|
|
},
|
|
},
|
|
"error in page (embedded list options)": {
|
|
url: "/foo?page=-1",
|
|
errorMessage: "negative page value",
|
|
},
|
|
"error in team_id (negative_number)": {
|
|
url: "/foo?team_id=-1",
|
|
errorMessage: "Invalid team_id",
|
|
},
|
|
"error in team_id (not a number)": {
|
|
url: "/foo?team_id=foo",
|
|
errorMessage: "Invalid team_id",
|
|
},
|
|
}
|
|
|
|
for name, tt := range userListOptionsTests {
|
|
t.Run(
|
|
name, func(t *testing.T) {
|
|
urlStruct, _ := url.Parse(tt.url)
|
|
req := &http.Request{URL: urlStruct}
|
|
opt, err := userListOptionsFromRequest(req)
|
|
|
|
if tt.errorMessage != "" {
|
|
assert.NotNil(t, err)
|
|
var be *fleet.BadRequestError
|
|
require.ErrorAs(t, err, &be)
|
|
assert.True(
|
|
t, strings.Contains(err.Error(), tt.errorMessage),
|
|
"error message '%v' should contain '%v'", err.Error(), tt.errorMessage,
|
|
)
|
|
return
|
|
}
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.userListOptions, opt)
|
|
},
|
|
)
|
|
}
|
|
}
|