]>
Commit | Line | Data |
---|---|---|
9b12e4fe JC |
1 | package idtools |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "os/exec" | |
6 | "path/filepath" | |
7 | "strings" | |
8 | "syscall" | |
9 | ) | |
10 | ||
11 | // add a user and/or group to Linux /etc/passwd, /etc/group using standard | |
12 | // Linux distribution commands: | |
13 | // adduser --uid <id> --shell /bin/login --no-create-home --disabled-login --ingroup <groupname> <username> | |
14 | // useradd -M -u <id> -s /bin/nologin -N -g <groupname> <username> | |
15 | // addgroup --gid <id> <groupname> | |
16 | // groupadd -g <id> <groupname> | |
17 | ||
18 | const baseUID int = 10000 | |
19 | const baseGID int = 10000 | |
20 | const idMAX int = 65534 | |
21 | ||
22 | var ( | |
23 | userCommand string | |
24 | groupCommand string | |
25 | ||
26 | cmdTemplates = map[string]string{ | |
27 | "adduser": "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s", | |
28 | "useradd": "-M -u %d -s /bin/false -N -g %s %s", | |
29 | "addgroup": "--gid %d %s", | |
30 | "groupadd": "-g %d %s", | |
31 | } | |
32 | ) | |
33 | ||
34 | func init() { | |
35 | // set up which commands are used for adding users/groups dependent on distro | |
36 | if _, err := resolveBinary("adduser"); err == nil { | |
37 | userCommand = "adduser" | |
38 | } else if _, err := resolveBinary("useradd"); err == nil { | |
39 | userCommand = "useradd" | |
40 | } | |
41 | if _, err := resolveBinary("addgroup"); err == nil { | |
42 | groupCommand = "addgroup" | |
43 | } else if _, err := resolveBinary("groupadd"); err == nil { | |
44 | groupCommand = "groupadd" | |
45 | } | |
46 | } | |
47 | ||
48 | func resolveBinary(binname string) (string, error) { | |
49 | binaryPath, err := exec.LookPath(binname) | |
50 | if err != nil { | |
51 | return "", err | |
52 | } | |
53 | resolvedPath, err := filepath.EvalSymlinks(binaryPath) | |
54 | if err != nil { | |
55 | return "", err | |
56 | } | |
57 | //only return no error if the final resolved binary basename | |
58 | //matches what was searched for | |
59 | if filepath.Base(resolvedPath) == binname { | |
60 | return resolvedPath, nil | |
61 | } | |
62 | return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) | |
63 | } | |
64 | ||
65 | // AddNamespaceRangesUser takes a name and finds an unused uid, gid pair | |
66 | // and calls the appropriate helper function to add the group and then | |
67 | // the user to the group in /etc/group and /etc/passwd respectively. | |
68 | // This new user's /etc/sub{uid,gid} ranges will be used for user namespace | |
69 | // mapping ranges in containers. | |
70 | func AddNamespaceRangesUser(name string) (int, int, error) { | |
71 | // Find unused uid, gid pair | |
72 | uid, err := findUnusedUID(baseUID) | |
73 | if err != nil { | |
74 | return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err) | |
75 | } | |
76 | gid, err := findUnusedGID(baseGID) | |
77 | if err != nil { | |
78 | return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err) | |
79 | } | |
80 | ||
81 | // First add the group that we will use | |
82 | if err := addGroup(name, gid); err != nil { | |
83 | return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err) | |
84 | } | |
85 | // Add the user as a member of the group | |
86 | if err := addUser(name, uid, name); err != nil { | |
87 | return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err) | |
88 | } | |
89 | return uid, gid, nil | |
90 | } | |
91 | ||
92 | func addUser(userName string, uid int, groupName string) error { | |
93 | ||
94 | if userCommand == "" { | |
95 | return fmt.Errorf("Cannot add user; no useradd/adduser binary found") | |
96 | } | |
97 | args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName) | |
98 | return execAddCmd(userCommand, args) | |
99 | } | |
100 | ||
101 | func addGroup(groupName string, gid int) error { | |
102 | ||
103 | if groupCommand == "" { | |
104 | return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found") | |
105 | } | |
106 | args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName) | |
107 | // only error out if the error isn't that the group already exists | |
108 | // if the group exists then our needs are already met | |
109 | if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") { | |
110 | return err | |
111 | } | |
112 | return nil | |
113 | } | |
114 | ||
115 | func execAddCmd(cmd, args string) error { | |
116 | execCmd := exec.Command(cmd, strings.Split(args, " ")...) | |
117 | out, err := execCmd.CombinedOutput() | |
118 | if err != nil { | |
119 | return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out)) | |
120 | } | |
121 | return nil | |
122 | } | |
123 | ||
124 | func findUnusedUID(startUID int) (int, error) { | |
125 | return findUnused("passwd", startUID) | |
126 | } | |
127 | ||
128 | func findUnusedGID(startGID int) (int, error) { | |
129 | return findUnused("group", startGID) | |
130 | } | |
131 | ||
132 | func findUnused(file string, id int) (int, error) { | |
133 | for { | |
134 | cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id) | |
135 | cmd := exec.Command("sh", "-c", cmdStr) | |
136 | if err := cmd.Run(); err != nil { | |
137 | // if a non-zero return code occurs, then we know the ID was not found | |
138 | // and is usable | |
139 | if exiterr, ok := err.(*exec.ExitError); ok { | |
140 | // The program has exited with an exit code != 0 | |
141 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { | |
142 | if status.ExitStatus() == 1 { | |
143 | //no match, we can use this ID | |
144 | return id, nil | |
145 | } | |
146 | } | |
147 | } | |
148 | return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err) | |
149 | } | |
150 | id++ | |
151 | if id > idMAX { | |
152 | return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file) | |
153 | } | |
154 | } | |
155 | } |