Add new endpoints to retrieve device information by orbit identifier (#4531)

This commit is contained in:
Martin Angers 2022-03-09 16:13:56 -05:00 committed by GitHub
parent 81bc22bb74
commit a1c67547b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 472 additions and 132 deletions

View File

@ -0,0 +1 @@
* Add device-authenticated endpoints to retrieve read-only information about the current device.

View File

@ -70,6 +70,20 @@ func (a *Authorizer) SkipAuthorization(ctx context.Context) {
}
}
// IsAlreadyAuthorized returns true if the context has already authorized. This
// may be useful to skip authorization checks in the Service layer when e.g.
// the current request has already been authorized due to its different
// authentication method, such as the device authentication token which allows
// reading the corresponding host's information even if there is no user
// associated with the request (so typical user-based authorization checks
// would fail).
func (a *Authorizer) IsAlreadyAuthorized(ctx context.Context) bool {
if authctx, ok := authz_ctx.FromContext(ctx); ok {
return authctx.Checked()
}
return false
}
// Authorize checks authorization for the provided object, and action,
// retrieving the subject from the context.
//

View File

@ -709,8 +709,8 @@ func (ds *Datastore) EnrollHost(ctx context.Context, osqueryHostID, nodeKey stri
return &host, nil
}
// GetContextTryStmt will attempt to run sqlx.GetContext on a cached statement if available, resorting to ds.reader.
func (ds *Datastore) GetContextTryStmt(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
// getContextTryStmt will attempt to run sqlx.GetContext on a cached statement if available, resorting to ds.reader.
func (ds *Datastore) getContextTryStmt(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
var err error
//nolint the statements are closed in Datastore.Close.
if stmt := ds.loadOrPrepareStmt(ctx, query); stmt != nil {
@ -727,7 +727,32 @@ func (ds *Datastore) LoadHostByNodeKey(ctx context.Context, nodeKey string) (*fl
query := `SELECT * FROM hosts WHERE node_key = ?`
var host fleet.Host
switch err := ds.GetContextTryStmt(ctx, &host, query, nodeKey); {
switch err := ds.getContextTryStmt(ctx, &host, query, nodeKey); {
case err == nil:
return &host, nil
case errors.Is(err, sql.ErrNoRows):
return nil, ctxerr.Wrap(ctx, notFound("Host"))
default:
return nil, ctxerr.Wrap(ctx, err, "find host")
}
}
// LoadHostByDeviceAuthToken loads the whole host identified by the device auth token.
// If the token is invalid it returns a NotFoundError.
func (ds *Datastore) LoadHostByDeviceAuthToken(ctx context.Context, authToken string) (*fleet.Host, error) {
const query = `
SELECT
h.*
FROM
host_device_auth hda
INNER JOIN
hosts h
ON
hda.host_id = h.id
WHERE hda.token = ?`
var host fleet.Host
switch err := sqlx.GetContext(ctx, ds.reader, &host, query, authToken); {
case err == nil:
return &host, nil
case errors.Is(err, sql.ErrNoRows):

View File

@ -2,6 +2,7 @@ package mysql
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
@ -111,6 +112,7 @@ func TestHosts(t *testing.T) {
{"HostLite", testHostsLite},
{"UpdateOsqueryIntervals", testUpdateOsqueryIntervals},
{"UpdateRefetchRequested", testUpdateRefetchRequested},
{"LoadHostByDeviceAuthToken", testHostsLoadHostByDeviceAuthToken},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@ -3688,3 +3690,30 @@ func testHostsSaveHostUsers(t *testing.T, ds *Datastore) {
require.Len(t, host.Users, 2)
test.ElementsMatchSkipID(t, users, host.Users)
}
func testHostsLoadHostByDeviceAuthToken(t *testing.T, ds *Datastore) {
host, err := ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: "1",
UUID: "1",
Hostname: "foo.local",
PrimaryIP: "192.168.1.1",
PrimaryMac: "30-65-EC-6F-C4-58",
})
require.NoError(t, err)
validToken := "abcd"
_, err = ds.writer.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, host.ID, validToken)
require.NoError(t, err)
_, err = ds.LoadHostByDeviceAuthToken(context.Background(), "nosuchtoken")
require.Error(t, err)
assert.ErrorIs(t, err, sql.ErrNoRows)
h, err := ds.LoadHostByDeviceAuthToken(context.Background(), validToken)
require.NoError(t, err)
require.Equal(t, host.ID, h.ID)
}

View File

@ -16,7 +16,7 @@ func Up_20220307104655(tx *sql.Tx) error {
host_id int(10) UNSIGNED NOT NULL,
token VARCHAR(255) NOT NULL,
PRIMARY KEY (host_id),
INDEX idx_host_device_auth_token (token)
UNIQUE INDEX idx_host_device_auth_token (token)
);
`
if _, err := tx.Exec(hostDeviceAuthTable); err != nil {

View File

@ -129,7 +129,7 @@ CREATE TABLE `host_device_auth` (
`host_id` int(10) unsigned NOT NULL,
`token` varchar(255) NOT NULL,
PRIMARY KEY (`host_id`),
KEY `idx_host_device_auth_token` (`token`)
UNIQUE KEY `idx_host_device_auth_token` (`token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40101 SET @saved_cs_client = @@character_set_client */;

View File

@ -202,6 +202,10 @@ type Datastore interface {
CountHostsInLabel(ctx context.Context, filter TeamFilter, lid uint, opt HostListOptions) (int, error)
ListHostDeviceMapping(ctx context.Context, id uint) ([]*HostDeviceMapping, error)
// LoadHostByDeviceAuthToken loads the host identified by the device auth token.
// If the token is invalid it returns a NotFoundError.
LoadHostByDeviceAuthToken(ctx context.Context, authToken string) (*Host, error)
// ListPoliciesForHost lists the policies that a host will check and whether they are passing
ListPoliciesForHost(ctx context.Context, host *Host) ([]*HostPolicy, error)

View File

@ -234,6 +234,10 @@ type Service interface {
///////////////////////////////////////////////////////////////////////////////
// HostService
// AuthenticateDevice loads host identified by the device's auth token.
// Returns an error if the auth token doesn't exist.
AuthenticateDevice(ctx context.Context, authToken string) (host *Host, debug bool, err error)
ListHosts(ctx context.Context, opt HostListOptions) (hosts []*Host, err error)
GetHost(ctx context.Context, id uint) (host *HostDetail, err error)
GetHostSummary(ctx context.Context, teamID *uint, platform *string) (summary *HostSummary, err error)

View File

@ -170,6 +170,8 @@ type CountHostsInLabelFunc func(ctx context.Context, filter fleet.TeamFilter, li
type ListHostDeviceMappingFunc func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error)
type LoadHostByDeviceAuthTokenFunc func(ctx context.Context, authToken string) (*fleet.Host, error)
type ListPoliciesForHostFunc func(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error)
type GetMunkiVersionFunc func(ctx context.Context, hostID uint) (string, error)
@ -620,6 +622,9 @@ type DataStore struct {
ListHostDeviceMappingFunc ListHostDeviceMappingFunc
ListHostDeviceMappingFuncInvoked bool
LoadHostByDeviceAuthTokenFunc LoadHostByDeviceAuthTokenFunc
LoadHostByDeviceAuthTokenFuncInvoked bool
ListPoliciesForHostFunc ListPoliciesForHostFunc
ListPoliciesForHostFuncInvoked bool
@ -1334,6 +1339,11 @@ func (s *DataStore) ListHostDeviceMapping(ctx context.Context, id uint) ([]*flee
return s.ListHostDeviceMappingFunc(ctx, id)
}
func (s *DataStore) LoadHostByDeviceAuthToken(ctx context.Context, authToken string) (*fleet.Host, error) {
s.LoadHostByDeviceAuthTokenFuncInvoked = true
return s.LoadHostByDeviceAuthTokenFunc(ctx, authToken)
}
func (s *DataStore) ListPoliciesForHost(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
s.ListPoliciesForHostFuncInvoked = true
return s.ListPoliciesForHostFunc(ctx, host)

View File

@ -148,6 +148,10 @@ type carveBeginRequest struct {
RequestId string `json:"request_id"`
}
func (r *carveBeginRequest) hostNodeKey() string {
return r.NodeKey
}
type carveBeginResponse struct {
SessionId string `json:"session_id"`
Success bool `json:"success,omitempty"`

144
server/service/devices.go Normal file
View File

@ -0,0 +1,144 @@
package service
import (
"context"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/fleet"
)
/////////////////////////////////////////////////////////////////////////////////
// Get Current Device's Host
/////////////////////////////////////////////////////////////////////////////////
type getDeviceHostRequest struct {
Token string `url:"token"`
}
func (r *getDeviceHostRequest) deviceAuthToken() string {
return r.Token
}
func getDeviceHostEndpoint(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
}
// must still load the full host details, as it returns more information
hostDetails, err := svc.GetHost(ctx, host.ID)
if err != nil {
return getHostResponse{Err: err}, nil
}
resp, err := hostDetailResponseForHost(ctx, svc, hostDetails)
if err != nil {
return getHostResponse{Err: err}, nil
}
return getHostResponse{Host: resp}, nil
}
// AuthenticateDevice returns the host identified by the device authentication
// token, along with a boolean indicating if debug logging is enabled for that
// host.
func (svc *Service) AuthenticateDevice(ctx context.Context, authToken string) (*fleet.Host, bool, error) {
// skipauth: Authorization is currently for user endpoints only.
svc.authz.SkipAuthorization(ctx)
if authToken == "" {
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: missing device authentication token"))
}
host, err := svc.ds.LoadHostByDeviceAuthToken(ctx, authToken)
switch {
case err == nil:
// OK
case fleet.IsNotFound(err):
return nil, false, ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("authentication error: invalid device authentication token"))
default:
return nil, false, ctxerr.Wrap(ctx, err, "authenticate device")
}
return host, svc.debugEnabledForHost(ctx, host.ID), nil
}
/////////////////////////////////////////////////////////////////////////////////
// Refetch Current Device's Host
/////////////////////////////////////////////////////////////////////////////////
type refetchDeviceHostRequest struct {
Token string `url:"token"`
}
func (r *refetchDeviceHostRequest) deviceAuthToken() string {
return r.Token
}
func refetchDeviceHostEndpoint(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
}
err := svc.RefetchHost(ctx, host.ID)
if err != nil {
return refetchHostResponse{Err: err}, nil
}
return refetchHostResponse{}, nil
}
////////////////////////////////////////////////////////////////////////////////
// List Current Device's Host Device Mappings
////////////////////////////////////////////////////////////////////////////////
type listDeviceHostDeviceMappingRequest struct {
Token string `url:"token"`
}
func (r *listDeviceHostDeviceMappingRequest) deviceAuthToken() string {
return r.Token
}
func listDeviceHostDeviceMappingEndpoint(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
}
dms, err := svc.ListHostDeviceMapping(ctx, host.ID)
if err != nil {
return listHostDeviceMappingResponse{Err: err}, nil
}
return listHostDeviceMappingResponse{HostID: host.ID, DeviceMapping: dms}, nil
}
////////////////////////////////////////////////////////////////////////////////
// Get Current Device's Macadmins
////////////////////////////////////////////////////////////////////////////////
type getDeviceMacadminsDataRequest struct {
Token string `url:"token"`
}
func (r *getDeviceMacadminsDataRequest) deviceAuthToken() string {
return r.Token
}
func getDeviceMacadminsDataEndpoint(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.MacadminsData(ctx, host.ID)
if err != nil {
return getMacadminsDataResponse{Err: err}, nil
}
return getMacadminsDataResponse{Macadmins: data}, nil
}

View File

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"reflect"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/fleet"
@ -37,6 +36,47 @@ func instrumentHostLogger(ctx context.Context, extras ...interface{}) {
)
}
func authenticatedDevice(svc fleet.Service, logger log.Logger, next endpoint.Endpoint) endpoint.Endpoint {
authDeviceFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
token, err := getDeviceAuthToken(request)
if err != nil {
return nil, err
}
host, debug, err := svc.AuthenticateDevice(ctx, token)
if err != nil {
logging.WithErr(ctx, err)
return nil, err
}
hlogger := log.With(logger, "host-id", host.ID)
if debug {
logJSON(hlogger, request, "request")
}
ctx = hostctx.NewContext(ctx, host)
instrumentHostLogger(ctx)
resp, err := next(ctx, request)
if err != nil {
return nil, err
}
if debug {
logJSON(hlogger, request, "response")
}
return resp, nil
}
return logged(authDeviceFunc)
}
func getDeviceAuthToken(r interface{}) (string, error) {
if dat, ok := r.(interface{ deviceAuthToken() string }); ok {
return dat.deviceAuthToken(), nil
}
return "", fleet.NewAuthRequiredError("request type does not implement deviceAuthToken method. This is likely a Fleet programmer error.")
}
// authenticatedHost wraps an endpoint, checks the validity of the node_key
// provided in the request, and attaches the corresponding osquery host to the
// context for the request
@ -75,29 +115,12 @@ func authenticatedHost(svc fleet.Service, logger log.Logger, next endpoint.Endpo
}
func getNodeKey(r interface{}) (string, error) {
// Retrieve node key by reflection (note that our options here
// are limited by the fact that request is an interface{})
v := reflect.ValueOf(r)
if v.Kind() == reflect.Ptr {
v = v.Elem()
if hnk, ok := r.(interface{ hostNodeKey() string }); ok {
return hnk.hostNodeKey(), nil
}
if v.Kind() != reflect.Struct {
return "", osqueryError{
message: "request type is not struct. This is likely a Fleet programmer error.",
}
return "", osqueryError{
message: "request type does not implement hostNodeKey method. This is likely a Fleet programmer error.",
}
nodeKeyField := v.FieldByName("NodeKey")
if !nodeKeyField.IsValid() {
return "", osqueryError{
message: "request struct missing NodeKey. This is likely a Fleet programmer error.",
}
}
if nodeKeyField.Kind() != reflect.String {
return "", osqueryError{
message: "NodeKey is not a string. This is likely a Fleet programmer error.",
}
}
return nodeKeyField.String(), nil
}
// authenticatedUser wraps an endpoint, requires that the Fleet user is

View File

@ -125,67 +125,12 @@ import (
// }
// }
// TestGetNodeKey tests the reflection logic for pulling the node key from
// various (fake) request types
func TestGetNodeKey(t *testing.T) {
type Foo struct {
Foo string
NodeKey string
}
type Bar struct {
Bar string
NodeKey string
}
type Nope struct {
Nope string
}
type Almost struct {
NodeKey int
}
getNodeKeyTests := []struct {
i interface{}
expectKey string
shouldErr bool
}{
{
i: Foo{Foo: "foo", NodeKey: "fookey"},
expectKey: "fookey",
shouldErr: false,
},
{
i: Bar{Bar: "bar", NodeKey: "barkey"},
expectKey: "barkey",
shouldErr: false,
},
{
i: Nope{Nope: "nope"},
expectKey: "",
shouldErr: true,
},
{
i: Almost{NodeKey: 10},
expectKey: "",
shouldErr: true,
},
}
for _, tt := range getNodeKeyTests {
t.Run("", func(t *testing.T) {
key, err := getNodeKey(tt.i)
assert.Equal(t, tt.expectKey, key)
if tt.shouldErr {
assert.IsType(t, osqueryError{}, err)
} else {
assert.Nil(t, err)
}
})
}
type testNodeKeyRequest struct {
NodeKey string
}
func (r *testNodeKeyRequest) hostNodeKey() string { return r.NodeKey }
func TestAuthenticatedHost(t *testing.T) {
ds := new(mock.Store)
svc := newTestService(ds, nil, nil)
@ -237,7 +182,7 @@ func TestAuthenticatedHost(t *testing.T) {
for _, tt := range authenticatedHostTests {
t.Run("", func(t *testing.T) {
r := struct{ NodeKey string }{NodeKey: tt.nodeKey}
r := &testNodeKeyRequest{NodeKey: tt.nodeKey}
_, err := endpoint(context.Background(), r)
if tt.shouldErr {
assert.IsType(t, osqueryError{}, err)

View File

@ -289,6 +289,19 @@ type authEndpointer struct {
customMiddleware []endpoint.Middleware
}
func newDeviceAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts []kithttp.ServerOption, r *mux.Router, versions ...string) *authEndpointer {
authFunc := func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
return authenticatedDevice(svc, logger, next)
}
return &authEndpointer{
svc: svc,
opts: opts,
r: r,
authFunc: authFunc,
versions: versions,
}
}
func newUserAuthenticatedEndpointer(svc fleet.Service, opts []kithttp.ServerOption, r *mux.Router, versions ...string) *authEndpointer {
return &authEndpointer{
svc: svc,

View File

@ -333,6 +333,13 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.GET("/api/_version_/fleet/status/result_store", statusResultStoreEndpoint, nil)
ue.GET("/api/_version_/fleet/status/live_query", statusLiveQueryEndpoint, nil)
// device-authenticated endpoints
de := newDeviceAuthenticatedEndpointer(svc, logger, opts, r, "v1")
de.GET("/api/_version_/fleet/device/{token}", getDeviceHostEndpoint, getDeviceHostRequest{})
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{})
// host-authenticated endpoints
he := newHostAuthenticatedEndpointer(svc, logger, opts, r, "v1")
he.POST("/api/_version_/osquery/config", getClientConfigEndpoint, getClientConfigRequest{})

View File

@ -265,10 +265,13 @@ func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service
}
func (svc *Service) GetHost(ctx context.Context, id uint) (*fleet.HostDetail, error) {
// First ensure the user has access to list hosts, then check the specific
// host once team_id is loaded.
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
alreadyAuthd := svc.authz.IsAlreadyAuthorized(ctx)
if !alreadyAuthd {
// First ensure the user has access to list hosts, then check the specific
// host once team_id is loaded.
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
}
host, err := svc.ds.Host(ctx, id, false)
@ -276,9 +279,11 @@ func (svc *Service) GetHost(ctx context.Context, id uint) (*fleet.HostDetail, er
return nil, ctxerr.Wrap(ctx, err, "get host")
}
// Authorize again with team loaded now that we have team_id
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return nil, err
if !alreadyAuthd {
// Authorize again with team loaded now that we have team_id
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return nil, err
}
}
return svc.getHostDetails(ctx, host)
@ -545,22 +550,24 @@ func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}
func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return err
if !svc.authz.IsAlreadyAuthorized(ctx) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return err
}
host, err := svc.ds.HostLite(ctx, id)
if err != nil {
return ctxerr.Wrap(ctx, err, "find host for refetch")
}
// We verify fleet.ActionRead instead of fleet.ActionWrite because we want to allow
// observers to be able to refetch hosts.
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return err
}
}
host, err := svc.ds.HostLite(ctx, id)
if err != nil {
return ctxerr.Wrap(ctx, err, "find host for refetch")
}
// We verify fleet.ActionRead instead of fleet.ActionWrite because we want to allow
// observers to be able to refetch hosts.
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return err
}
if err := svc.ds.UpdateHostRefetchRequested(ctx, host.ID, true); err != nil {
if err := svc.ds.UpdateHostRefetchRequested(ctx, id, true); err != nil {
return ctxerr.Wrap(ctx, err, "save host")
}
@ -659,18 +666,20 @@ func listHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc
}
func (svc *Service) ListHostDeviceMapping(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
if !svc.authz.IsAlreadyAuthorized(ctx) {
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, "get host")
}
host, err := svc.ds.HostLite(ctx, id)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get host")
}
// Authorize again with team loaded now that we have team_id
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return nil, err
// Authorize again with team loaded now that we have team_id
if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
return nil, err
}
}
return svc.ds.ListHostDeviceMapping(ctx, id)
@ -701,17 +710,19 @@ func getMacadminsDataEndpoint(ctx context.Context, request interface{}, svc flee
}
func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.MacadminsData, error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
if !svc.authz.IsAlreadyAuthorized(ctx) {
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, "find host for macadmins")
}
host, err := svc.ds.HostLite(ctx, id)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "find host for macadmins")
}
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
}
}
var munkiInfo *fleet.HostMunkiInfo

