fleet/cmd/fleetctl/api.go
Juan Fernandez 7226b7f087
Warnings in fleetctl should use Stderr not Stdout (#12316)
Fixed issue were the expired license banner was being sent to Stdout instead of Stderr
2023-06-15 13:13:41 -04:00

208 lines
5.8 KiB
Go

package main
import (
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"runtime"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/kolide/kit/version"
"github.com/urfave/cli/v2"
)
func unauthenticatedClientFromCLI(c *cli.Context) (*service.Client, error) {
cc, err := clientConfigFromCLI(c)
if err != nil {
return nil, err
}
return unauthenticatedClientFromConfig(cc, getDebug(c), c.App.Writer, c.App.ErrWriter)
}
func clientFromCLI(c *cli.Context) (*service.Client, error) {
fleetClient, err := unauthenticatedClientFromCLI(c)
if err != nil {
return nil, err
}
configPath, context := c.String("config"), c.String("context")
// if a config file is explicitly provided, do not set an invalid arbitrary token
if !c.IsSet("config") && flag.Lookup("test.v") != nil {
fleetClient.SetToken("AAAA")
return fleetClient, nil
}
// Add authentication token
t, err := getConfigValue(configPath, context, "token")
if err != nil {
return nil, fmt.Errorf("error getting token from the config: %w", err)
}
token, ok := t.(string)
if !ok {
fmt.Fprintln(os.Stderr, "Token invalid. Please log in with: fleetctl login")
return nil, fmt.Errorf("token config value expected type %T, got %T: %+v", "", t, t)
}
if token == "" {
fmt.Fprintln(os.Stderr, "Token missing. Please log in with: fleetctl login")
return nil, errors.New("token config value missing")
}
fleetClient.SetToken(token)
// Check if version matches fleet server. Also ensures that the token is valid.
clientInfo := version.Version()
serverInfo, err := fleetClient.Version()
if err != nil {
if errors.Is(err, service.ErrUnauthenticated) {
fmt.Fprintln(os.Stderr, "Token invalid or session expired. Please log in with: fleetctl login")
}
return nil, err
}
if clientInfo.Version != serverInfo.Version {
fmt.Fprintf(
os.Stderr,
"Warning: Version mismatch.\nClient Version: %s\nServer Version: %s\n",
clientInfo.Version, serverInfo.Version,
)
// This is just a warning, continue ...
}
// check that AppConfig's Apple BM terms are not expired.
var sce kithttp.StatusCoder
switch appCfg, err := fleetClient.GetAppConfig(); {
case err == nil:
if appCfg.MDM.AppleBMTermsExpired {
fleet.WriteAppleBMTermsExpiredBanner(os.Stderr)
// This is just a warning, continue ...
}
case errors.As(err, &sce) && sce.StatusCode() == http.StatusForbidden:
// OK, could be a user without permissions to read app config (e.g. gitops).
default:
return nil, err
}
return fleetClient, nil
}
func unauthenticatedClientFromConfig(cc Context, debug bool, outputWriter io.Writer, errWriter io.Writer) (*service.Client, error) {
options := []service.ClientOption{
service.SetClientOutputWriter(outputWriter),
service.SetClientErrorWriter(errWriter),
}
if len(cc.CustomHeaders) > 0 {
options = append(options, service.WithCustomHeaders(cc.CustomHeaders))
}
if flag.Lookup("test.v") != nil {
return service.NewClient(
os.Getenv("FLEET_SERVER_ADDRESS"), true, "", "", options...)
}
if cc.Address == "" {
return nil, errors.New("set the Fleet API address with: fleetctl config set --address https://localhost:8080")
}
if runtime.GOOS == "windows" && cc.RootCA == "" && !cc.TLSSkipVerify {
return nil, errors.New("Windows clients must configure rootca (secure) or tls-skip-verify (insecure)")
}
if debug {
options = append(options, service.EnableClientDebug())
}
fleet, err := service.NewClient(
cc.Address,
cc.TLSSkipVerify,
cc.RootCA,
cc.URLPrefix,
options...,
)
if err != nil {
return nil, fmt.Errorf("error creating Fleet API client handler: %w", err)
}
return fleet, nil
}
// returns an HTTP client and the parsed URL for the configured server's
// address. The reason why this exists instead of using
// unauthenticatedClientFromConfig is because this doesn't apply the same rules
// around TLS config - in particular, it only sets a root CA if one is
// explicitly configured.
func rawHTTPClientFromConfig(cc Context) (*http.Client, *url.URL, error) {
if flag.Lookup("test.v") != nil {
cc.Address = os.Getenv("FLEET_SERVER_ADDRESS")
}
baseURL, err := url.Parse(cc.Address)
if err != nil {
return nil, nil, fmt.Errorf("parse address: %w", err)
}
var rootCA *x509.CertPool
if cc.RootCA != "" {
rootCA = x509.NewCertPool()
// read in the root cert file specified in the context
certs, err := ioutil.ReadFile(cc.RootCA)
if err != nil {
return nil, nil, fmt.Errorf("reading root CA: %w", err)
}
// add certs to pool
if ok := rootCA.AppendCertsFromPEM(certs); !ok {
return nil, nil, errors.New("failed to add certificates to root CA pool")
}
}
cli := fleethttp.NewClient(fleethttp.WithTLSClientConfig(&tls.Config{
InsecureSkipVerify: cc.TLSSkipVerify,
RootCAs: rootCA,
}))
return cli, baseURL, nil
}
func clientConfigFromCLI(c *cli.Context) (Context, error) {
// if a config file is explicitly provided, do not return a default context,
// just override the address and skip verify before returning.
if !c.IsSet("config") && flag.Lookup("test.v") != nil {
return Context{
Address: os.Getenv("FLEET_SERVER_ADDRESS"),
TLSSkipVerify: true,
}, nil
}
var zeroCtx Context
if err := makeConfigIfNotExists(c.String("config")); err != nil {
return zeroCtx, fmt.Errorf("error verifying that config exists at %s: %w", c.String("config"), err)
}
config, err := readConfig(c.String("config"))
if err != nil {
return zeroCtx, err
}
cc, ok := config.Contexts[c.String("context")]
if !ok {
return zeroCtx, fmt.Errorf("context %q is not found", c.String("context"))
}
if flag.Lookup("test.v") != nil {
cc.Address = os.Getenv("FLEET_SERVER_ADDRESS")
cc.TLSSkipVerify = true
}
return cc, nil
}