mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Add new endpoints to retrieve device information by orbit identifier (#4531)
This commit is contained in:
parent
81bc22bb74
commit
a1c67547b3
1
changes/issue-4416-add-device-auth-endpoints
Normal file
1
changes/issue-4416-add-device-auth-endpoints
Normal file
@ -0,0 +1 @@
|
||||
* Add device-authenticated endpoints to retrieve read-only information about the current device.
|
@ -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.
|
||||
//
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 */;
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
144
server/service/devices.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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{})
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user