Skip to content
53 changes: 52 additions & 1 deletion internal/system/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"os/user"
"strings"

"github.com/fatih/color"
)
Expand Down Expand Up @@ -58,5 +59,55 @@ func realUser() (*user.User, error) {
return user.Lookup("root")
}

return user.Lookup(realUser)
u, err := user.Lookup(realUser)
if err == nil {
return u, nil
}

var unknownUserErr user.UnknownUserError
if errors.As(err, &unknownUserErr) {
return lookupUserGetent(realUser)
}

return nil, err
}

// getentBinary is the name of the `getent` binary to invoke. It is a variable
// so that tests can replace it to exercise failure modes (e.g. binary missing).
var getentBinary = "getent"

// lookupUserGetent looks up a user via `getent passwd`, which queries NSS and
// therefore works for users provided by SSSD, LDAP, and similar sources. This
// is needed because Go's [user.Lookup] only reads /etc/passwd when the binary
// is built with CGO_ENABLED=0.
//
// This uses exec.Command directly rather than the System worker because it runs
// during System construction, before the worker pipeline is available.
func lookupUserGetent(username string) (*user.User, error) {
out, err := exec.Command(getentBinary, "passwd", username).Output()
if err != nil {
// `getent passwd <key>` exits 2 when the key is not found; treat that
// as "unknown user" to match the stdlib error. Anything else (binary
// missing, NSS misconfiguration, ...) is wrapped so the underlying
// cause is not lost.
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 2 {
return nil, user.UnknownUserError(username)
}
return nil, fmt.Errorf("getent passwd %s: %w", username, err)
}

// getent passwd format: username:password:uid:gid:gecos:home:shell
parts := strings.SplitN(strings.TrimSpace(string(out)), ":", 7)
if len(parts) < 6 {
return nil, fmt.Errorf("getent passwd %s: unexpected output %q", username, string(out))
}

return &user.User{
Username: parts[0],
Uid: parts[2],
Gid: parts[3],
Name: parts[4],
HomeDir: parts[5],
}, nil
}
70 changes: 70 additions & 0 deletions internal/system/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package system

import (
"errors"
"os/exec"
"os/user"
"testing"
)

func TestLookupUserGetent(t *testing.T) {
// Verify getent is available on this system.
if _, err := exec.LookPath("getent"); err != nil {
t.Skip("getent not available on this system")
}
Comment thread
tonyandrewmeyer marked this conversation as resolved.

// user.Current works even for SSSD/LDAP users not in /etc/passwd, because
// it reads the UID from /proc/self rather than searching /etc/passwd by
// name. Use it as the reference to verify lookupUserGetent returns the
// same information.
current, err := user.Current()
if err != nil {
t.Fatalf("user.Current() failed: %v", err)
}

got, err := lookupUserGetent(current.Username)
if err != nil {
t.Fatalf("lookupUserGetent(%q) failed: %v", current.Username, err)
}

if got.Username != current.Username {
t.Errorf("Username: got %q, want %q", got.Username, current.Username)
}
if got.Uid != current.Uid {
t.Errorf("Uid: got %q, want %q", got.Uid, current.Uid)
}
if got.Gid != current.Gid {
t.Errorf("Gid: got %q, want %q", got.Gid, current.Gid)
}
if got.HomeDir != current.HomeDir {
t.Errorf("HomeDir: got %q, want %q", got.HomeDir, current.HomeDir)
}
}

func TestLookupUserGetentUnknownUser(t *testing.T) {
if _, err := exec.LookPath("getent"); err != nil {
t.Skip("getent not available on this system")
}

_, err := lookupUserGetent("nonexistent-user-that-should-not-exist")
var unknownUserErr user.UnknownUserError
if !errors.As(err, &unknownUserErr) {
t.Fatalf("expected user.UnknownUserError, got %v", err)
}
}

func TestLookupUserGetentBinaryMissing(t *testing.T) {
t.Cleanup(func() { getentBinary = "getent" })
getentBinary = "/nonexistent/path/to/getent"

_, err := lookupUserGetent("anyone")
if err == nil {
t.Fatal("expected error when getent binary is missing, got nil")
}
// A missing binary must not be reported as "unknown user" — that would
// hide the real failure from the operator.
var unknownUserErr user.UnknownUserError
if errors.As(err, &unknownUserErr) {
t.Fatalf("missing binary should not surface as UnknownUserError, got %v", err)
}
}
Loading