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 --shell /bin/login --no-create-home --disabled-login --ingroup // useradd -M -u -s /bin/nologin -N -g // addgroup --gid // groupadd -g 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) } } }