Ver código fonte

Add utility/support package for user namespace support

The `pkg/idtools` package supports the creation of user(s) for
retrieving /etc/sub{u,g}id ranges and creation of the UID/GID mappings
provided to clone() to add support for user namespaces in Docker.

Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com> (github: estesp)
Phil Estes 9 anos atrás
pai
commit
9a3ab0358e

+ 207 - 0
pkg/idtools/idtools.go

@@ -0,0 +1,207 @@
+package idtools
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+
+	"github.com/docker/docker/pkg/system"
+)
+
+// IDMap contains a single entry for user namespace range remapping. An array
+// of IDMap entries represents the structure that will be provided to the Linux
+// kernel for creating a user namespace.
+type IDMap struct {
+	ContainerID int `json:"container_id"`
+	HostID      int `json:"host_id"`
+	Size        int `json:"size"`
+}
+
+type subIDRange struct {
+	Start  int
+	Length int
+}
+
+type ranges []subIDRange
+
+func (e ranges) Len() int           { return len(e) }
+func (e ranges) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
+func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start }
+
+const (
+	subuidFileName string = "/etc/subuid"
+	subgidFileName string = "/etc/subgid"
+)
+
+// MkdirAllAs creates a directory (include any along the path) and then modifies
+// ownership to the requested uid/gid.  If the directory already exists, this
+// function will still change ownership to the requested uid/gid pair.
+func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
+	return mkdirAs(path, mode, ownerUID, ownerGID, true)
+}
+
+// MkdirAs creates a directory and then modifies ownership to the requested uid/gid.
+// If the directory already exists, this function still changes ownership
+func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
+	return mkdirAs(path, mode, ownerUID, ownerGID, false)
+}
+
+func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error {
+	if mkAll {
+		if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) {
+			return err
+		}
+	} else {
+		if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) {
+			return err
+		}
+	}
+	// even if it existed, we will chown to change ownership as requested
+	if err := os.Chown(path, ownerUID, ownerGID); err != nil {
+		return err
+	}
+	return nil
+}
+
+// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
+// If the maps are empty, then the root uid/gid will default to "real" 0/0
+func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
+	var uid, gid int
+
+	if uidMap != nil {
+		xUID, err := ToHost(0, uidMap)
+		if err != nil {
+			return -1, -1, err
+		}
+		uid = xUID
+	}
+	if gidMap != nil {
+		xGID, err := ToHost(0, gidMap)
+		if err != nil {
+			return -1, -1, err
+		}
+		gid = xGID
+	}
+	return uid, gid, nil
+}
+
+// ToContainer takes an id mapping, and uses it to translate a
+// host ID to the remapped ID. If no map is provided, then the translation
+// assumes a 1-to-1 mapping and returns the passed in id
+func ToContainer(hostID int, idMap []IDMap) (int, error) {
+	if idMap == nil {
+		return hostID, nil
+	}
+	for _, m := range idMap {
+		if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) {
+			contID := m.ContainerID + (hostID - m.HostID)
+			return contID, nil
+		}
+	}
+	return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID)
+}
+
+// ToHost takes an id mapping and a remapped ID, and translates the
+// ID to the mapped host ID. If no map is provided, then the translation
+// assumes a 1-to-1 mapping and returns the passed in id #
+func ToHost(contID int, idMap []IDMap) (int, error) {
+	if idMap == nil {
+		return contID, nil
+	}
+	for _, m := range idMap {
+		if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) {
+			hostID := m.HostID + (contID - m.ContainerID)
+			return hostID, nil
+		}
+	}
+	return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID)
+}
+
+// CreateIDMappings takes a requested user and group name and
+// using the data from /etc/sub{uid,gid} ranges, creates the
+// proper uid and gid remapping ranges for that user/group pair
+func CreateIDMappings(username, groupname string) ([]IDMap, []IDMap, error) {
+	subuidRanges, err := parseSubuid(username)
+	if err != nil {
+		return nil, nil, err
+	}
+	subgidRanges, err := parseSubgid(groupname)
+	if err != nil {
+		return nil, nil, err
+	}
+	if len(subuidRanges) == 0 {
+		return nil, nil, fmt.Errorf("No subuid ranges found for user %q", username)
+	}
+	if len(subgidRanges) == 0 {
+		return nil, nil, fmt.Errorf("No subgid ranges found for group %q", groupname)
+	}
+
+	return createIDMap(subuidRanges), createIDMap(subgidRanges), nil
+}
+
+func createIDMap(subidRanges ranges) []IDMap {
+	idMap := []IDMap{}
+
+	// sort the ranges by lowest ID first
+	sort.Sort(subidRanges)
+	containerID := 0
+	for _, idrange := range subidRanges {
+		idMap = append(idMap, IDMap{
+			ContainerID: containerID,
+			HostID:      idrange.Start,
+			Size:        idrange.Length,
+		})
+		containerID = containerID + idrange.Length
+	}
+	return idMap
+}
+
+func parseSubuid(username string) (ranges, error) {
+	return parseSubidFile(subuidFileName, username)
+}
+
+func parseSubgid(username string) (ranges, error) {
+	return parseSubidFile(subgidFileName, username)
+}
+
+func parseSubidFile(path, username string) (ranges, error) {
+	var rangeList ranges
+
+	subidFile, err := os.Open(path)
+	if err != nil {
+		return rangeList, err
+	}
+	defer subidFile.Close()
+
+	s := bufio.NewScanner(subidFile)
+	for s.Scan() {
+		if err := s.Err(); err != nil {
+			return rangeList, err
+		}
+
+		text := strings.TrimSpace(s.Text())
+		if text == "" {
+			continue
+		}
+		parts := strings.Split(text, ":")
+		if len(parts) != 3 {
+			return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path)
+		}
+		if parts[0] == username {
+			// return the first entry for a user; ignores potential for multiple ranges per user
+			startid, err := strconv.Atoi(parts[1])
+			if err != nil {
+				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
+			}
+			length, err := strconv.Atoi(parts[2])
+			if err != nil {
+				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
+			}
+			rangeList = append(rangeList, subIDRange{startid, length})
+		}
+	}
+	return rangeList, nil
+}

