mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Fix orbit active GUI session detection to start Fleet Desktop and key escrowing on Linux (#39777)
Resolves #36024 and #34501. Main change is about stop using the first user in the `users` command output [*] and instead use `loginctl` commands to pick the correct current active GUI user. [*] `users` was returning empty on some new distributions, and in multi-sessions we were always picking the first one (even if it wasn't active). - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. ## Testing - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [x] Verified that fleetd runs on macOS, Linux and Windows - [x] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Bug Fixes** * Fixed Fleet Desktop startup to correctly detect and use the active GUI session on Linux systems. * Improved GUI user detection for dialog prompts, ensuring system dialogs run in the proper user context. * **Improvements** * Enhanced error reporting and logging clarity for GUI session detection failures. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
77e4004133
commit
0823cc7e76
12 changed files with 285 additions and 215 deletions
1
orbit/changes/34501-fix-fleet-desktop-gui-sessions
Normal file
1
orbit/changes/34501-fix-fleet-desktop-gui-sessions
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Improved GUI user detection in orbit to use the correct active GUI session when starting Fleet Desktop.
|
||||
|
|
@ -1870,20 +1870,25 @@ func (d *desktopRunner) Execute() error {
|
|||
for {
|
||||
// First retry logic to start fleet-desktop.
|
||||
if done := retry(30*time.Second, false, d.interruptCh, func() bool {
|
||||
// On MacOS, if we attempt to run Fleet Desktop while the user is not logged in through
|
||||
// the GUI, MacOS returns an error. See https://github.com/fleetdm/fleet/issues/14698
|
||||
// for more details.
|
||||
//
|
||||
// - On MacOS, if we attempt to run Fleet Desktop while the user is not logged in through
|
||||
// the GUI, MacOS returns an error. See https://github.com/fleetdm/fleet/issues/14698
|
||||
// for more details.
|
||||
// - On Linux, we also don't want to start the Fleet Desktop unless there's an active GUI session.
|
||||
// - On Windows, user.UserLoggedInViaGui is a no-op, and execuser.Run will take care of
|
||||
// starting Fleet Desktop with the correct GUI user.
|
||||
//
|
||||
|
||||
loggedInUser, err := user.UserLoggedInViaGui()
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("desktop.IsUserLoggedInGui")
|
||||
log.Debug().Err(err).Msg("desktop.IsUserLoggedViaGui")
|
||||
return true
|
||||
}
|
||||
if loggedInUser == nil {
|
||||
log.Debug().Msg("No GUI user found, skipping fleet-desktop start")
|
||||
return true
|
||||
}
|
||||
log.Debug().Msg(fmt.Sprintf("Found GUI user: %v, attempting fleet-desktop start", loggedInUser))
|
||||
|
||||
log.Debug().Msgf("found GUI user: %q, attempting fleet-desktop start", *loggedInUser)
|
||||
if *loggedInUser != "" {
|
||||
opts = append(opts, execuser.WithUser(*loggedInUser))
|
||||
}
|
||||
|
|
@ -2418,7 +2423,7 @@ func executeFleetDesktopWithPermanentError(desktopPath string, errorMessage stri
|
|||
log.Debug().Msg("No GUI user found, skipping fleet-desktop start")
|
||||
return nil
|
||||
}
|
||||
log.Debug().Msg(fmt.Sprintf("Found GUI user: %v, attempting fleet-desktop start", loggedInUser))
|
||||
log.Debug().Msgf("found GUI user: %q, attempting fleet-desktop start", *loggedInUser)
|
||||
|
||||
log.Info().Msg("killing any pre-existing fleet-desktop instances")
|
||||
if err := platform.SignalProcessBeforeTerminate(constant.DesktopAppExecName); err != nil &&
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
package execuser
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -70,11 +69,3 @@ func RunWithOutput(path string, opts ...Option) (output []byte, exitCode int, er
|
|||
}
|
||||
return runWithOutput(path, o)
|
||||
}
|
||||
|
||||
func RunWithStdin(path string, opts ...Option) (io.WriteCloser, error) {
|
||||
var o eopts
|
||||
for _, fn := range opts {
|
||||
fn(&o)
|
||||
}
|
||||
return runWithStdin(path, o)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,3 @@ func run(path string, opts eopts) (lastLogs string, err error) {
|
|||
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
|
||||
return nil, 0, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
userpkg "github.com/fleetdm/fleet/v4/orbit/pkg/user"
|
||||
|
|
@ -20,7 +19,11 @@ import (
|
|||
|
||||
// base command to setup an exec.Cmd using `runuser`
|
||||
func baserun(path string, opts eopts) (cmd *exec.Cmd, err error) {
|
||||
args, env, err := getConfigForCommand(path)
|
||||
if opts.user == "" {
|
||||
return nil, errors.New("missing user")
|
||||
}
|
||||
|
||||
args, env, err := getConfigForCommand(opts.user, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get args: %w", err)
|
||||
}
|
||||
|
|
@ -84,7 +87,7 @@ func run(path string, opts eopts) (lastLogs string, err error) {
|
|||
return "", nil
|
||||
}
|
||||
|
||||
// run a command and return its output and exit code.
|
||||
// runWithOutput runs a command and return its output and exit code.
|
||||
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
|
||||
cmd, err := baserun(path, opts)
|
||||
if err != nil {
|
||||
|
|
@ -103,85 +106,80 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
|
|||
return output, exitCode, nil
|
||||
}
|
||||
|
||||
// run a command that requires stdin input, returning a pipe to write to stdin.
|
||||
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
|
||||
cmd, err := baserun(path, opts)
|
||||
func getUserID(user string) (string, error) {
|
||||
uid_, err := exec.Command("id", "-u", user).Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", fmt.Errorf("failed to execute id command for %q: %w", user, err)
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stdin pipe: %w", err)
|
||||
uid := strings.TrimSpace(string(uid_))
|
||||
if uid == "" {
|
||||
return "", errors.New("failed to get uid")
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("open path %q: %w", path, err)
|
||||
}
|
||||
|
||||
return stdin, nil
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func getConfigForCommand(path string) (args []string, env []string, err error) {
|
||||
user, err := userpkg.GetLoginUser()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("get user: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("user", user.Name).Int64("id", user.ID).Msg("attempting to get user session type and display")
|
||||
|
||||
// Get user's display session type (x11 vs. wayland).
|
||||
uid := strconv.FormatInt(user.ID, 10)
|
||||
userDisplaySessionType, err := userpkg.GetUserDisplaySessionType(uid)
|
||||
if userDisplaySessionType == userpkg.GuiSessionTypeTty {
|
||||
return nil, nil, fmt.Errorf("user %q (%d) is not running a GUI session", user.Name, user.ID)
|
||||
}
|
||||
if err != nil {
|
||||
// Wayland is the default for most distributions, thus we assume
|
||||
// wayland if we couldn't determine the session type.
|
||||
log.Error().Err(err).Msg("assuming wayland session")
|
||||
userDisplaySessionType = userpkg.GuiSessionTypeWayland
|
||||
}
|
||||
|
||||
var display string
|
||||
if userDisplaySessionType == userpkg.GuiSessionTypeX11 {
|
||||
x11Display, err := getUserX11Display(user.Name)
|
||||
func getDisplayVariableForSession(user string, userID string, displaySessionType userpkg.GuiSessionType) string {
|
||||
if displaySessionType == userpkg.GuiSessionTypeX11 {
|
||||
x11Display, err := getUserX11Display(user)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get X11 display, using default :0")
|
||||
// TODO(lucas): Revisit when working on multi-user/multi-session support.
|
||||
// Default to display ':0' if user display could not be found.
|
||||
// This assumes there's only one desktop session and belongs to the
|
||||
// user returned in `getLoginUID'.
|
||||
display = ":0"
|
||||
} else {
|
||||
display = x11Display
|
||||
}
|
||||
} else {
|
||||
waylandDisplay, err := getUserWaylandDisplay(uid)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get wayland display, using default wayland-0")
|
||||
// TODO(lucas): Revisit when working on multi-user/multi-session support.
|
||||
// Default to display 'wayland-0' if user display could not be found.
|
||||
// This assumes there's only one desktop session and belongs to the
|
||||
// user returned in `getLoginUID'.
|
||||
display = "wayland-0"
|
||||
} else {
|
||||
display = waylandDisplay
|
||||
return ":0"
|
||||
}
|
||||
return x11Display
|
||||
}
|
||||
|
||||
waylandDisplay, err := getUserWaylandDisplay(userID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get wayland display, using default wayland-0")
|
||||
// TODO(lucas): Revisit when working on multi-user/multi-session support.
|
||||
// Default to display 'wayland-0' if user display could not be found.
|
||||
// This assumes there's only one desktop session and belongs to the
|
||||
// user returned in `getLoginUID'.
|
||||
return "wayland-0"
|
||||
}
|
||||
return waylandDisplay
|
||||
}
|
||||
|
||||
func getConfigForCommand(user string, path string) (args []string, env []string, err error) {
|
||||
// Get user ID
|
||||
userID, err := getUserID(user)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("get user ID: %w", err)
|
||||
}
|
||||
log.Info().Str("user", user).Str("id", userID).Msg("attempting to get user session type and display")
|
||||
|
||||
// Get user's display session type.
|
||||
userDisplaySession, err := userpkg.GetUserDisplaySessionType(userID)
|
||||
if err != nil {
|
||||
// Wayland is the default for most distributions,
|
||||
// thus we assume wayland if we couldn't determine the session type.
|
||||
log.Error().Err(err).Msg("assuming wayland session")
|
||||
userDisplaySession = &userpkg.UserDisplaySession{
|
||||
Type: userpkg.GuiSessionTypeWayland,
|
||||
}
|
||||
} else if userDisplaySession.Type == userpkg.GuiSessionTypeTty {
|
||||
return nil, nil, fmt.Errorf("user %q (%s) is not running a GUI session", user, userID)
|
||||
}
|
||||
|
||||
// Get user's "display" variable for the GUI session.
|
||||
display := getDisplayVariableForSession(user, userID, userDisplaySession.Type)
|
||||
|
||||
log.Info().
|
||||
Str("path", path).
|
||||
Str("user", user.Name).
|
||||
Int64("id", user.ID).
|
||||
Str("user", user).
|
||||
Str("id", userID).
|
||||
Str("display", display).
|
||||
Str("session_type", userDisplaySessionType.String()).
|
||||
Str("session_type", userDisplaySession.Type.String()).
|
||||
Msg("running sudo")
|
||||
|
||||
args = []string{"-n", "-i", "-u", user.Name, "-H"}
|
||||
args = []string{"-n", "-i", "-u", user, "-H"}
|
||||
env = make([]string, 0)
|
||||
|
||||
if userDisplaySessionType == userpkg.GuiSessionTypeWayland {
|
||||
if userDisplaySession.Type == userpkg.GuiSessionTypeWayland {
|
||||
env = append(env, "WAYLAND_DISPLAY="+display)
|
||||
// For xdg-open to work on a Wayland session we still need to set the DISPLAY variable.
|
||||
x11Display := ":" + strings.TrimPrefix(display, "wayland-")
|
||||
|
|
@ -197,7 +195,7 @@ func getConfigForCommand(path string) (args []string, env []string, err error) {
|
|||
//
|
||||
// This is required for Ubuntu 18, and not required for Ubuntu 21/22
|
||||
// (because it's already part of the user).
|
||||
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.ID),
|
||||
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%s/bus", userID),
|
||||
)
|
||||
|
||||
return args, env, nil
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package execuser
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
|
@ -136,10 +135,6 @@ func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err er
|
|||
return nil, 0, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// getCurrentUserSessionId will attempt to resolve
|
||||
// the session ID of the user currently active on
|
||||
// the system.
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package kdialog
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/user"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const kdialogProcessName = "kdialog"
|
||||
|
|
@ -79,6 +82,17 @@ func execCmdWithOutput(timeout time.Duration, args ...string) ([]byte, int, erro
|
|||
opts = append(opts, execuser.WithTimeout(timeout))
|
||||
}
|
||||
|
||||
// Retrieve and set active GUI user.
|
||||
loggedInUser, err := user.UserLoggedInViaGui()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("user logged in via GUI: %w", err)
|
||||
}
|
||||
if loggedInUser == nil || *loggedInUser == "" {
|
||||
return nil, 0, errors.New("no GUI user found")
|
||||
}
|
||||
log.Debug().Msgf("found GUI user: %s, attempting kdialog", *loggedInUser)
|
||||
opts = append(opts, execuser.WithUser(*loggedInUser))
|
||||
|
||||
output, exitCode, err := execuser.RunWithOutput(kdialogProcessName, opts...)
|
||||
if err != nil {
|
||||
return nil, exitCode, err
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by
|
|||
device := luksdevice.New(luksdevice.AESXTSPlain64Cipher)
|
||||
|
||||
// Prompt user for existing LUKS passphrase
|
||||
passphrase, err := lr.entryPrompt(ctx, entryDialogTitle, entryDialogText)
|
||||
passphrase, err := lr.entryPrompt(entryDialogTitle, entryDialogText)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to show passphrase entry prompt: %w", err)
|
||||
}
|
||||
|
|
@ -157,7 +157,7 @@ func (lr *LuksRunner) getEscrowKey(ctx context.Context, devicePath string) ([]by
|
|||
break
|
||||
}
|
||||
|
||||
passphrase, err = lr.entryPrompt(ctx, entryDialogTitle, retryEntryDialogText)
|
||||
passphrase, err = lr.entryPrompt(entryDialogTitle, retryEntryDialogText)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed re-prompting for passphrase: %w", err)
|
||||
}
|
||||
|
|
@ -275,7 +275,7 @@ func generateRandomPassphrase() ([]byte, error) {
|
|||
return passphrase, nil
|
||||
}
|
||||
|
||||
func (lr *LuksRunner) entryPrompt(ctx context.Context, title, text string) ([]byte, error) {
|
||||
func (lr *LuksRunner) entryPrompt(title, text string) ([]byte, error) {
|
||||
passphrase, err := lr.notifier.ShowEntry(dialog.EntryOptions{
|
||||
Title: title,
|
||||
Text: text,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -20,170 +19,146 @@ type User struct {
|
|||
ID int64
|
||||
}
|
||||
|
||||
// UserLoggedInViaGui returns the username that has an active GUI session.
|
||||
// It returns nil, nil if there's no user with an active GUI session.
|
||||
func UserLoggedInViaGui() (*string, error) {
|
||||
user, err := GetLoginUser()
|
||||
users, err := getLoginUsers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get login user: %w", err)
|
||||
return nil, fmt.Errorf("get login users: %w", err)
|
||||
}
|
||||
|
||||
// Bail out if the user is gdm or root, since they aren't GUI users.
|
||||
// User gdm-greeter is active during the GUI log-in prompt (GNOME 49).
|
||||
if user.Name == "gdm" || user.Name == "root" || user.Name == "gdm-greeter" {
|
||||
return nil, nil
|
||||
}
|
||||
// Check if the user has a GUI session.
|
||||
displaySessionType, err := GetUserDisplaySessionType(strconv.FormatInt(user.ID, 10))
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msgf("failed to get user display session type for user %s", user.Name)
|
||||
return nil, nil
|
||||
}
|
||||
if displaySessionType == GuiSessionTypeTty {
|
||||
log.Debug().Msgf("user %s is logged in via TTY, not GUI", user.Name)
|
||||
return nil, nil
|
||||
for _, user := range users {
|
||||
// Skip system/display manager users since they aren't GUI users.
|
||||
// User gdm-greeter is active during the GUI log-in prompt (GNOME 49).
|
||||
if user.Name == "gdm" || user.Name == "root" || user.Name == "gdm-greeter" {
|
||||
continue
|
||||
}
|
||||
// Check if the user has an active GUI session.
|
||||
session, err := GetUserDisplaySessionType(strconv.FormatInt(user.ID, 10))
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msgf("failed to get user display session for user %s", user.Name)
|
||||
continue
|
||||
}
|
||||
if !session.Active {
|
||||
log.Debug().Msgf("user %s has an inactive display session, skipping", user.Name)
|
||||
continue
|
||||
}
|
||||
if session.Type == GuiSessionTypeTty {
|
||||
log.Debug().Msgf("user %s is logged in via TTY, not GUI", user.Name)
|
||||
continue
|
||||
}
|
||||
return &user.Name, nil
|
||||
}
|
||||
|
||||
return &user.Name, nil
|
||||
// No valid user found
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetLoginUser returns the name and uid of the first login user
|
||||
// as reported by the `users' command.
|
||||
//
|
||||
// NOTE(lucas): It is always picking first login user as returned
|
||||
// by `users', revisit when working on multi-user/multi-session support.
|
||||
func GetLoginUser() (*User, error) {
|
||||
out, err := exec.Command("users").CombinedOutput()
|
||||
// getLoginUsers returns all logged-in users as reported by
|
||||
// `loginctl list-users`.
|
||||
func getLoginUsers() ([]User, error) {
|
||||
out, err := exec.Command("loginctl", "list-users", "--no-legend", "--no-pager").CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("users exec failed: %w", err)
|
||||
return nil, fmt.Errorf("loginctl list-users exec failed: %w, output: %s", err, string(out))
|
||||
}
|
||||
usernames := parseUsersOutput(string(out))
|
||||
username := usernames[0]
|
||||
if username == "" {
|
||||
return parseLoginctlUsersOutput(string(out))
|
||||
}
|
||||
|
||||
// parseLoginctlUsersOutput parses the output of `loginctl list-users --no-legend`.
|
||||
// Each line has the format: UID USERNAME
|
||||
func parseLoginctlUsersOutput(s string) ([]User, error) {
|
||||
var users []User
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(s), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
uid, err := strconv.ParseInt(fields[0], 10, 64)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msgf("failed to parse uid from loginctl output: %q", line)
|
||||
continue
|
||||
}
|
||||
users = append(users, User{
|
||||
Name: fields[1],
|
||||
ID: uid,
|
||||
})
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return nil, errors.New("no user session found")
|
||||
}
|
||||
out, err = exec.Command("id", "-u", username).CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("id exec failed: %w", err)
|
||||
}
|
||||
uid, err := parseIDOutput(string(out))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &User{
|
||||
Name: username,
|
||||
ID: uid,
|
||||
}, nil
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetSELinuxUserContext returns the SELinux context for the given user.
|
||||
// Example: `unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023`
|
||||
//
|
||||
// If SELinux is not enabled, the `runcon` command is not available,
|
||||
// or context cannot be determined, it returns nil.
|
||||
func GetSELinuxUserContext(user *User) *string {
|
||||
// If SELinux is not enabled, return nil right away.
|
||||
if _, err := os.Stat("/sys/fs/selinux/enforce"); err != nil {
|
||||
return nil
|
||||
}
|
||||
// If runcon is not available, we won't be able to switch contexts,
|
||||
// so return nil.
|
||||
if _, err := exec.LookPath("runcon"); err != nil {
|
||||
log.Warn().Msg("runcon not available, returning nil for user context since we can't switch contexts")
|
||||
return nil
|
||||
}
|
||||
// Find the first systemd process for the user and read its SELinux context.
|
||||
pidBytes, err := exec.Command("pgrep", "-u", strconv.FormatInt(user.ID, 10), "-nx", "systemd").Output() // #nosec G204
|
||||
if err != nil {
|
||||
log.Debug().Msgf("Error finding systemd process for user %s: %v", user.Name, err)
|
||||
return nil
|
||||
}
|
||||
pid := strings.TrimSpace(string(pidBytes))
|
||||
if pid == "" {
|
||||
log.Debug().Msgf("No systemd process found for user %s", user.Name)
|
||||
return nil
|
||||
}
|
||||
ctx, err := os.ReadFile("/proc/" + pid + "/attr/current")
|
||||
if err != nil {
|
||||
log.Debug().Msgf("Error reading SELinux context for user %s: %v", user.Name, err)
|
||||
return nil
|
||||
}
|
||||
context := strings.TrimSpace(string(ctx))
|
||||
// Remove any null byte at the end
|
||||
context = strings.TrimSuffix(context, "\x00")
|
||||
if context == "" {
|
||||
log.Debug().Msg("Empty SELinux context for user " + user.Name)
|
||||
return nil
|
||||
}
|
||||
return &context
|
||||
// UserDisplaySession holds the display session type and active status for a user.
|
||||
type UserDisplaySession struct {
|
||||
Type GuiSessionType
|
||||
Active bool
|
||||
}
|
||||
|
||||
// parseUsersOutput parses the output of the `users' command.
|
||||
//
|
||||
// `users' command prints on a single line a blank-separated list of user names of
|
||||
// users currently logged in to the current host. Each user name
|
||||
// corresponds to a login session, so if a user has more than one login
|
||||
// session, that user's name will appear the same number of times in the
|
||||
// output.
|
||||
//
|
||||
// Returns the list of usernames.
|
||||
func parseUsersOutput(s string) []string {
|
||||
var users []string
|
||||
users = append(users, strings.Split(strings.TrimSpace(s), " ")...)
|
||||
return users
|
||||
}
|
||||
|
||||
// parseIDOutput parses the output of the `id' command.
|
||||
//
|
||||
// Returns the parsed uid.
|
||||
func parseIDOutput(s string) (int64, error) {
|
||||
uid, err := strconv.ParseInt(strings.TrimSpace(s), 10, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse uid: %w", err)
|
||||
}
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
// getUserDisplaySessionType returns the display session type (X11 or Wayland) of the given user.
|
||||
func GetUserDisplaySessionType(uid string) (guiSessionType, error) {
|
||||
// GetUserDisplaySessionType returns the display session type (X11 or Wayland)
|
||||
// and active status of the given user. Returns an error if the user doesn't have
|
||||
// a Display session.
|
||||
func GetUserDisplaySessionType(uid string) (*UserDisplaySession, error) {
|
||||
// Get the "Display" session ID of the user.
|
||||
cmd := exec.Command("loginctl", "show-user", uid, "-p", "Display", "--value")
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
return 0, fmt.Errorf("run 'loginctl' to get user GUI session: %w", err)
|
||||
return nil, fmt.Errorf("run 'loginctl' to get user GUI session: %w", err)
|
||||
}
|
||||
guiSessionID := strings.TrimSpace(stdout.String())
|
||||
if guiSessionID == "" {
|
||||
return 0, nil
|
||||
return nil, errors.New("empty display session")
|
||||
}
|
||||
|
||||
// Get the "Type" of session.
|
||||
cmd = exec.Command("loginctl", "show-session", guiSessionID, "-p", "Type", "--value")
|
||||
stdout.Reset()
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
return 0, fmt.Errorf("run 'loginctl' to get user GUI session type: %w", err)
|
||||
return nil, fmt.Errorf("run 'loginctl' to get user GUI session type: %w", err)
|
||||
}
|
||||
guiSessionType := strings.TrimSpace(stdout.String())
|
||||
switch guiSessionType {
|
||||
var sessionType GuiSessionType
|
||||
switch t := strings.TrimSpace(stdout.String()); t {
|
||||
case "":
|
||||
return 0, errors.New("empty GUI session type")
|
||||
return nil, errors.New("empty GUI session type")
|
||||
case "x11":
|
||||
return GuiSessionTypeX11, nil
|
||||
sessionType = GuiSessionTypeX11
|
||||
case "wayland":
|
||||
return GuiSessionTypeWayland, nil
|
||||
sessionType = GuiSessionTypeWayland
|
||||
case "tty":
|
||||
return GuiSessionTypeTty, nil
|
||||
sessionType = GuiSessionTypeTty
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown GUI session type: %q", guiSessionType)
|
||||
return nil, fmt.Errorf("unknown GUI session type: %q", t)
|
||||
}
|
||||
|
||||
// Get the "Active" property of the session.
|
||||
cmd = exec.Command("loginctl", "show-session", guiSessionID, "-p", "Active", "--value")
|
||||
stdout.Reset()
|
||||
cmd.Stdout = &stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("run 'loginctl' to get session active status: %w", err)
|
||||
}
|
||||
active := strings.TrimSpace(stdout.String()) == "yes"
|
||||
return &UserDisplaySession{
|
||||
Type: sessionType,
|
||||
Active: active,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type guiSessionType int
|
||||
type GuiSessionType int
|
||||
|
||||
const (
|
||||
GuiSessionTypeX11 guiSessionType = iota + 1
|
||||
GuiSessionTypeX11 GuiSessionType = iota + 1
|
||||
GuiSessionTypeWayland
|
||||
GuiSessionTypeTty
|
||||
)
|
||||
|
||||
func (s guiSessionType) String() string {
|
||||
func (s GuiSessionType) String() string {
|
||||
if s == GuiSessionTypeX11 {
|
||||
return "x11"
|
||||
}
|
||||
|
|
|
|||
83
orbit/pkg/user/user_linux_test.go
Normal file
83
orbit/pkg/user/user_linux_test.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//go:build linux
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseLoginctlUsersOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []User
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "single user",
|
||||
input: " 1000 alice\n",
|
||||
want: []User{{Name: "alice", ID: 1000}},
|
||||
},
|
||||
{
|
||||
name: "multiple users",
|
||||
input: " 1000 alice\n 1001 bob\n",
|
||||
want: []User{
|
||||
{Name: "alice", ID: 1000},
|
||||
{Name: "bob", ID: 1001},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "includes system user",
|
||||
input: " 0 root\n 1000 alice\n 120 gdm\n",
|
||||
want: []User{
|
||||
{Name: "root", ID: 0},
|
||||
{Name: "alice", ID: 1000},
|
||||
{Name: "gdm", ID: 120},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no leading whitespace",
|
||||
input: "1000 alice\n1001 bob\n",
|
||||
want: []User{
|
||||
{Name: "alice", ID: 1000},
|
||||
{Name: "bob", ID: 1001},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "extra columns (some systemd versions)",
|
||||
input: "1000 alice extra-stuff\n",
|
||||
want: []User{{Name: "alice", ID: 1000}},
|
||||
},
|
||||
{
|
||||
name: "empty output",
|
||||
input: "",
|
||||
wantErr: "no user session found",
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
input: " \n \n",
|
||||
wantErr: "no user session found",
|
||||
},
|
||||
{
|
||||
name: "skips malformed lines",
|
||||
input: "not-a-uid alice\n1000 bob\n",
|
||||
want: []User{{Name: "bob", ID: 1000}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseLoginctlUsersOutput(tt.input)
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package user
|
||||
|
||||
// UserLoggedInViaGui returns the name of the user logged into the machine via the GUI. This
|
||||
// function is only relevant on MacOS, where it is used to prevent errors when launching Fleet
|
||||
// Desktop. For other platforms we return an empty string which can be ignored.
|
||||
// function is only relevant on MacOS and Linux, where it is used to prevent errors when launching Fleet Desktop.
|
||||
func UserLoggedInViaGui() (*string, error) {
|
||||
user := ""
|
||||
return &user, nil
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import (
|
|||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/dialog"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/execuser"
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/user"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const zenityProcessName = "zenity"
|
||||
|
|
@ -88,6 +90,18 @@ func execCmdWithOutput(args ...string) ([]byte, int, error) {
|
|||
opts = append(opts, execuser.WithArg(arg, "")) // Using empty value for positional args
|
||||
}
|
||||
|
||||
// Retrieve and set active GUI user.
|
||||
loggedInUser, err := user.UserLoggedInViaGui()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("user logged in via GUI: %w", err)
|
||||
}
|
||||
if loggedInUser == nil || *loggedInUser == "" {
|
||||
return nil, 0, errors.New("no GUI user found")
|
||||
}
|
||||
log.Debug().Msgf("found GUI user: %s, attempting zenity", *loggedInUser)
|
||||
opts = append(opts, execuser.WithUser(*loggedInUser))
|
||||
|
||||
// Execute zenity.
|
||||
output, exitCode, err := execuser.RunWithOutput(zenityProcessName, opts...)
|
||||
|
||||
// Trim the newline from zenity output
|
||||
|
|
|
|||
Loading…
Reference in a new issue