@@ -0,0 +1 @@
|
||||
- Added agent support for setup experience on Linux
|
||||
@@ -125,6 +125,11 @@ func packageCommand() *cli.Command {
|
||||
Usage: "Disable auto updates on the generated package",
|
||||
Destination: &opt.DisableUpdates,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "disable-setup-experience",
|
||||
Usage: "Disable setup experience for Linux hosts",
|
||||
Destination: &opt.DisableSetupExperience,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "update-url",
|
||||
Usage: "URL for update server",
|
||||
|
||||
@@ -241,6 +241,11 @@ func main() {
|
||||
Usage: "Configures fleetd to use TPM-backed key to sign HTTP requests. This functionality is licensed under the Fleet EE License. Usage requires a current Fleet EE subscription.",
|
||||
EnvVars: []string{"ORBIT_FLEET_MANAGED_HOST_IDENTITY_CERTIFICATE"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "disable-setup-experience",
|
||||
Usage: "Disables checking for setup experience on Linux hosts",
|
||||
EnvVars: []string{"ORBIT_DISABLE_SETUP_EXPERIENCE"},
|
||||
},
|
||||
}
|
||||
app.Before = func(c *cli.Context) error {
|
||||
// handle old installations, which had default root dir set to /var/lib/orbit
|
||||
@@ -1514,6 +1519,60 @@ func main() {
|
||||
|
||||
go sigusrListener(c.String("root-dir"))
|
||||
|
||||
isLinux := runtime.GOOS == "linux"
|
||||
serverHasWebSetup := orbitClient.GetServerCapabilities().Has(fleet.CapabilityWebSetupExperience)
|
||||
setupExperienceNotDisabled := !c.Bool("disable-setup-experience")
|
||||
runSetupExperience := isLinux && serverHasWebSetup && setupExperienceNotDisabled
|
||||
log.Debug().
|
||||
Bool("isLinux", isLinux).
|
||||
Bool("serverHasSetup", serverHasWebSetup).
|
||||
Bool("notDisabled", setupExperienceNotDisabled).
|
||||
Msg("checking setup experience preflight values")
|
||||
|
||||
openMyDevicePage := func() error {
|
||||
log.Debug().Msg("launching browser for my device page")
|
||||
token, err := trw.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting device token: %w", err)
|
||||
}
|
||||
// My Device page
|
||||
browserURL := deviceClient.BrowserDeviceURL(token)
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
loggedInUser, err := user.UserLoggedInViaGui()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get logged in user: %w", err)
|
||||
}
|
||||
|
||||
if loggedInUser == nil {
|
||||
return errors.New("no user logged in")
|
||||
}
|
||||
|
||||
var opts []execuser.Option
|
||||
opts = append(opts, execuser.WithUser(*loggedInUser))
|
||||
opts = append(opts, execuser.WithArg(browserURL, ""))
|
||||
if _, err := execuser.Run("/usr/bin/xdg-open", opts...); err != nil {
|
||||
return fmt.Errorf("opening browser with xdg-open: %w", err)
|
||||
}
|
||||
default:
|
||||
log.Debug().Msg("could not open browser, unsupported OS: " + runtime.GOOS)
|
||||
return errors.New("opening setup experience browser page not supported on " + runtime.GOOS)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if runSetupExperience {
|
||||
log.Debug().Msg("web setup experience enabled")
|
||||
setupExpPath := path.Join(c.String("root-dir"), constant.SetupExperienceFilename)
|
||||
if err := processSetupExperience(orbitClient, setupExpPath, openMyDevicePage); err != nil {
|
||||
log.Error().Err(err).Msg("initiating setup experience")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msg("not running setup experience")
|
||||
}
|
||||
|
||||
if err := g.Run(); err != nil {
|
||||
log.Error().Err(err).Msg("unexpected exit")
|
||||
}
|
||||
@@ -1531,6 +1590,80 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func processSetupExperience(oc *service.OrbitClient, setupExperienceStatusPath string, openMyDevicePage func() error) error {
|
||||
log.Debug().Msg("checking setup experience file")
|
||||
exp, err := readSetupExperienceStatusFile(setupExperienceStatusPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read setup experience file: %w", err)
|
||||
}
|
||||
|
||||
// Setup experience has been completed
|
||||
if exp != nil && exp.TimeInitiated != nil {
|
||||
log.Debug().Msg("setup experience already completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug().Msg("initiating setup experience")
|
||||
resp, err := oc.InitiateSetupExperience()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing server-side setup experience: %w", err)
|
||||
}
|
||||
|
||||
// Setup experience enabled for us and is now kicked off, open a browser
|
||||
if resp.Enabled {
|
||||
if err := openMyDevicePage(); err != nil {
|
||||
return fmt.Errorf("opening my device page: %w", err)
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msg("setup experience not enabled on team")
|
||||
}
|
||||
|
||||
// Even if it wasn't enabled, mark it as complete so we don't start it again later
|
||||
log.Debug().Msg("writing setup experience file")
|
||||
initTime := time.Now()
|
||||
if err := writeSetupExperienceStatusFile(setupExperienceStatusPath, &SetupExperienceInfo{
|
||||
TimeInitiated: &initTime,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("writing setup experience file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SetupExperienceInfo struct {
|
||||
TimeInitiated *time.Time `json:"time_initiated,omitempty"`
|
||||
}
|
||||
|
||||
// Returns the time setup experience was completed, or nil if it hasn't
|
||||
func readSetupExperienceStatusFile(experienceCompletedPath string) (*SetupExperienceInfo, error) {
|
||||
f, err := os.Open(experienceCompletedPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read setup experience file: %w", err)
|
||||
}
|
||||
var exp *SetupExperienceInfo
|
||||
if err := json.NewDecoder(f).Decode(&exp); err != nil {
|
||||
return nil, fmt.Errorf("decoding setup experience file: %w", err)
|
||||
}
|
||||
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
func writeSetupExperienceStatusFile(experienceCompletedPath string, exp *SetupExperienceInfo) error {
|
||||
f, err := os.Create(experienceCompletedPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create setup experience completed file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(f).Encode(exp); err != nil {
|
||||
return fmt.Errorf("write setup experience completed file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteSecretPathIfExists(enrollSecretPath string) {
|
||||
// Since the secret is in the keystore, we can delete the original secret file if it exists
|
||||
if _, err := os.Stat(enrollSecretPath); err == nil {
|
||||
|
||||
@@ -75,6 +75,8 @@ const (
|
||||
DesktopTUFTargetName = "desktop"
|
||||
// FleetURLFileName is the file where Fleet URL is stored after being read from Apple config profile.
|
||||
FleetURLFileName = "fleet_url.txt"
|
||||
// SetupExperienceComplete is a file created when Linux (and soon Windows) completes setup experience
|
||||
SetupExperienceFilename = "setup_experience.json"
|
||||
|
||||
FleetHTTPSignatureCertificateFileName = "host_identity.crt"
|
||||
// FleetHTTPSignatureTPMKeyFileName is the filename for the TPM key used for HTTP signature authentication
|
||||
|
||||
@@ -353,6 +353,7 @@ ORBIT_FLEET_DESKTOP_ALTERNATIVE_BROWSER_HOST={{ .FleetDesktopAlternativeBrowserH
|
||||
{{ if .OsqueryDB }}ORBIT_OSQUERY_DB={{.OsqueryDB}}{{ end }}
|
||||
{{ if .EndUserEmail }}ORBIT_END_USER_EMAIL={{.EndUserEmail}}{{ end }}
|
||||
{{ if .FleetManagedHostIdentityCertificate }}ORBIT_FLEET_MANAGED_HOST_IDENTITY_CERTIFICATE=true{{ end }}
|
||||
{{ if .DisableSetupExperience }}ORBIT_DISABLE_SETUP_EXPERIENCE=true{{ end }}
|
||||
`))
|
||||
|
||||
func writeEnvFile(opt Options, rootPath string) error {
|
||||
|
||||
@@ -61,6 +61,8 @@ type Options struct {
|
||||
FleetDesktopAlternativeBrowserHost string
|
||||
// DisableUpdates disables auto updates on the generated package.
|
||||
DisableUpdates bool
|
||||
// DisableSetupExperience disables setup experience for Linux hosts
|
||||
DisableSetupExperience bool
|
||||
// OrbitChannel is the update channel to use for Orbit.
|
||||
OrbitChannel string
|
||||
// OsquerydChannel is the update channel to use for Osquery (osqueryd).
|
||||
|
||||
@@ -88,6 +88,9 @@ const (
|
||||
// the ability of the client to show the corresponding UI to support that
|
||||
// flow.
|
||||
CapabilitySetupExperience Capability = "setup_experience"
|
||||
// CapabilityWebSetupExperience denotes the ability of the server to support installing software
|
||||
// as part of a non-blocking setup experience for Linux and Windows
|
||||
CapabilityWebSetupExperience Capability = "web_setup_experience"
|
||||
)
|
||||
|
||||
func GetServerOrbitCapabilities() CapabilityMap {
|
||||
@@ -98,6 +101,7 @@ func GetServerOrbitCapabilities() CapabilityMap {
|
||||
CapabilityEscrowBuddy: {},
|
||||
CapabilityLinuxDiskEncryptionEscrow: {},
|
||||
CapabilitySetupExperience: {},
|
||||
CapabilityWebSetupExperience: {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -737,3 +737,13 @@ func (oc *OrbitClient) SendLinuxKeyEscrowResponse(lr luks.LuksResponse) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (oc *OrbitClient) InitiateSetupExperience() (fleet.SetupExperienceInitResult, error) {
|
||||
verb, path := "POST", "/api/fleet/orbit/setup_experience/init"
|
||||
var resp orbitSetupExperienceInitResponse
|
||||
if err := oc.authenticatedRequest(verb, path, &orbitSetupExperienceInitRequest{}, &resp); err != nil {
|
||||
return fleet.SetupExperienceInitResult{}, err
|
||||
}
|
||||
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user