126 lines
3.1 KiB
Go
126 lines
3.1 KiB
Go
package engine
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// User is one identity inside the sandbox, resolved from a docker-style
|
|
// user spec against the image's /etc/passwd and /etc/group.
|
|
type User struct {
|
|
UID, GID int
|
|
Home string
|
|
}
|
|
|
|
// LookupUser resolves `user[:group]` — names or numbers, as docker and
|
|
// podman take them — inside rootfs. A name must exist in the image's
|
|
// passwd/group; a number need not, then the group defaults to the uid
|
|
// and the home to "" (the engine's /root). A named user without an
|
|
// explicit group gets the group of its passwd entry.
|
|
func LookupUser(rootfs, spec string) (User, error) {
|
|
userPart, groupPart, hasGroup := strings.Cut(spec, ":")
|
|
if userPart == "" {
|
|
return User{}, fmt.Errorf("invalid user %q", spec)
|
|
}
|
|
u := User{}
|
|
entry, found, err := passwdEntry(rootfs, userPart)
|
|
if err != nil {
|
|
return User{}, err
|
|
}
|
|
switch {
|
|
case found:
|
|
u.UID, u.GID, u.Home = entry.uid, entry.gid, entry.home
|
|
default:
|
|
n, err := strconv.Atoi(userPart)
|
|
if err != nil || n < 0 {
|
|
return User{}, fmt.Errorf("user %q not found in the image's /etc/passwd", userPart)
|
|
}
|
|
u.UID, u.GID = n, n
|
|
}
|
|
if hasGroup {
|
|
gid, found, err := groupEntry(rootfs, groupPart)
|
|
if err != nil {
|
|
return User{}, err
|
|
}
|
|
if !found {
|
|
n, err := strconv.Atoi(groupPart)
|
|
if err != nil || n < 0 {
|
|
return User{}, fmt.Errorf("group %q not found in the image's /etc/group", groupPart)
|
|
}
|
|
gid = n
|
|
}
|
|
u.GID = gid
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
type passwdLine struct {
|
|
uid, gid int
|
|
home string
|
|
}
|
|
|
|
// passwdEntry finds key (a name or a numeric uid) in rootfs/etc/passwd.
|
|
func passwdEntry(rootfs, key string) (passwdLine, bool, error) {
|
|
var result passwdLine
|
|
found := false
|
|
err := eachColonLine(filepath.Join(rootfs, "etc", "passwd"), func(fields []string) bool {
|
|
if len(fields) < 7 || (fields[0] != key && fields[2] != key) {
|
|
return false
|
|
}
|
|
uid, err1 := strconv.Atoi(fields[2])
|
|
gid, err2 := strconv.Atoi(fields[3])
|
|
if err1 != nil || err2 != nil {
|
|
return false
|
|
}
|
|
result, found = passwdLine{uid: uid, gid: gid, home: fields[5]}, true
|
|
return true
|
|
})
|
|
return result, found, err
|
|
}
|
|
|
|
// groupEntry finds key (a name or a numeric gid) in rootfs/etc/group.
|
|
func groupEntry(rootfs, key string) (int, bool, error) {
|
|
gid, found := 0, false
|
|
err := eachColonLine(filepath.Join(rootfs, "etc", "group"), func(fields []string) bool {
|
|
if len(fields) < 3 || (fields[0] != key && fields[2] != key) {
|
|
return false
|
|
}
|
|
n, err := strconv.Atoi(fields[2])
|
|
if err != nil {
|
|
return false
|
|
}
|
|
gid, found = n, true
|
|
return true
|
|
})
|
|
return gid, found, err
|
|
}
|
|
|
|
// eachColonLine calls fn per line of a colon-separated file until fn
|
|
// returns true; a missing file is not an error (busybox images may
|
|
// lack /etc/group).
|
|
func eachColonLine(path string, fn func(fields []string) bool) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
line := sc.Text()
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if fn(strings.Split(line, ":")) {
|
|
return nil
|
|
}
|
|
}
|
|
return sc.Err()
|
|
}
|