mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
3757aace08
#8129 Apart from fixing the issue in #8129, this change also introduces UUIDs to Fleet errors. To be able to match a returned error from the API to a error in the Fleet logs. See https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for more context. Samples with the changes in this PR: ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d '' { "message": "Bad request", "errors": [ { "name": "base", "reason": "Expected JSON Body" } ], "uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0" } ``` ``` curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd' { "message": "Bad request", "errors": [ { "name": "base", "reason": "json decoder error" } ], "uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d" } ``` ``` curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams" { "message": "Authentication required", "errors": [ { "name": "base", "reason": "Authentication required" } ], "uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Authorization header required", "errors": [ { "name": "base", "reason": "Authorization header required" } ], "uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3" } ``` ``` curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}' { "message": "Permission Denied", "uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06" } ``` - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md) - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [X] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
250 lines
6.9 KiB
Go
250 lines
6.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/go-kit/kit/log"
|
|
"github.com/go-kit/kit/log/level"
|
|
kithttp "github.com/go-kit/kit/transport/http"
|
|
|
|
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
|
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/token"
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/go-kit/kit/endpoint"
|
|
)
|
|
|
|
func logJSON(logger log.Logger, v interface{}, key string) {
|
|
jsonV, err := json.Marshal(v)
|
|
if err != nil {
|
|
level.Debug(logger).Log("err", fmt.Errorf("marshaling %s for debug: %w", key, err))
|
|
return
|
|
}
|
|
level.Debug(logger).Log(key, string(jsonV))
|
|
}
|
|
|
|
// instrumentHostLogger adds host IP information and extras to the context logger.
|
|
func instrumentHostLogger(ctx context.Context, extras ...interface{}) {
|
|
remoteAddr, _ := ctx.Value(kithttp.ContextKeyRequestRemoteAddr).(string)
|
|
xForwardedFor, _ := ctx.Value(kithttp.ContextKeyRequestXForwardedFor).(string)
|
|
logging.WithExtras(
|
|
logging.WithNoUser(ctx),
|
|
append(extras, "ip_addr", remoteAddr, "x_for_ip_addr", xForwardedFor)...,
|
|
)
|
|
}
|
|
|
|
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)
|
|
if ac, ok := authz_ctx.FromContext(ctx); ok {
|
|
ac.SetAuthnMethod(authz_ctx.AuthnDeviceToken)
|
|
}
|
|
|
|
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
|
|
func authenticatedHost(svc fleet.Service, logger log.Logger, next endpoint.Endpoint) endpoint.Endpoint {
|
|
authHostFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
|
|
nodeKey, err := getNodeKey(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
host, debug, err := svc.AuthenticateHost(ctx, nodeKey)
|
|
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)
|
|
if ac, ok := authz_ctx.FromContext(ctx); ok {
|
|
ac.SetAuthnMethod(authz_ctx.AuthnHostToken)
|
|
}
|
|
|
|
resp, err := next(ctx, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if debug {
|
|
logJSON(hlogger, resp, "response")
|
|
}
|
|
return resp, nil
|
|
}
|
|
return logged(authHostFunc)
|
|
}
|
|
|
|
func authenticatedOrbitHost(svc fleet.Service, logger log.Logger, next endpoint.Endpoint) endpoint.Endpoint {
|
|
authHostFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
|
|
nodeKey, err := getOrbitNodeKey(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
host, debug, err := svc.AuthenticateOrbitHost(ctx, nodeKey)
|
|
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)
|
|
if ac, ok := authz_ctx.FromContext(ctx); ok {
|
|
ac.SetAuthnMethod(authz_ctx.AuthnOrbitToken)
|
|
}
|
|
|
|
resp, err := next(ctx, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if debug {
|
|
logJSON(hlogger, resp, "response")
|
|
}
|
|
return resp, nil
|
|
}
|
|
return logged(authHostFunc)
|
|
}
|
|
|
|
func getOrbitNodeKey(r interface{}) (string, error) {
|
|
if onk, err := r.(interface{ orbitHostNodeKey() string }); err {
|
|
return onk.orbitHostNodeKey(), nil
|
|
}
|
|
return "", errors.New("error getting orbit node key")
|
|
}
|
|
|
|
func getNodeKey(r interface{}) (string, error) {
|
|
if hnk, ok := r.(interface{ hostNodeKey() string }); ok {
|
|
return hnk.hostNodeKey(), nil
|
|
}
|
|
return "", newOsqueryError("request type does not implement hostNodeKey method. This is likely a Fleet programmer error.")
|
|
}
|
|
|
|
// authenticatedUser wraps an endpoint, requires that the Fleet user is
|
|
// authenticated, and populates the context with a Viewer struct for that user.
|
|
//
|
|
// If auth fails or the user must reset their password, an error is returned.
|
|
func authenticatedUser(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
|
authUserFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
|
|
// first check if already successfully set
|
|
if v, ok := viewer.FromContext(ctx); ok {
|
|
if v.User.IsAdminForcedPasswordReset() {
|
|
return nil, fleet.ErrPasswordResetRequired
|
|
}
|
|
|
|
return next(ctx, request)
|
|
}
|
|
|
|
// if not succesful, try again this time with errors
|
|
sessionKey, ok := token.FromContext(ctx)
|
|
if !ok {
|
|
return nil, fleet.NewAuthHeaderRequiredError("no auth token")
|
|
}
|
|
|
|
v, err := authViewer(ctx, string(sessionKey), svc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if v.User.IsAdminForcedPasswordReset() {
|
|
return nil, fleet.ErrPasswordResetRequired
|
|
}
|
|
|
|
ctx = viewer.NewContext(ctx, *v)
|
|
if ac, ok := authz_ctx.FromContext(ctx); ok {
|
|
ac.SetAuthnMethod(authz_ctx.AuthnUserToken)
|
|
}
|
|
return next(ctx, request)
|
|
}
|
|
|
|
return logged(authUserFunc)
|
|
}
|
|
|
|
func unauthenticatedRequest(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
|
return logged(next)
|
|
}
|
|
|
|
// logged wraps an endpoint and adds the error if the context supports it
|
|
func logged(next endpoint.Endpoint) endpoint.Endpoint {
|
|
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
|
|
res, err := next(ctx, request)
|
|
if err != nil {
|
|
logging.WithErr(ctx, err)
|
|
return nil, err
|
|
}
|
|
if errResp, ok := res.(errorer); ok {
|
|
err = errResp.error()
|
|
if err != nil {
|
|
logging.WithErr(ctx, err)
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
}
|
|
|
|
// authViewer creates an authenticated viewer by validating the session key.
|
|
func authViewer(ctx context.Context, sessionKey string, svc fleet.Service) (*viewer.Viewer, error) {
|
|
session, err := svc.GetSessionByKey(ctx, sessionKey)
|
|
if err != nil {
|
|
return nil, fleet.NewAuthRequiredError(err.Error())
|
|
}
|
|
user, err := svc.UserUnauthorized(ctx, session.UserID)
|
|
if err != nil {
|
|
return nil, fleet.NewAuthRequiredError(err.Error())
|
|
}
|
|
return &viewer.Viewer{User: user, Session: session}, nil
|
|
}
|