Files
coder/agent/usershell/usershell.go
T
Mathias Fredriksson 3955df796e fix(agent): unify working directory resolution (#26102)
agentssh's CommandEnv, sftpHandler, and agentproc each resolved the
session working directory on their own and had drifted: sftpHandler used
the configured directory without checking it exists and bypassed the
injected EnvInfoer, while the others stat-checked and fell back to home.
Home and shell lookups could also skip the EnvInfoer seam through the
exported usershell.HomeDir and Get.

Resolve through a single usershell.ResolveWorkingDirectory and confine
host home and shell lookups to usershell, so SSH sessions, the process
API, and tests can no longer diverge and the injected environment is
always honored. This also fixes SFTP landing in a configured directory
that no longer exists.

Refs coder/coder#26099
2026-06-08 14:24:32 +03:00

93 lines
2.7 KiB
Go

package usershell
import (
"os"
"os/user"
"github.com/spf13/afero"
"golang.org/x/xerrors"
)
// homeDir returns the home directory of the current user, giving
// priority to the $HOME environment variable. It backs
// SystemEnvInfo.HomeDir. Callers outside this package resolve the home
// directory through an EnvInfoer so the injected environment is honored.
func homeDir() (string, error) {
// First we check the environment.
homedir, err := os.UserHomeDir()
if err == nil {
return homedir, nil
}
// As a fallback, we try the user information.
u, err := user.Current()
if err != nil {
return "", xerrors.Errorf("current user: %w", err)
}
return u.HomeDir, nil
}
// ResolveWorkingDirectory returns dir when it is non-empty and an existing
// directory on fs. Otherwise it falls back to the home directory
// reported by ei. SSH sessions and the process API share this so their
// working directory resolution cannot drift, and the home fallback goes
// through the injected EnvInfoer rather than the host directly.
func ResolveWorkingDirectory(fs afero.Fs, ei EnvInfoer, dir string) (string, error) {
if dir != "" {
if info, err := fs.Stat(dir); err == nil && info.IsDir() {
return dir, nil
}
}
return ei.HomeDir()
}
// EnvInfoer encapsulates external information about the environment.
type EnvInfoer interface {
// User returns the current user.
User() (*user.User, error)
// Environ returns the environment variables of the current process.
Environ() []string
// HomeDir returns the home directory of the current user.
HomeDir() (string, error)
// Shell returns the shell of the given user.
Shell(username string) (string, error)
// ModifyCommand modifies the command and arguments before execution based on
// the environment. This is useful for executing a command inside a container.
// In the default case, the command and arguments are returned unchanged.
ModifyCommand(name string, args ...string) (string, []string)
}
// SystemEnvInfo encapsulates the information about the environment
// just using the default Go implementations.
type SystemEnvInfo struct{}
func (SystemEnvInfo) User() (*user.User, error) {
return user.Current()
}
func (SystemEnvInfo) Environ() []string {
var env []string
for _, e := range os.Environ() {
// Ignore GOTRACEBACK=none, as it disables stack traces, it can
// be set on the agent due to changes in capabilities.
// https://pkg.go.dev/runtime#hdr-Security.
if e == "GOTRACEBACK=none" {
continue
}
env = append(env, e)
}
return env
}
func (SystemEnvInfo) HomeDir() (string, error) {
return homeDir()
}
func (SystemEnvInfo) Shell(username string) (string, error) {
return get(username)
}
func (SystemEnvInfo) ModifyCommand(name string, args ...string) (string, []string) {
return name, args
}