c6f45fd2ee
This should not have been in init() as it causes these lookups to happen in all reexecs of the Docker binary. The only time it needs to be resolved is when a user is added, which is extremely rare. Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com>
188 lines
5.6 KiB
Go
188 lines
5.6 KiB
Go
package idtools
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// add a user and/or group to Linux /etc/passwd, /etc/group using standard
|
|
// Linux distribution commands:
|
|
// adduser --system --shell /bin/false --disabled-login --disabled-password --no-create-home --group <username>
|
|
// useradd -r -s /bin/false <username>
|
|
|
|
var (
|
|
once sync.Once
|
|
userCommand string
|
|
|
|
cmdTemplates = map[string]string{
|
|
"adduser": "--system --shell /bin/false --no-create-home --disabled-login --disabled-password --group %s",
|
|
"useradd": "-r -s /bin/false %s",
|
|
"usermod": "-%s %d-%d %s",
|
|
}
|
|
|
|
idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`)
|
|
// default length for a UID/GID subordinate range
|
|
defaultRangeLen = 65536
|
|
defaultRangeStart = 100000
|
|
userMod = "usermod"
|
|
)
|
|
|
|
func resolveBinary(binname string) (string, error) {
|
|
binaryPath, err := exec.LookPath(binname)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resolvedPath, err := filepath.EvalSymlinks(binaryPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
//only return no error if the final resolved binary basename
|
|
//matches what was searched for
|
|
if filepath.Base(resolvedPath) == binname {
|
|
return resolvedPath, nil
|
|
}
|
|
return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath)
|
|
}
|
|
|
|
// AddNamespaceRangesUser takes a username and uses the standard system
|
|
// utility to create a system user/group pair used to hold the
|
|
// /etc/sub{uid,gid} ranges which will be used for user namespace
|
|
// mapping ranges in containers.
|
|
func AddNamespaceRangesUser(name string) (int, int, error) {
|
|
if err := addUser(name); err != nil {
|
|
return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
|
|
}
|
|
|
|
// Query the system for the created uid and gid pair
|
|
out, err := execCmd("id", name)
|
|
if err != nil {
|
|
return -1, -1, fmt.Errorf("Error trying to find uid/gid for new user %q: %v", name, err)
|
|
}
|
|
matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out)))
|
|
if len(matches) != 3 {
|
|
return -1, -1, fmt.Errorf("Can't find uid, gid from `id` output: %q", string(out))
|
|
}
|
|
uid, err := strconv.Atoi(matches[1])
|
|
if err != nil {
|
|
return -1, -1, fmt.Errorf("Can't convert found uid (%s) to int: %v", matches[1], err)
|
|
}
|
|
gid, err := strconv.Atoi(matches[2])
|
|
if err != nil {
|
|
return -1, -1, fmt.Errorf("Can't convert found gid (%s) to int: %v", matches[2], err)
|
|
}
|
|
|
|
// Now we need to create the subuid/subgid ranges for our new user/group (system users
|
|
// do not get auto-created ranges in subuid/subgid)
|
|
|
|
if err := createSubordinateRanges(name); err != nil {
|
|
return -1, -1, fmt.Errorf("Couldn't create subordinate ID ranges: %v", err)
|
|
}
|
|
return uid, gid, nil
|
|
}
|
|
|
|
func addUser(userName string) error {
|
|
once.Do(func() {
|
|
// set up which commands are used for adding users/groups dependent on distro
|
|
if _, err := resolveBinary("adduser"); err == nil {
|
|
userCommand = "adduser"
|
|
} else if _, err := resolveBinary("useradd"); err == nil {
|
|
userCommand = "useradd"
|
|
}
|
|
})
|
|
if userCommand == "" {
|
|
return fmt.Errorf("Cannot add user; no useradd/adduser binary found")
|
|
}
|
|
args := fmt.Sprintf(cmdTemplates[userCommand], userName)
|
|
out, err := execCmd(userCommand, args)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to add user with error: %v; output: %q", err, string(out))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createSubordinateRanges(name string) error {
|
|
|
|
// first, we should verify that ranges weren't automatically created
|
|
// by the distro tooling
|
|
ranges, err := parseSubuid(name)
|
|
if err != nil {
|
|
return fmt.Errorf("Error while looking for subuid ranges for user %q: %v", name, err)
|
|
}
|
|
if len(ranges) == 0 {
|
|
// no UID ranges; let's create one
|
|
startID, err := findNextUIDRange()
|
|
if err != nil {
|
|
return fmt.Errorf("Can't find available subuid range: %v", err)
|
|
}
|
|
out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "v", startID, startID+defaultRangeLen-1, name))
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err)
|
|
}
|
|
}
|
|
|
|
ranges, err = parseSubgid(name)
|
|
if err != nil {
|
|
return fmt.Errorf("Error while looking for subgid ranges for user %q: %v", name, err)
|
|
}
|
|
if len(ranges) == 0 {
|
|
// no GID ranges; let's create one
|
|
startID, err := findNextGIDRange()
|
|
if err != nil {
|
|
return fmt.Errorf("Can't find available subgid range: %v", err)
|
|
}
|
|
out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "w", startID, startID+defaultRangeLen-1, name))
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findNextUIDRange() (int, error) {
|
|
ranges, err := parseSubuid("ALL")
|
|
if err != nil {
|
|
return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subuid file: %v", err)
|
|
}
|
|
sort.Sort(ranges)
|
|
return findNextRangeStart(ranges)
|
|
}
|
|
|
|
func findNextGIDRange() (int, error) {
|
|
ranges, err := parseSubgid("ALL")
|
|
if err != nil {
|
|
return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subgid file: %v", err)
|
|
}
|
|
sort.Sort(ranges)
|
|
return findNextRangeStart(ranges)
|
|
}
|
|
|
|
func findNextRangeStart(rangeList ranges) (int, error) {
|
|
startID := defaultRangeStart
|
|
for _, arange := range rangeList {
|
|
if wouldOverlap(arange, startID) {
|
|
startID = arange.Start + arange.Length
|
|
}
|
|
}
|
|
return startID, nil
|
|
}
|
|
|
|
func wouldOverlap(arange subIDRange, ID int) bool {
|
|
low := ID
|
|
high := ID + defaultRangeLen
|
|
if (low >= arange.Start && low <= arange.Start+arange.Length) ||
|
|
(high <= arange.Start+arange.Length && high >= arange.Start) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func execCmd(cmd, args string) ([]byte, error) {
|
|
execCmd := exec.Command(cmd, strings.Split(args, " ")...)
|
|
return execCmd.CombinedOutput()
|
|
}
|