2025-06-27 13:41:13 +00:00
|
|
|
//go:build linux
|
|
|
|
|
// +build linux
|
|
|
|
|
|
|
|
|
|
package user
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os/exec"
|
2026-02-24 15:59:35 +00:00
|
|
|
"os/user"
|
2025-06-27 13:41:13 +00:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type User struct {
|
|
|
|
|
Name string
|
|
|
|
|
ID int64
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 15:59:35 +00:00
|
|
|
func getDisplaySessionFor(user User) *UserDisplaySession {
|
|
|
|
|
// 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" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// Check if the user has an active GUI session.
|
|
|
|
|
userID := strconv.FormatInt(user.ID, 10)
|
|
|
|
|
session, err := GetUserDisplaySessionType(userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Debug().Err(err).Msgf("failed to get user display session for user %s", user.Name)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if !session.Active {
|
|
|
|
|
log.Debug().Msgf("user %s has an inactive display session, skipping", user.Name)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if session.Type == GuiSessionTypeTty {
|
|
|
|
|
log.Debug().Msgf("user %s is logged in via TTY, not GUI", user.Name)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return session
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:41:16 +00:00
|
|
|
// UserLoggedInViaGui returns the username that has an active GUI session.
|
|
|
|
|
// It returns nil, nil if there's no user with an active GUI session.
|
2025-06-27 13:41:13 +00:00
|
|
|
func UserLoggedInViaGui() (*string, error) {
|
2026-02-16 14:41:16 +00:00
|
|
|
users, err := getLoginUsers()
|
2025-06-27 13:41:13 +00:00
|
|
|
if err != nil {
|
2026-02-16 14:41:16 +00:00
|
|
|
return nil, fmt.Errorf("get login users: %w", err)
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
2026-02-10 14:30:24 +00:00
|
|
|
|
2026-02-24 15:59:35 +00:00
|
|
|
for _, u := range users {
|
|
|
|
|
if session := getDisplaySessionFor(u); session != nil {
|
|
|
|
|
return &u.Name, nil
|
2026-02-16 14:41:16 +00:00
|
|
|
}
|
2026-02-24 15:59:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No valid user found
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetCurrentUserDisplaySession returns the Display session of the user associated with the current process
|
|
|
|
|
func GetCurrentUserDisplaySession() (*UserDisplaySession, error) {
|
|
|
|
|
currentUser, err := user.Current()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("get current user: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if currentUser == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uid, err := strconv.ParseInt(currentUser.Uid, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("parse uid: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if session := getDisplaySessionFor(User{Name: currentUser.Username, ID: uid}); session != nil {
|
|
|
|
|
return session, nil
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:41:16 +00:00
|
|
|
// No valid user found
|
|
|
|
|
return nil, nil
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:41:16 +00:00
|
|
|
// 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()
|
2025-06-27 13:41:13 +00:00
|
|
|
if err != nil {
|
2026-02-16 14:41:16 +00:00
|
|
|
return nil, fmt.Errorf("loginctl list-users exec failed: %w, output: %s", err, string(out))
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
2026-02-16 14:41:16 +00:00
|
|
|
return parseLoginctlUsersOutput(string(out))
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:41:16 +00:00
|
|
|
// 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")
|
Run fleet desktop with user SELinux context when applicable (#30882)
For #29793
# Details
This PR changes the way that Orbit launches processes such as the
desktop app on Linux, in order to ensure that on SELinux-enabled systems
the correct user context is set when running the command.
Previously, `sudo -u` was used to launch commands on Linux. This PR
switches to use `runuser` instead, which is recommended in situations
where the root user wants to execute a command as a user with reduced
privileges (see [the blog post by one of the creators of
runuser](https://danwalsh.livejournal.com/55588.html)). This avoids
certain errors that can come from interacting with PAM modules as the
system user.
Additionally, if we detect that SELinux is set up on a system, we now
use `runcon` to force the command to run using the logged-in user's
SELinux context. It's possible that on some systems they may have
configuration where `sudo` will switch to the user's SELinux context
automatically, but this is not guaranteed. Using `runuser` + `runcon` is
our best bet for ensuring that the desktop app (and anything that it
spawns) runs under the correct context.
This PR also does some refactoring so that the three `run` methods for
Linux (`run`, `runWithOutput` and `runWithStdin`) all use the same base
code to create the command with the correct args and env vars, and
differ only in how they handle the i/o.
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- For Orbit and Fleet Desktop changes:
- [x] Make sure fleetd is compatible 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] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [x] ~Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.~ (n/a, code is linux only)
- [x] ~Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~ n/a
# Testing
- [x] Ubuntu with SELinux on
- [x] Ubuntu with SELinux off
- [ ] Fedora with SELinux on
- [ ] Fedora with SELinux off
- [ ] Debian with SELinux on
- [x] Debian with SELinux off
- [x] `runWithOutput` still works (tested with `go run
./tools/dialog/main.go --dialog=zenity`)
- [ ] ~`runWithStdin` still works~ (this isn't currently used by Linux)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved security and user context handling when launching the fleet
desktop application on Linux systems.
* **Refactor**
* Enhanced process launch mechanism to use proper SELinux context and
user session, ensuring processes start under the correct user and
security environment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 13:44:13 +00:00
|
|
|
}
|
2026-02-16 14:41:16 +00:00
|
|
|
return users, nil
|
Run fleet desktop with user SELinux context when applicable (#30882)
For #29793
# Details
This PR changes the way that Orbit launches processes such as the
desktop app on Linux, in order to ensure that on SELinux-enabled systems
the correct user context is set when running the command.
Previously, `sudo -u` was used to launch commands on Linux. This PR
switches to use `runuser` instead, which is recommended in situations
where the root user wants to execute a command as a user with reduced
privileges (see [the blog post by one of the creators of
runuser](https://danwalsh.livejournal.com/55588.html)). This avoids
certain errors that can come from interacting with PAM modules as the
system user.
Additionally, if we detect that SELinux is set up on a system, we now
use `runcon` to force the command to run using the logged-in user's
SELinux context. It's possible that on some systems they may have
configuration where `sudo` will switch to the user's SELinux context
automatically, but this is not guaranteed. Using `runuser` + `runcon` is
our best bet for ensuring that the desktop app (and anything that it
spawns) runs under the correct context.
This PR also does some refactoring so that the three `run` methods for
Linux (`run`, `runWithOutput` and `runWithStdin`) all use the same base
code to create the command with the correct args and env vars, and
differ only in how they handle the i/o.
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- For Orbit and Fleet Desktop changes:
- [x] Make sure fleetd is compatible 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] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [x] ~Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.~ (n/a, code is linux only)
- [x] ~Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~ n/a
# Testing
- [x] Ubuntu with SELinux on
- [x] Ubuntu with SELinux off
- [ ] Fedora with SELinux on
- [ ] Fedora with SELinux off
- [ ] Debian with SELinux on
- [x] Debian with SELinux off
- [x] `runWithOutput` still works (tested with `go run
./tools/dialog/main.go --dialog=zenity`)
- [ ] ~`runWithStdin` still works~ (this isn't currently used by Linux)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved security and user context handling when launching the fleet
desktop application on Linux systems.
* **Refactor**
* Enhanced process launch mechanism to use proper SELinux context and
user session, ensuring processes start under the correct user and
security environment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-25 13:44:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:41:16 +00:00
|
|
|
// UserDisplaySession holds the display session type and active status for a user.
|
|
|
|
|
type UserDisplaySession struct {
|
2026-02-24 15:59:35 +00:00
|
|
|
Type GuiSessionType
|
|
|
|
|
Active bool
|
|
|
|
|
Desktop string
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-24 15:59:35 +00:00
|
|
|
// GetUserDisplaySessionType returns the display session type (X11 or Wayland),
|
|
|
|
|
// active status, and desktop session env var of the given user. Returns an error if the user doesn't have
|
2026-02-16 14:41:16 +00:00
|
|
|
// a Display session.
|
|
|
|
|
func GetUserDisplaySessionType(uid string) (*UserDisplaySession, error) {
|
|
|
|
|
// Get the "Display" session ID of the user.
|
2025-06-27 13:41:13 +00:00
|
|
|
cmd := exec.Command("loginctl", "show-user", uid, "-p", "Display", "--value")
|
|
|
|
|
var stdout bytes.Buffer
|
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
2026-02-16 14:41:16 +00:00
|
|
|
return nil, fmt.Errorf("run 'loginctl' to get user GUI session: %w", err)
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
|
|
|
|
guiSessionID := strings.TrimSpace(stdout.String())
|
|
|
|
|
if guiSessionID == "" {
|
2026-02-16 14:41:16 +00:00
|
|
|
return nil, errors.New("empty display session")
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
2026-02-16 14:41:16 +00:00
|
|
|
|
|
|
|
|
// Get the "Type" of session.
|
2025-06-27 13:41:13 +00:00
|
|
|
cmd = exec.Command("loginctl", "show-session", guiSessionID, "-p", "Type", "--value")
|
|
|
|
|
stdout.Reset()
|
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
2026-02-16 14:41:16 +00:00
|
|
|
return nil, fmt.Errorf("run 'loginctl' to get user GUI session type: %w", err)
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
2026-02-16 14:41:16 +00:00
|
|
|
var sessionType GuiSessionType
|
|
|
|
|
switch t := strings.TrimSpace(stdout.String()); t {
|
2025-06-27 13:41:13 +00:00
|
|
|
case "":
|
2026-02-16 14:41:16 +00:00
|
|
|
return nil, errors.New("empty GUI session type")
|
2025-06-27 13:41:13 +00:00
|
|
|
case "x11":
|
2026-02-16 14:41:16 +00:00
|
|
|
sessionType = GuiSessionTypeX11
|
2025-06-27 13:41:13 +00:00
|
|
|
case "wayland":
|
2026-02-16 14:41:16 +00:00
|
|
|
sessionType = GuiSessionTypeWayland
|
2025-06-27 13:41:13 +00:00
|
|
|
case "tty":
|
2026-02-16 14:41:16 +00:00
|
|
|
sessionType = GuiSessionTypeTty
|
2025-06-27 13:41:13 +00:00
|
|
|
default:
|
2026-02-16 14:41:16 +00:00
|
|
|
return nil, fmt.Errorf("unknown GUI session type: %q", t)
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
2026-02-16 14:41:16 +00:00
|
|
|
|
|
|
|
|
// 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"
|
2026-02-24 15:59:35 +00:00
|
|
|
|
|
|
|
|
// Get the "Desktop" property of the session.
|
|
|
|
|
cmd = exec.Command("loginctl", "show-session", guiSessionID, "-p", "Desktop", "--value")
|
|
|
|
|
stdout.Reset()
|
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
|
desktop := ""
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
log.Debug().Err(err).Msgf("failed to get desktop session for user %s", uid)
|
|
|
|
|
} else {
|
|
|
|
|
desktop = strings.TrimSpace(stdout.String())
|
|
|
|
|
}
|
2026-02-16 14:41:16 +00:00
|
|
|
return &UserDisplaySession{
|
2026-02-24 15:59:35 +00:00
|
|
|
Type: sessionType,
|
|
|
|
|
Active: active,
|
|
|
|
|
Desktop: desktop,
|
2026-02-16 14:41:16 +00:00
|
|
|
}, nil
|
2025-06-27 13:41:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 14:41:16 +00:00
|
|
|
type GuiSessionType int
|
2025-06-27 13:41:13 +00:00
|
|
|
|
|
|
|
|
const (
|
2026-02-16 14:41:16 +00:00
|
|
|
GuiSessionTypeX11 GuiSessionType = iota + 1
|
2025-06-27 13:41:13 +00:00
|
|
|
GuiSessionTypeWayland
|
|
|
|
|
GuiSessionTypeTty
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-16 14:41:16 +00:00
|
|
|
func (s GuiSessionType) String() string {
|
2025-06-27 13:41:13 +00:00
|
|
|
if s == GuiSessionTypeX11 {
|
|
|
|
|
return "x11"
|
|
|
|
|
}
|
|
|
|
|
if s == GuiSessionTypeTty {
|
|
|
|
|
return "tty"
|
|
|
|
|
}
|
|
|
|
|
return "wayland"
|
|
|
|
|
}
|