+ 155 - 0
pkg/idtools/usergroupadd_linux.go

@@ -0,0 +1,155 @@
+package idtools
+
+import (
+	"fmt"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"syscall"
+)
+
+// add a user and/or group to Linux /etc/passwd, /etc/group using standard
+// Linux distribution commands:
+// adduser --uid <id> --shell /bin/login --no-create-home --disabled-login --ingroup <groupname> <username>
+// useradd -M -u <id> -s /bin/nologin -N -g <groupname> <username>
+// addgroup --gid <id> <groupname>
+// groupadd -g <id> <groupname>
+
+const baseUID int = 10000
+const baseGID int = 10000
+const idMAX int = 65534
+
+var (
+	userCommand  string
+	groupCommand string
+
+	cmdTemplates = map[string]string{
+		"adduser":  "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s",
+		"useradd":  "-M -u %d -s /bin/false -N -g %s %s",
+		"addgroup": "--gid %d %s",
+		"groupadd": "-g %d %s",
+	}
+)
+
+func init() {
+	// 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 _, err := resolveBinary("addgroup"); err == nil {
+		groupCommand = "addgroup"
+	} else if _, err := resolveBinary("groupadd"); err == nil {
+		groupCommand = "groupadd"
+	}
+}
+
+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 name and finds an unused uid, gid pair
+// and calls the appropriate helper function to add the group and then
+// the user to the group in /etc/group and /etc/passwd respectively.
+// This new user's /etc/sub{uid,gid} ranges will be used for user namespace
+// mapping ranges in containers.
+func AddNamespaceRangesUser(name string) (int, int, error) {
+	// Find unused uid, gid pair
+	uid, err := findUnusedUID(baseUID)
+	if err != nil {
+		return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err)
+	}
+	gid, err := findUnusedGID(baseGID)
+	if err != nil {
+		return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err)
+	}
+
+	// First add the group that we will use
+	if err := addGroup(name, gid); err != nil {
+		return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err)
+	}
+	// Add the user as a member of the group
+	if err := addUser(name, uid, name); err != nil {
+		return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err)
+	}
+	return uid, gid, nil
+}
+
+func addUser(userName string, uid int, groupName string) error {
+
+	if userCommand == "" {
+		return fmt.Errorf("Cannot add user; no useradd/adduser binary found")
+	}
+	args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName)
+	return execAddCmd(userCommand, args)
+}
+
+func addGroup(groupName string, gid int) error {
+
+	if groupCommand == "" {
+		return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found")
+	}
+	args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName)
+	// only error out if the error isn't that the group already exists
+	// if the group exists then our needs are already met
+	if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") {
+		return err
+	}
+	return nil
+}
+
+func execAddCmd(cmd, args string) error {
+	execCmd := exec.Command(cmd, strings.Split(args, " ")...)
+	out, err := execCmd.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out))
+	}
+	return nil
+}
+
+func findUnusedUID(startUID int) (int, error) {
+	return findUnused("passwd", startUID)
+}
+
+func findUnusedGID(startGID int) (int, error) {
+	return findUnused("group", startGID)
+}
+
+func findUnused(file string, id int) (int, error) {
+	for {
+		cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id)
+		cmd := exec.Command("sh", "-c", cmdStr)
+		if err := cmd.Run(); err != nil {
+			// if a non-zero return code occurs, then we know the ID was not found
+			// and is usable
+			if exiterr, ok := err.(*exec.ExitError); ok {
+				// The program has exited with an exit code != 0
+				if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
+					if status.ExitStatus() == 1 {
+						//no match, we can use this ID
+						return id, nil
+					}
+				}
+			}
+			return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err)
+		}
+		id++
+		if id > idMAX {
+			return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file)
+		}
+	}
+}

+ 12 - 0
pkg/idtools/usergroupadd_unsupported.go

@@ -0,0 +1,12 @@
+// +build !linux
+
+package idtools
+
+import "fmt"
+
+// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair
+// and calls the appropriate helper function to add the group and then
+// the user to the group in /etc/group and /etc/passwd respectively.
+func AddNamespaceRangesUser(name string) (int, int, error) {
+	return -1, -1, fmt.Errorf("No support for adding users or groups on this OS")
+}