mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 16:39:01 +00:00
orbit: run the profiles command to renew the enrollment profile when signaled by fleet (#9409)
This commit is contained in:
parent
1b4e8e692a
commit
357c0484fc
6 changed files with 232 additions and 2 deletions
1
orbit/changes/issue-9278-orbit-renew-enrollment-profile
Normal file
1
orbit/changes/issue-9278-orbit-renew-enrollment-profile
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added support to `fleetd` to run the necessary command to renew the MDM enrollment profile on the devices that are pending automatic enrollment into Fleet MDM.
|
||||
|
|
@ -505,8 +505,17 @@ func main() {
|
|||
if err != nil {
|
||||
return fmt.Errorf("error new orbit client: %w", err)
|
||||
}
|
||||
|
||||
// create the notifications middleware that wraps the orbit client
|
||||
// (must be shared by all runners that use a ConfigFetcher).
|
||||
const renewEnrollmentProfileCommandFrequency = time.Hour
|
||||
configFetcher := &update.RenewEnrollmentProfileConfigFetcher{
|
||||
Fetcher: orbitClient,
|
||||
Frequency: renewEnrollmentProfileCommandFrequency,
|
||||
}
|
||||
|
||||
const orbitFlagsUpdateInterval = 30 * time.Second
|
||||
flagRunner := update.NewFlagRunner(orbitClient, update.FlagUpdateOptions{
|
||||
flagRunner := update.NewFlagRunner(configFetcher, update.FlagUpdateOptions{
|
||||
CheckInterval: orbitFlagsUpdateInterval,
|
||||
RootDir: c.String("root-dir"),
|
||||
})
|
||||
|
|
@ -523,7 +532,7 @@ func main() {
|
|||
// and all relevant things for it (like certs, enroll secrets, tls proxy, etc) is configured
|
||||
if !c.Bool("disable-updates") || c.Bool("dev-mode") {
|
||||
const orbitExtensionUpdateInterval = 60 * time.Second
|
||||
extRunner := update.NewExtensionConfigUpdateRunner(orbitClient, update.ExtensionUpdateOptions{
|
||||
extRunner := update.NewExtensionConfigUpdateRunner(configFetcher, update.ExtensionUpdateOptions{
|
||||
CheckInterval: orbitExtensionUpdateInterval,
|
||||
RootDir: c.String("root-dir"),
|
||||
}, updateRunner)
|
||||
|
|
|
|||
69
orbit/pkg/update/notifications.go
Normal file
69
orbit/pkg/update/notifications.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package update
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type runCmdFunc func() error
|
||||
|
||||
// RenewEnrollmentProfileConfigFetcher is a kind of middleware that wraps an
|
||||
// OrbitConfigFetcher and detects if the fleet server sent a notification to
|
||||
// renew the enrollment profile. If so, it runs the command (as root) to
|
||||
// bootstrap the renewal of the profile on the device (the user still needs to
|
||||
// execute some manual steps to accept the new profile).
|
||||
//
|
||||
// It ensures only one renewal command is executed at any given time, and that
|
||||
// it doesn't re-execute the command until a certain amount of time has passed.
|
||||
type RenewEnrollmentProfileConfigFetcher struct {
|
||||
// Fetcher is the OrbitConfigFetcher that will be wrapped. It is responsible
|
||||
// for actually returning the orbit configuration or an error.
|
||||
Fetcher OrbitConfigFetcher
|
||||
// Frequency is the minimum amount of time that must pass between two
|
||||
// executions of the profile renewal command.
|
||||
Frequency time.Duration
|
||||
|
||||
// for tests, to be able to mock command execution. If nil, will use
|
||||
// runRenewEnrollmentProfile.
|
||||
runCmdFn runCmdFunc
|
||||
|
||||
// ensures only one command runs at a time, protects access to lastRun
|
||||
cmdMu sync.Mutex
|
||||
lastRun time.Time
|
||||
}
|
||||
|
||||
// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet
|
||||
// server set the renew enrollment profile flag to true, executes the command
|
||||
// to renew the enrollment profile.
|
||||
func (h *RenewEnrollmentProfileConfigFetcher) GetConfig() (*service.OrbitConfig, error) {
|
||||
cfg, err := h.Fetcher.GetConfig()
|
||||
if err == nil && cfg.Notifications.RenewEnrollmentProfile {
|
||||
if h.cmdMu.TryLock() {
|
||||
defer h.cmdMu.Unlock()
|
||||
|
||||
// Note that the macOS notification popup will be shown periodically
|
||||
// until the Fleet server gets notified that the device is now properly
|
||||
// enrolled (after the user's manual steps, and osquery reporting the
|
||||
// updated mdm enrollment).
|
||||
// See https://github.com/fleetdm/fleet/pull/9409#discussion_r1084382455
|
||||
if time.Since(h.lastRun) > h.Frequency {
|
||||
fn := h.runCmdFn
|
||||
if fn == nil {
|
||||
fn = runRenewEnrollmentProfile
|
||||
}
|
||||
if err := fn(); err != nil {
|
||||
log.Info().Err(err).Msg("calling /usr/bin/profiles to renew enrollment profile failed")
|
||||
} else {
|
||||
h.lastRun = time.Now()
|
||||
log.Info().Msg("successfully called /usr/bin/profiles to renew enrollment profile")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msg("skipped calling /usr/bin/profiles to renew enrollment profile, last run was too recent")
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
21
orbit/pkg/update/notifications_darwin.go
Normal file
21
orbit/pkg/update/notifications_darwin.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
//go:build darwin
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func runRenewEnrollmentProfile() error {
|
||||
cmd := exec.Command("/usr/bin/profiles", "renew", "--type", "enrollment")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil && len(out) > 0 {
|
||||
// just as a precaution, limit the length of the output
|
||||
if len(out) > 512 {
|
||||
out = out[:512]
|
||||
}
|
||||
err = fmt.Errorf("%w: %s", err, string(out))
|
||||
}
|
||||
return err
|
||||
}
|
||||
7
orbit/pkg/update/notifications_stub.go
Normal file
7
orbit/pkg/update/notifications_stub.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//go:build !darwin
|
||||
|
||||
package update
|
||||
|
||||
func runRenewEnrollmentProfile() error {
|
||||
return nil
|
||||
}
|
||||
123
orbit/pkg/update/notifications_test.go
Normal file
123
orbit/pkg/update/notifications_test.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package update
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRenewEnrollmentProfile(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
|
||||
oldLog := log.Logger
|
||||
log.Logger = log.Output(&logBuf)
|
||||
t.Cleanup(func() { log.Logger = oldLog })
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
renewFlag bool
|
||||
cmdErr error
|
||||
wantCmdCalled bool
|
||||
wantLog string
|
||||
}{
|
||||
{"renew=false", false, nil, false, ""},
|
||||
{"renew=true; success", true, nil, true, "successfully called /usr/bin/profiles to renew enrollment profile"},
|
||||
{"renew=true; fail", true, io.ErrUnexpectedEOF, true, "calling /usr/bin/profiles to renew enrollment profile failed"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
logBuf.Reset()
|
||||
|
||||
fetcher := &dummyConfigFetcher{
|
||||
cfg: &service.OrbitConfig{Notifications: fleet.OrbitConfigNotifications{RenewEnrollmentProfile: c.renewFlag}},
|
||||
}
|
||||
|
||||
var cmdGotCalled bool
|
||||
renewFetcher := &RenewEnrollmentProfileConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: time.Hour, // doesn't matter for this test
|
||||
runCmdFn: func() error {
|
||||
cmdGotCalled = true
|
||||
return c.cmdErr
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := renewFetcher.GetConfig()
|
||||
require.NoError(t, err) // the dummy fetcher never returns an error
|
||||
require.Equal(t, fetcher.cfg, cfg) // the renew enrollment wrapper properly returns the expected config
|
||||
|
||||
require.Equal(t, c.wantCmdCalled, cmdGotCalled)
|
||||
require.Contains(t, logBuf.String(), c.wantLog)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewEnrollmentProfilePrevented(t *testing.T) {
|
||||
var logBuf bytes.Buffer
|
||||
|
||||
oldLog := log.Logger
|
||||
log.Logger = log.Output(&logBuf)
|
||||
t.Cleanup(func() { log.Logger = oldLog })
|
||||
|
||||
fetcher := &dummyConfigFetcher{
|
||||
cfg: &service.OrbitConfig{Notifications: fleet.OrbitConfigNotifications{RenewEnrollmentProfile: true}},
|
||||
}
|
||||
|
||||
var cmdCallCount int
|
||||
chProceed := make(chan struct{})
|
||||
renewFetcher := &RenewEnrollmentProfileConfigFetcher{
|
||||
Fetcher: fetcher,
|
||||
Frequency: 2 * time.Second, // just to be safe with slow environments (CI)
|
||||
runCmdFn: func() error {
|
||||
<-chProceed // will be unblocked only when allowed
|
||||
cmdCallCount++ // no need for sync, single-threaded call of this func is guaranteed by the fetcher's mutex
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
assertResult := func(cfg *service.OrbitConfig, err error) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fetcher.cfg, cfg)
|
||||
}
|
||||
|
||||
started := make(chan struct{})
|
||||
go func() {
|
||||
close(started)
|
||||
|
||||
// the first call will block in runCmdFn
|
||||
cfg, err := renewFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
}()
|
||||
|
||||
<-started
|
||||
// this call will happen while the first call is blocked in runCmdFn, so it
|
||||
// won't call the command (won't be able to lock the mutex). However it will
|
||||
// still complete successfully without being blocked by the other call in
|
||||
// progress.
|
||||
cfg, err := renewFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
|
||||
// unblock the first call
|
||||
close(chProceed)
|
||||
|
||||
// this next call won't execute the command because of the frequency
|
||||
// restriction (it got called less than N seconds ago)
|
||||
cfg, err = renewFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
|
||||
// wait for the fetcher's frequency to pass
|
||||
time.Sleep(renewFetcher.Frequency)
|
||||
|
||||
// this call executes the command
|
||||
cfg, err = renewFetcher.GetConfig()
|
||||
assertResult(cfg, err)
|
||||
|
||||
require.Equal(t, 2, cmdCallCount) // the initial call and the one after sleep
|
||||
}
|
||||
Loading…
Reference in a new issue