mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-03-13 10:26:06 +00:00
Add platform-specific username detection for Control D metadata: - macOS: directory services (dscl) with console user fallback - Linux: systemd loginctl, utmp, /etc/passwd traversal - Windows: WTS session enumeration, registry, token lookup
135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
//go:build darwin
|
|
|
|
package ctrld
|
|
|
|
import (
|
|
"context"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// DiscoverMainUser attempts to find the primary user on macOS systems.
|
|
// This is designed to work reliably under RMM deployments where traditional
|
|
// environment variables and session detection may not be available.
|
|
//
|
|
// Priority chain (deterministic, lowest UID wins among candidates):
|
|
// 1. Console user from stat -f %Su /dev/console
|
|
// 2. Active console session user via scutil
|
|
// 3. First user with UID >= 501 from dscl (standard macOS user range)
|
|
func DiscoverMainUser(ctx context.Context) string {
|
|
logger := LoggerFromCtx(ctx).Debug()
|
|
|
|
// Method 1: Check console owner via stat
|
|
logger.Msg("attempting to discover user via console stat")
|
|
if user := getConsoleUser(ctx); user != "" && user != "root" {
|
|
logger.Str("method", "stat").Str("user", user).Msg("found user via console stat")
|
|
return user
|
|
}
|
|
|
|
// Method 2: Check active console session via scutil
|
|
logger.Msg("attempting to discover user via scutil ConsoleUser")
|
|
if user := getScutilConsoleUser(ctx); user != "" && user != "root" {
|
|
logger.Str("method", "scutil").Str("user", user).Msg("found user via scutil ConsoleUser")
|
|
return user
|
|
}
|
|
|
|
// Method 3: Find lowest UID >= 501 from directory services
|
|
logger.Msg("attempting to discover user via dscl directory scan")
|
|
if user := getLowestRegularUser(ctx); user != "" {
|
|
logger.Str("method", "dscl").Str("user", user).Msg("found user via dscl scan")
|
|
return user
|
|
}
|
|
|
|
logger.Msg("all user discovery methods failed")
|
|
return "unknown"
|
|
}
|
|
|
|
// getConsoleUser uses stat to find the owner of /dev/console
|
|
func getConsoleUser(ctx context.Context) string {
|
|
cmd := exec.CommandContext(ctx, "stat", "-f", "%Su", "/dev/console")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
LoggerFromCtx(ctx).Debug().Err(err).Msg("failed to stat /dev/console")
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
// getScutilConsoleUser uses scutil to get the current console user
|
|
func getScutilConsoleUser(ctx context.Context) string {
|
|
cmd := exec.CommandContext(ctx, "scutil", "-r", "ConsoleUser")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
LoggerFromCtx(ctx).Debug().Err(err).Msg("failed to get ConsoleUser via scutil")
|
|
return ""
|
|
}
|
|
|
|
lines := strings.Split(string(out), "\n")
|
|
for _, line := range lines {
|
|
if strings.Contains(line, "Name :") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 3 {
|
|
return strings.TrimSpace(parts[2])
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// getLowestRegularUser finds the user with the lowest UID >= 501
|
|
func getLowestRegularUser(ctx context.Context) string {
|
|
// Get list of all users with UID >= 501
|
|
cmd := exec.CommandContext(ctx, "dscl", ".", "list", "/Users", "UniqueID")
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
LoggerFromCtx(ctx).Debug().Err(err).Msg("failed to list users via dscl")
|
|
return ""
|
|
}
|
|
|
|
var candidates []struct {
|
|
name string
|
|
uid int
|
|
}
|
|
|
|
lines := strings.Split(string(out), "\n")
|
|
for _, line := range lines {
|
|
fields := strings.Fields(line)
|
|
if len(fields) != 2 {
|
|
continue
|
|
}
|
|
|
|
username := fields[0]
|
|
uidStr := fields[1]
|
|
|
|
uid, err := strconv.Atoi(uidStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Only consider regular users (UID >= 501 on macOS)
|
|
if uid >= 501 {
|
|
candidates = append(candidates, struct {
|
|
name string
|
|
uid int
|
|
}{username, uid})
|
|
}
|
|
}
|
|
|
|
if len(candidates) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Find the candidate with the lowest UID (deterministic choice)
|
|
lowestUID := candidates[0].uid
|
|
result := candidates[0].name
|
|
|
|
for _, candidate := range candidates[1:] {
|
|
if candidate.uid < lowestUID {
|
|
lowestUID = candidate.uid
|
|
result = candidate.name
|
|
}
|
|
}
|
|
|
|
return result
|
|
} |