fleet/cmd/fleetctl/api.go
Lucas Manuel Rodriguez 417f45fc61
Move external dependency fleetdm/kolide-kit to monorepo (#15861)
#15556

We will need to pay attention when releasing fleet (the github actions
were modified to use the local file now).

Should be reviewed by commits (first commit is the actual adding of the
`version.go` file)

- [X] Manual QA for all new/changed functionality

Manually tested the following:
- `Settings -> My account` on the UI and checked the `/version` endpoint
response. (Or also visiting https://localhost:8080/version on a
browser).
- Ran `make fleetctl fleet`, `./build/fleetctl --version` and
`./build/fleet version`.
2024-01-02 18:22:52 -03:00

208 lines
5.8 KiB
Go

package main
import (
"crypto/tls"
"crypto/x509"
"errors"
"flag"
"fmt"
"io"
"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"
"github.com/fleetdm/fleet/v4/server/version"
kithttp "github.com/go-kit/kit/transport/http"
"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 := os.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
}