merge main

This commit is contained in:
Carlo DiCelico
2026-06-25 13:57:22 -04:00
1546 changed files with 52746 additions and 25726 deletions
+13 -8
View File
@@ -891,6 +891,7 @@ func newAutomationsSchedule(
logger *slog.Logger,
intervalReload time.Duration,
failingPoliciesSet fleet.FailingPolicySet,
newActivitySvc activity_api.NewActivityService,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronAutomations)
@@ -929,7 +930,7 @@ func newAutomationsSchedule(
schedule.WithJob(
"failing_policies_automation",
func(ctx context.Context) error {
return triggerFailingPoliciesAutomation(ctx, ds, logger.With("automation", "failing_policies"), failingPoliciesSet)
return triggerFailingPoliciesAutomation(ctx, ds, logger.With("automation", "failing_policies"), failingPoliciesSet, newActivitySvc)
},
),
)
@@ -966,6 +967,7 @@ func triggerFailingPoliciesAutomation(
ds fleet.Datastore,
logger *slog.Logger,
failingPoliciesSet fleet.FailingPolicySet,
newActivitySvc activity_api.NewActivityService,
) error {
appConfig, err := ds.AppConfig(ctx)
if err != nil {
@@ -980,7 +982,7 @@ func triggerFailingPoliciesAutomation(
switch cfg.AutomationType {
case policies.FailingPolicyWebhook:
return webhooks.SendFailingPoliciesBatchedPOSTs(
ctx, policy, failingPoliciesSet, cfg.HostBatchSize, serverURL, cfg.WebhookURL, time.Now(), logger)
ctx, policy, failingPoliciesSet, cfg.HostBatchSize, serverURL, cfg.WebhookURL, time.Now(), logger, newActivitySvc)
case policies.FailingPolicyJira:
hosts, err := failingPoliciesSet.ListHosts(policy.ID)
@@ -1025,6 +1027,7 @@ func newWorkerIntegrationsSchedule(
androidModule android.Service,
chartSvc chart_api.Service,
androidBatchSize int,
newActivitySvc activity_api.NewActivityService,
) (*schedule.Schedule, error) {
const (
name = string(fleet.CronWorkerIntegrations)
@@ -1046,14 +1049,16 @@ func newWorkerIntegrationsSchedule(
// leave the url empty for now, will be filled when the lock is acquired with
// the up-to-date config.
jira := &worker.Jira{
Datastore: ds,
Log: logger,
NewClientFunc: newJiraClient,
Datastore: ds,
Log: logger,
NewClientFunc: newJiraClient,
NewActivitySvc: newActivitySvc,
}
zendesk := &worker.Zendesk{
Datastore: ds,
Log: logger,
NewClientFunc: newZendeskClient,
Datastore: ds,
Log: logger,
NewClientFunc: newZendeskClient,
NewActivitySvc: newActivitySvc,
}
var (
depSvc *apple_mdm.DEPService
+3 -3
View File
@@ -194,11 +194,11 @@ func registerVulnerabilityCrons(ctx context.Context, deps cronSchedulesDeps) {
// integrations schedule.
func registerWorkerCrons(ctx context.Context, deps cronSchedulesDeps) {
deps.register("failed to register automations schedule", func() (fleet.CronSchedule, error) {
return newAutomationsSchedule(ctx, deps.instanceID, deps.ds, deps.logger, 5*time.Minute, deps.failingPolicySet)
return newAutomationsSchedule(ctx, deps.instanceID, deps.ds, deps.logger, 5*time.Minute, deps.failingPolicySet, deps.activitySvc)
})
deps.register("failed to register worker integrations schedule", func() (fleet.CronSchedule, error) {
return newWorkerIntegrationsSchedule(ctx, deps.instanceID, deps.ds, deps.logger, deps.depStorage, deps.commander, deps.androidSvc, deps.chartSvc, deps.config.MDM.AndroidBatchSize)
return newWorkerIntegrationsSchedule(ctx, deps.instanceID, deps.ds, deps.logger, deps.depStorage, deps.commander, deps.androidSvc, deps.chartSvc, deps.config.MDM.AndroidBatchSize, deps.activitySvc)
})
}
@@ -342,7 +342,7 @@ func registerPremiumCrons(ctx context.Context, deps cronSchedulesDeps) {
} else {
deps.config.Calendar.Periodicity = 5 * time.Minute
}
return cron.NewCalendarSchedule(ctx, deps.instanceID, deps.ds, deps.distributedLock, deps.config.Calendar, deps.logger)
return cron.NewCalendarSchedule(ctx, deps.instanceID, deps.ds, deps.distributedLock, deps.config.Calendar, deps.logger, deps.activitySvc)
})
}
+126
View File
@@ -0,0 +1,126 @@
package main
import (
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"github.com/fleetdm/fleet/v4/pkg/scripts"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/installersize"
)
// apiTimeoutOverrideHandler wraps the main API handler with per-route request
// read/write deadline overrides for endpoints that legitimately run long:
// synchronous script runs, large software-installer and bootstrap-package
// uploads, the Android enterprise signup SSE stream, and large MDM profile
// batch operations. For package-upload routes it also caps the request body and
// threads the configured max installer size through the request context.
//
// Deadline overrides are best-effort: if the ResponseWriter does not support
// SetReadDeadline/SetWriteDeadline the error is logged and the request proceeds.
func apiTimeoutOverrideHandler(apiHandler http.Handler, cfg config.FleetConfig, logger *slog.Logger) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/scripts/run/sync") {
// when running a script synchronously, we wait a while for a script
// execution result, so the write timeout (to write the response)
// must be extended.
rc := http.NewResponseController(rw)
// add an additional 30 seconds to prevent race conditions where the
// request is terminated early.
if err := rc.SetWriteDeadline(time.Now().Add(scripts.MaxServerWaitTime + (30 * time.Second))); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint write timeout for script sync run",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
}
if (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/software/package")) ||
(req.Method == http.MethodPatch && strings.HasSuffix(req.URL.Path, "/package") && strings.Contains(req.URL.Path,
"/fleet/software/titles/")) ||
(req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/bootstrap")) ||
(req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet_maintained_apps")) ||
(req.Method == http.MethodGet && strings.Contains(req.URL.Path, "/package/token")) ||
(req.Method == http.MethodPost && strings.Contains(req.URL.Path, "orbit/software_install/package")) {
var zeroTime time.Time
rc := http.NewResponseController(rw)
// For large software installers and bootstrap packages, the server time needs time to read the full
// request body so we use the zero value to remove the deadline and override the
// default read timeout.
// TODO: Is this really how we want to handle this? Or would an arbitrarily long
// timeout be better?
if err := rc.SetReadDeadline(zeroTime); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint read timeout for software package upload",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
// For large software installers, the server time needs time to store the
// installer to S3 (or the configured storage location) and write the response
// body so we use the zero value to remove the deadline and override the
// default write timeout.
// TODO: Is this really how we want to handle this? Or would an arbitrarily long
// timeout be better?
if err := rc.SetWriteDeadline(zeroTime); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint write timeout for software package upload",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
// We need to add the context value here because we need the installer max size when doing request
// parsing, which happens somewhere where we're only passed the request (and not the service object)
req.Body = http.MaxBytesReader(rw, req.Body, cfg.Server.MaxInstallerSizeBytes)
req = req.WithContext(installersize.NewContext(req.Context(), cfg.Server.MaxInstallerSizeBytes))
}
if req.Method == http.MethodGet && strings.HasSuffix(req.URL.Path, "/fleet/android_enterprise/signup_sse") {
// When enabling Android MDM, frontend UI will wait for the admin to finish the setup in Google.
rc := http.NewResponseController(rw)
if err := rc.SetWriteDeadline(time.Now().Add(30 * time.Minute)); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint write timeout for android enterpriset setup",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
}
if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/mdm/profiles/batch") ||
(req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/configuration_profiles/batch")) {
// For customers using large profiles and/or large numbers of profiles, the
// server needs time to completely read the request body and also to process
// all the side effects of a potentially large number of profiles being changed
// across a large number of hosts, so set the timeouts a bit higher than default
rc := http.NewResponseController(rw)
if err := rc.SetWriteDeadline(time.Now().Add(5 * time.Minute)); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint write timeout for MDM profiles batch endpoint",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
if err := rc.SetReadDeadline(time.Now().Add(5 * time.Minute)); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint read timeout for MDM profiles batch endpoint",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
}
apiHandler.ServeHTTP(rw, req)
}
}
+65
View File
@@ -0,0 +1,65 @@
package main
import (
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/installersize"
"github.com/stretchr/testify/assert"
)
func TestAPITimeoutOverrideHandler(t *testing.T) {
logger := slog.New(slog.DiscardHandler)
const customMax int64 = 4242
cfg := config.FleetConfig{}
cfg.Server.MaxInstallerSizeBytes = customMax
for _, tc := range []struct {
name string
method string
path string
wantInstallerSize int64
}{
{
name: "software package upload threads configured max size",
method: http.MethodPost,
path: "/api/latest/fleet/software/package",
wantInstallerSize: customMax,
},
{
name: "bootstrap package upload threads configured max size",
method: http.MethodPost,
path: "/api/latest/fleet/mdm/bootstrap",
wantInstallerSize: customMax,
},
{
name: "non-upload request leaves the default max size",
method: http.MethodGet,
path: "/api/latest/fleet/hosts",
wantInstallerSize: installersize.MaxSoftwareInstallerSize,
},
} {
t.Run(tc.name, func(t *testing.T) {
var (
called bool
seen int64
)
downstream := http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
called = true
seen = installersize.FromContext(req.Context())
})
apiTimeoutOverrideHandler(downstream, cfg, logger).ServeHTTP(
httptest.NewRecorder(),
httptest.NewRequest(tc.method, tc.path, nil),
)
assert.True(t, called, "the wrapped API handler must always be invoked")
assert.Equal(t, tc.wantInstallerSize, seen)
})
}
}
+1 -103
View File
@@ -34,7 +34,6 @@ import (
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity"
"github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig"
"github.com/fleetdm/fleet/v4/ee/server/service/scep"
"github.com/fleetdm/fleet/v4/pkg/scripts"
"github.com/fleetdm/fleet/v4/pkg/str"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/acl/acmeacl"
@@ -49,7 +48,6 @@ import (
chart_bootstrap "github.com/fleetdm/fleet/v4/server/chart/bootstrap"
configpkg "github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/installersize"
licensectx "github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/datastore/failing"
"github.com/fleetdm/fleet/v4/server/datastore/filesystem"
@@ -937,107 +935,7 @@ func runServeCmd(cmd *cobra.Command, configManager configpkg.Manager, debug, dev
// See https://pkg.go.dev/net/http#NewResponseController which explains
// the Unwrap method that the prometheus wrapper of http.ResponseWriter
// does not implement.
rootMux.HandleFunc("/api/", func(rw http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/scripts/run/sync") {
// when running a script synchronously, we wait a while for a script
// execution result, so the write timeout (to write the response)
// must be extended.
rc := http.NewResponseController(rw)
// add an additional 30 seconds to prevent race conditions where the
// request is terminated early.
if err := rc.SetWriteDeadline(time.Now().Add(scripts.MaxServerWaitTime + (30 * time.Second))); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint write timeout for script sync run",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
}
if (req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/software/package")) ||
(req.Method == http.MethodPatch && strings.HasSuffix(req.URL.Path, "/package") && strings.Contains(req.URL.Path,
"/fleet/software/titles/")) ||
(req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/bootstrap")) ||
(req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet_maintained_apps")) ||
(req.Method == http.MethodGet && strings.Contains(req.URL.Path, "/package/token")) ||
(req.Method == http.MethodPost && strings.Contains(req.URL.Path, "orbit/software_install/package")) {
var zeroTime time.Time
rc := http.NewResponseController(rw)
// For large software installers and bootstrap packages, the server time needs time to read the full
// request body so we use the zero value to remove the deadline and override the
// default read timeout.
// TODO: Is this really how we want to handle this? Or would an arbitrarily long
// timeout be better?
if err := rc.SetReadDeadline(zeroTime); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint read timeout for software package upload",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
// For large software installers, the server time needs time to store the
// installer to S3 (or the configured storage location) and write the response
// body so we use the zero value to remove the deadline and override the
// default write timeout.
// TODO: Is this really how we want to handle this? Or would an arbitrarily long
// timeout be better?
if err := rc.SetWriteDeadline(zeroTime); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint write timeout for software package upload",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
// We need to add the context value here because we need the installer max size when doing request
// parsing, which happens somewhere where we're only passed the request (and not the service object)
req.Body = http.MaxBytesReader(rw, req.Body, config.Server.MaxInstallerSizeBytes)
req = req.WithContext(installersize.NewContext(req.Context(), config.Server.MaxInstallerSizeBytes))
}
if req.Method == http.MethodGet && strings.HasSuffix(req.URL.Path, "/fleet/android_enterprise/signup_sse") {
// When enabling Android MDM, frontend UI will wait for the admin to finish the setup in Google.
rc := http.NewResponseController(rw)
if err := rc.SetWriteDeadline(time.Now().Add(30 * time.Minute)); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint write timeout for android enterpriset setup",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
}
if req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/mdm/profiles/batch") ||
(req.Method == http.MethodPost && strings.HasSuffix(req.URL.Path, "/fleet/configuration_profiles/batch")) {
// For customers using large profiles and/or large numbers of profiles, the
// server needs time to completely read the request body and also to process
// all the side effects of a potentially large number of profiles being changed
// across a large number of hosts, so set the timeouts a bit higher than default
rc := http.NewResponseController(rw)
if err := rc.SetWriteDeadline(time.Now().Add(5 * time.Minute)); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint write timeout for MDM profiles batch endpoint",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
if err := rc.SetReadDeadline(time.Now().Add(5 * time.Minute)); err != nil {
logger.ErrorContext(req.Context(),
"http middleware failed to override endpoint read timeout for MDM profiles batch endpoint",
"response_writer_type", fmt.Sprintf("%T", rw),
"response_writer", fmt.Sprintf("%+v", rw),
"err", err,
)
}
}
apiHandler.ServeHTTP(rw, req)
})
rootMux.HandleFunc("/api/", apiTimeoutOverrideHandler(apiHandler, config, logger))
// The `/api/{version}/fleet/scim` base path is used by SCIM handler. In order to route the `details` route to the apiHandler,
// we have to explicitly handle that path at the root. The Go router takes precedence for a more specific path. The v1/latest are used in the path for it to be more specific.
// The Fleet API was designed this way for end-user simplicity.
+3 -3
View File
@@ -305,7 +305,7 @@ func TestAutomationsSchedule(t *testing.T) {
defer cancelFunc()
failingPoliciesSet := service.NewMemFailingPolicySet()
s, err := newAutomationsSchedule(ctx, "test_instance", ds, slog.New(slog.DiscardHandler), 5*time.Minute, failingPoliciesSet)
s, err := newAutomationsSchedule(ctx, "test_instance", ds, slog.New(slog.DiscardHandler), 5*time.Minute, failingPoliciesSet, &mock.MockActivityService{})
require.NoError(t, err)
s.Start()
@@ -1002,7 +1002,7 @@ func TestAutomationsScheduleLockDuration(t *testing.T) {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
s, err := newAutomationsSchedule(ctx, "test_instance", ds, slog.New(slog.DiscardHandler), 1*time.Second, service.NewMemFailingPolicySet())
s, err := newAutomationsSchedule(ctx, "test_instance", ds, slog.New(slog.DiscardHandler), 1*time.Second, service.NewMemFailingPolicySet(), &mock.MockActivityService{})
require.NoError(t, err)
s.Start()
@@ -1069,7 +1069,7 @@ func TestAutomationsScheduleIntervalChange(t *testing.T) {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
s, err := newAutomationsSchedule(ctx, "test_instance", ds, slog.New(slog.DiscardHandler), 200*time.Millisecond, service.NewMemFailingPolicySet())
s, err := newAutomationsSchedule(ctx, "test_instance", ds, slog.New(slog.DiscardHandler), 200*time.Millisecond, service.NewMemFailingPolicySet(), &mock.MockActivityService{})
require.NoError(t, err)
s.Start()
+19 -3
View File
@@ -37,6 +37,22 @@ func unauthenticatedClientFromCLI(c *cli.Context) (*service.Client, error) {
return unauthenticatedClientFromConfig(cc, getDebug(c), c.App.Writer, c.App.ErrWriter)
}
const ssoAuthInstructions = "SSO is enabled for this Fleet instance. Email/password login is not supported on SSO-enabled accounts.\n" +
"If your account isn't SSO-enabled, use fleetctl login.\n\n" +
"Learn how to authenticate with fleetctl for SSO-enabled accounts:\n" +
"https://fleetdm.com/guides/fleetctl#users-with-single-sign-on-sso-or-email-two-factor-authentication-2-fa"
// printAuthError prints an authentication error message. If SSO is enabled on the server,
// it directs the user to authenticate via API token instead of fleetctl login.
func printAuthError(w io.Writer, client *service.Client, prefix string) {
ssoSettings, err := client.SSOSettings()
if err == nil && ssoSettings != nil && ssoSettings.SSOEnabled {
fmt.Fprintf(w, "%s %s\n", prefix, ssoAuthInstructions)
return
}
fmt.Fprintf(w, "%s Please log in with: fleetctl login\n", prefix)
}
func clientFromCLI(c *cli.Context) (*service.Client, error) {
fleetClient, err := unauthenticatedClientFromCLI(c)
if err != nil {
@@ -59,11 +75,11 @@ func clientFromCLI(c *cli.Context) (*service.Client, error) {
token, ok := t.(string)
if !ok {
fmt.Fprintln(os.Stderr, "Token invalid. Please log in with: fleetctl login")
printAuthError(os.Stderr, fleetClient, "Token invalid.")
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")
printAuthError(os.Stderr, fleetClient, "Token missing.")
return nil, errors.New("token config value missing")
}
fleetClient.SetToken(token)
@@ -74,7 +90,7 @@ func clientFromCLI(c *cli.Context) (*service.Client, error) {
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")
printAuthError(os.Stderr, fleetClient, "Token invalid or session expired.")
}
return nil, err
}
@@ -892,7 +892,7 @@ spec:
// appconfig macos setup assistant
name := writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", emptyMacosSetup))
runAppCheckErr(t, []string{"apply", "-f", name}, `applying fleet config: missing or invalid license`)
runAppCheckErr(t, []string{"apply", "-f", name}, `uploading apple setup assistant: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
@@ -900,7 +900,7 @@ spec:
assert.False(t, ds.SaveAppConfigFuncInvoked)
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "https://example.com", ""))
runAppCheckErr(t, []string{"apply", "-f", name}, `applying fleet config: missing or invalid license`)
runAppCheckErr(t, []string{"apply", "-f", name}, `verifying bootstrap package: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
@@ -1162,9 +1162,9 @@ spec:
expectedErr error
}{
{"signed.pkg", nil},
{"unsigned.pkg", errors.New("applying fleet config: Couldnt edit macos_bootstrap_package. The macos_bootstrap_package must be signed. Learn how to sign the package in the Fleet documentation: https://fleetdm.com/learn-more-about/setup-experience/bootstrap-package")},
{"invalid.tar.gz", errors.New("applying fleet config: Couldnt edit macos_bootstrap_package. The file must be a package (.pkg).")},
{"wrong-toc.pkg", errors.New("applying fleet config: checking package signature: decompressing TOC: unexpected EOF")},
{"unsigned.pkg", errors.New("verifying bootstrap package: Couldnt edit macos_bootstrap_package. The macos_bootstrap_package must be signed. Learn how to sign the package in the Fleet documentation: https://fleetdm.com/learn-more-about/setup-experience/bootstrap-package")},
{"invalid.tar.gz", errors.New("verifying bootstrap package: Couldnt edit macos_bootstrap_package. The file must be a package (.pkg).")},
{"wrong-toc.pkg", errors.New("verifying bootstrap package: checking package signature: decompressing TOC: unexpected EOF")},
}
for _, c := range cases {
+5 -5
View File
@@ -2502,7 +2502,7 @@ spec:
// appconfig macos setup assistant
name := writeTmpYml(t, fmt.Sprintf(appConfigSpec, "", emptyMacosSetup))
runAppCheckErr(t, []string{"apply", "-f", name}, `applying fleet config: missing or invalid license`)
runAppCheckErr(t, []string{"apply", "-f", name}, `uploading apple setup assistant: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
@@ -2510,7 +2510,7 @@ spec:
assert.False(t, ds.SaveAppConfigFuncInvoked)
name = writeTmpYml(t, fmt.Sprintf(appConfigSpec, "https://example.com", ""))
runAppCheckErr(t, []string{"apply", "-f", name}, `applying fleet config: missing or invalid license`)
runAppCheckErr(t, []string{"apply", "-f", name}, `verifying bootstrap package: missing or invalid license`)
assert.False(t, ds.SetOrUpdateMDMAppleSetupAssistantFuncInvoked)
assert.False(t, ds.GetMDMAppleBootstrapPackageMetaFuncInvoked)
assert.False(t, ds.InsertMDMAppleBootstrapPackageFuncInvoked)
@@ -2826,9 +2826,9 @@ spec:
expectedErr error
}{
{"signed.pkg", nil},
{"unsigned.pkg", errors.New("applying fleet config: Couldnt edit macos_bootstrap_package. The macos_bootstrap_package must be signed. Learn how to sign the package in the Fleet documentation: https://fleetdm.com/learn-more-about/setup-experience/bootstrap-package")},
{"invalid.tar.gz", errors.New("applying fleet config: Couldnt edit macos_bootstrap_package. The file must be a package (.pkg).")},
{"wrong-toc.pkg", errors.New("applying fleet config: checking package signature: decompressing TOC: unexpected EOF")},
{"unsigned.pkg", errors.New("verifying bootstrap package: Couldnt edit macos_bootstrap_package. The macos_bootstrap_package must be signed. Learn how to sign the package in the Fleet documentation: https://fleetdm.com/learn-more-about/setup-experience/bootstrap-package")},
{"invalid.tar.gz", errors.New("verifying bootstrap package: Couldnt edit macos_bootstrap_package. The file must be a package (.pkg).")},
{"wrong-toc.pkg", errors.New("verifying bootstrap package: checking package signature: decompressing TOC: unexpected EOF")},
}
for _, c := range cases {
+1
View File
@@ -277,6 +277,7 @@ type GenerateGitopsCommand struct {
func generateGitopsCommand() *cli.Command {
return &cli.Command{
Name: "generate-gitops",
Hidden: true,
Usage: "Generate GitOps configuration files for Fleet.",
Description: "This command generates GitOps configuration files for Fleet.",
Action: createGenerateGitopsAction(nil),
+5 -2
View File
@@ -2757,6 +2757,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
ds.GetSoftwareCategoryNameToIDMapFunc = func(ctx context.Context, teamID uint, names []string) (map[string]uint, error) {
return map[string]uint{}, nil
}
ds.GetABMTokenOrgNamesAssociatedByDefaultTeamsFunc = func(ctx context.Context, teamID *uint) ([]string, error) {
return nil, nil
}
testing_utils.StartAndServeVPPServer(t)
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
@@ -7886,12 +7889,12 @@ software:
teamExisting := []fleet.SoftwareCategory{
{ID: 100, Name: "🌎 Browsers", TeamID: 1},
{ID: 101, Name: "💻 Productivity", TeamID: 1},
{ID: 101, Name: "🖥️ Productivity", TeamID: 1},
{ID: 102, Name: "Stale Category", TeamID: 1},
}
noTeamExisting := []fleet.SoftwareCategory{
{ID: 200, Name: "🌎 Browsers", TeamID: 0},
{ID: 201, Name: "💻 Productivity", TeamID: 0},
{ID: 201, Name: "🖥️ Productivity", TeamID: 0},
{ID: 202, Name: "Stale No-team Category", TeamID: 0},
}
+9 -1
View File
@@ -23,7 +23,11 @@ fleetctl login [options]
Interactively prompts for email and password if not specified in the flags or environment variables.
Trying to login with SSO or MFA? First, login to the Fleet UI and retrieve your API token from the "My account" page. Then set your API token with the fleetctl config set --token <your-api-token-here> command. You're now logged in to fleetctl.
If SSO is enabled on the Fleet server, a warning will be displayed. You may still attempt to
log in with email and password, but it will only succeed if your account is not SSO-enabled.
Learn how to authenticate with fleetctl for SSO-enabled accounts:
https://fleetdm.com/guides/fleetctl#users-with-single-sign-on-sso-or-email-two-factor-authentication-2-fa
`,
Flags: []cli.Flag{
&cli.StringFlag{
@@ -50,6 +54,10 @@ Trying to login with SSO or MFA? First, login to the Fleet UI and retrieve your
return err
}
if ssoSettings, ssoErr := fleet.SSOSettings(); ssoErr == nil && ssoSettings != nil && ssoSettings.SSOEnabled {
fmt.Fprintf(os.Stderr, "Warning: %s\n\n", ssoAuthInstructions)
}
definedAsEnvOnly := func(flagName, envName string) bool {
cliArgPresent := false
for _, arg := range os.Args {
+61
View File
@@ -0,0 +1,61 @@
package fleetctl
import (
"bytes"
"context"
"testing"
"time"
"github.com/fleetdm/fleet/v4/cmd/fleetctl/fleetctl/testing_utils"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/stretchr/testify/require"
)
// TestPrintAuthError verifies that the authentication-error message adapts to
// whether SSO is enabled on the server. The SSO-enabled case also guards the
// response contract relied on by the warning (the "settings" wrapper and the
// "sso_enabled" field): a server-side rename of either would drop the
// instructions and fail here.
func TestPrintAuthError(t *testing.T) {
cfg := config.TestConfig()
server, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{
License: &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)},
FleetConfig: &cfg,
// Bypass the app config cache so each sub-test sees its own SSO setting.
NoCacheDatastore: true,
})
client, err := service.NewClient(server.URL, true, "", "")
require.NoError(t, err)
setSSO := func(enabled bool) {
ds.AppConfigFunc = func(context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{SSOSettings: &fleet.SSOSettings{EnableSSO: enabled}}, nil
}
}
t.Run("SSO enabled shows SSO instructions", func(t *testing.T) {
setSSO(true)
var buf bytes.Buffer
printAuthError(&buf, client, "Token missing.")
out := buf.String()
require.Contains(t, out, "Token missing.")
require.Contains(t, out, ssoAuthInstructions)
})
t.Run("SSO disabled shows default login message", func(t *testing.T) {
setSSO(false)
var buf bytes.Buffer
printAuthError(&buf, client, "Token missing.")
out := buf.String()
require.Contains(t, out, "Token missing.")
require.Contains(t, out, "Please log in with: fleetctl login")
require.NotContains(t, out, ssoAuthInstructions)
})
}
@@ -52,7 +52,7 @@ org_settings:
#
# Read more:
# • https://fleetdm.com/docs/configuration/yaml-files#end-user-authentication
# • https://fleetdm.com/guides/setup-experience#end-user-authentication
# • https://fleetdm.com/guides/setup-experience#require-idp-authentication
###########################################################
# end_user_authentication:
# idp_name: "Okta" # e.g. "Entra", "Okta", "Google Workspace", etc. (Displayed to end users.)
@@ -25,7 +25,7 @@ controls:
#
# Read more:
# • https://fleetdm.com/docs/configuration/yaml-files#end-user-authentication
# • https://fleetdm.com/guides/setup-experience#end-user-authentication
# • https://fleetdm.com/guides/setup-experience#require-idp-authentication
###########################################################
# enable_end_user_authentication: true
@@ -41,7 +41,7 @@ controls:
#
# Read more:
# • https://fleetdm.com/docs/configuration/yaml-files#end-user-authentication
# • https://fleetdm.com/guides/setup-experience#end-user-authentication
# • https://fleetdm.com/guides/setup-experience#require-idp-authentication
###########################################################
# enable_end_user_authentication: true
+1
View File
@@ -119,6 +119,7 @@ var allowedCategories = map[string]struct{}{
"Developer tools": {},
"Productivity": {},
"Security": {},
"Support": {},
"Utilities": {},
}