View File

@ -46,13 +46,9 @@ func (s *integrationTestSuite) TearDownTest() {
filter := fleet.TeamFilter{User: &u}
hosts, err := s.ds.ListHosts(ctx, filter, fleet.HostListOptions{})
require.NoError(t, err)
var ids []uint
for _, host := range hosts {
ids = append(ids, host.ID)
require.NoError(t, s.ds.UpdateHostSoftware(context.Background(), host.ID, nil))
}
if len(ids) > 0 {
require.NoError(t, s.ds.DeleteHosts(ctx, ids))
require.NoError(t, s.ds.DeleteHost(ctx, host.ID))
}
// recalculate software counts will remove the software entries
@ -3511,6 +3507,100 @@ func (s *integrationTestSuite) TestPasswordReset() {
res.Body.Close()
}
func (s *integrationTestSuite) TestDeviceAuthenticatedEndpoints() {
t := s.T()
hosts := s.createHosts(t)
// create some mappings and MDM/Munki data
s.ds.ReplaceHostDeviceMapping(context.Background(), hosts[0].ID, []*fleet.HostDeviceMapping{
{HostID: hosts[0].ID, Email: "a@b.c", Source: "google_chrome_profiles"},
{HostID: hosts[0].ID, Email: "b@b.c", Source: "google_chrome_profiles"},
})
require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), hosts[0].ID, true, "url", false))
require.NoError(t, s.ds.SetOrUpdateMunkiVersion(context.Background(), hosts[0].ID, "1.3.0"))
// 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 (?, ?)`, hosts[0].ID, token)
return err
})
// get host without token
res := s.DoRawNoAuth("GET", "/api/v1/fleet/device/", nil, http.StatusNotFound)
res.Body.Close()
// get host with invalid token
res = s.DoRawNoAuth("GET", "/api/v1/fleet/device/no_such_token", nil, http.StatusUnauthorized)
res.Body.Close()
// get host with valid token
var getHostResp getHostResponse
res = s.DoRawNoAuth("GET", "/api/v1/fleet/device/"+token, nil, http.StatusOK)
json.NewDecoder(res.Body).Decode(&getHostResp)
res.Body.Close()
require.Equal(t, hosts[0].ID, getHostResp.Host.ID)
require.False(t, getHostResp.Host.RefetchRequested)
hostDevResp := getHostResp.Host
// make request for same host on the host details API endpoint, responses should match
getHostResp = getHostResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hosts[0].ID), nil, http.StatusOK, &getHostResp)
require.Equal(t, hostDevResp, getHostResp.Host)
// request a refetch for that valid host
res = s.DoRawNoAuth("POST", "/api/v1/fleet/device/"+token+"/refetch", nil, http.StatusOK)
res.Body.Close()
// host should have that flag turned to true
getHostResp = getHostResponse{}
res = s.DoRawNoAuth("GET", "/api/v1/fleet/device/"+token, nil, http.StatusOK)
json.NewDecoder(res.Body).Decode(&getHostResp)
res.Body.Close()
require.True(t, getHostResp.Host.RefetchRequested)
// request a refetch for an invalid token
res = s.DoRawNoAuth("POST", "/api/v1/fleet/device/no_such_token/refetch", nil, http.StatusUnauthorized)
res.Body.Close()
// list device mappings for valid token
var listDMResp listHostDeviceMappingResponse
res = s.DoRawNoAuth("GET", "/api/v1/fleet/device/"+token+"/device_mapping", nil, http.StatusOK)
json.NewDecoder(res.Body).Decode(&listDMResp)
res.Body.Close()
require.Equal(t, hosts[0].ID, listDMResp.HostID)
require.Len(t, listDMResp.DeviceMapping, 2)
devDMs := listDMResp.DeviceMapping
// compare response with standard list device mapping API for that same host
listDMResp = listHostDeviceMappingResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d/device_mapping", hosts[0].ID), nil, http.StatusOK, &listDMResp)
require.Equal(t, hosts[0].ID, listDMResp.HostID)
require.Equal(t, devDMs, listDMResp.DeviceMapping)
// list device mappings for invalid token
res = s.DoRawNoAuth("GET", "/api/v1/fleet/device/no_such_token/device_mapping", nil, http.StatusUnauthorized)
res.Body.Close()
// get macadmins for valid token
var getMacadm getMacadminsDataResponse
res = s.DoRawNoAuth("GET", "/api/v1/fleet/device/"+token+"/macadmins", nil, http.StatusOK)
json.NewDecoder(res.Body).Decode(&getMacadm)
res.Body.Close()
require.Equal(t, "1.3.0", getMacadm.Macadmins.Munki.Version)
devMacadm := getMacadm.Macadmins
// compare response with standard macadmins API for that same host
getMacadm = getMacadminsDataResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d/macadmins", hosts[0].ID), nil, http.StatusOK, &getMacadm)
require.Equal(t, devMacadm, getMacadm.Macadmins)
// get macadmins for invalid token
res = s.DoRawNoAuth("GET", "/api/v1/fleet/device/no_such_token/macadmins", nil, http.StatusUnauthorized)
res.Body.Close()
}
// creates a session and returns it, its key is to be passed as authorization header.
func createSession(t *testing.T, uid uint, ds fleet.Datastore) *fleet.Session {
key := make([]byte, 64)

View File

@ -281,6 +281,10 @@ type getClientConfigRequest struct {
NodeKey string `json:"node_key"`
}
func (r *getClientConfigRequest) hostNodeKey() string {
return r.NodeKey
}
type getClientConfigResponse struct {
Config map[string]interface{}
Err error `json:"error,omitempty"`
@ -459,6 +463,10 @@ type getDistributedQueriesRequest struct {
NodeKey string `json:"node_key"`
}
func (r *getDistributedQueriesRequest) hostNodeKey() string {
return r.NodeKey
}
type getDistributedQueriesResponse struct {
Queries map[string]string `json:"queries"`
Accelerate uint `json:"accelerate,omitempty"`
@ -628,6 +636,10 @@ type submitDistributedQueryResultsRequestShim struct {
Messages map[string]string `json:"messages"`
}
func (shim *submitDistributedQueryResultsRequestShim) hostNodeKey() string {
return shim.NodeKey
}
func (shim *submitDistributedQueryResultsRequestShim) toRequest(ctx context.Context) (*SubmitDistributedQueryResultsRequest, error) {
results := fleet.OsqueryDistributedQueryResults{}
for query, raw := range shim.Results {
@ -1058,6 +1070,10 @@ type submitLogsRequest struct {
Data json.RawMessage `json:"data"`
}
func (r *submitLogsRequest) hostNodeKey() string {
return r.NodeKey
}
type submitLogsResponse struct {
Err error `json:"error,omitempty"`
}