Browse code

Add support for looking up user/groups via `getent`

When processing the --userns-remap flag, add the
capability to call out to `getent` if the user and
group information is not found via local file
parsing code already in libcontainer/user.

Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com>

Phil Estes authored on 2016/10/21 04:43:42
Showing 4 changed files
... ...
@@ -39,7 +39,6 @@ import (
39 39
 	"github.com/opencontainers/runc/libcontainer/cgroups"
40 40
 	"github.com/opencontainers/runc/libcontainer/label"
41 41
 	rsystem "github.com/opencontainers/runc/libcontainer/system"
42
-	"github.com/opencontainers/runc/libcontainer/user"
43 42
 	specs "github.com/opencontainers/runtime-spec/specs-go"
44 43
 	"github.com/vishvananda/netlink"
45 44
 )
... ...
@@ -894,7 +893,7 @@ func parseRemappedRoot(usergrp string) (string, string, error) {
894 894
 	if uid, err := strconv.ParseInt(idparts[0], 10, 32); err == nil {
895 895
 		// must be a uid; take it as valid
896 896
 		userID = int(uid)
897
-		luser, err := user.LookupUid(userID)
897
+		luser, err := idtools.LookupUID(userID)
898 898
 		if err != nil {
899 899
 			return "", "", fmt.Errorf("Uid %d has no entry in /etc/passwd: %v", userID, err)
900 900
 		}
... ...
@@ -902,7 +901,7 @@ func parseRemappedRoot(usergrp string) (string, string, error) {
902 902
 		if len(idparts) == 1 {
903 903
 			// if the uid was numeric and no gid was specified, take the uid as the gid
904 904
 			groupID = userID
905
-			lgrp, err := user.LookupGid(groupID)
905
+			lgrp, err := idtools.LookupGID(groupID)
906 906
 			if err != nil {
907 907
 				return "", "", fmt.Errorf("Gid %d has no entry in /etc/group: %v", groupID, err)
908 908
 			}
... ...
@@ -915,7 +914,7 @@ func parseRemappedRoot(usergrp string) (string, string, error) {
915 915
 		if lookupName == defaultIDSpecifier {
916 916
 			lookupName = defaultRemappedID
917 917
 		}
918
-		luser, err := user.LookupUser(lookupName)
918
+		luser, err := idtools.LookupUser(lookupName)
919 919
 		if err != nil && idparts[0] != defaultIDSpecifier {
920 920
 			// error if the name requested isn't the special "dockremap" ID
921 921
 			return "", "", fmt.Errorf("Error during uid lookup for %q: %v", lookupName, err)
... ...
@@ -932,7 +931,7 @@ func parseRemappedRoot(usergrp string) (string, string, error) {
932 932
 		username = luser.Name
933 933
 		if len(idparts) == 1 {
934 934
 			// we only have a string username, and no group specified; look up gid from username as group
935
-			group, err := user.LookupGroup(lookupName)
935
+			group, err := idtools.LookupGroup(lookupName)
936 936
 			if err != nil {
937 937
 				return "", "", fmt.Errorf("Error during gid lookup for %q: %v", lookupName, err)
938 938
 			}
... ...
@@ -947,14 +946,14 @@ func parseRemappedRoot(usergrp string) (string, string, error) {
947 947
 		if gid, err := strconv.ParseInt(idparts[1], 10, 32); err == nil {
948 948
 			// must be a gid, take it as valid
949 949
 			groupID = int(gid)
950
-			lgrp, err := user.LookupGid(groupID)
950
+			lgrp, err := idtools.LookupGID(groupID)
951 951
 			if err != nil {
952 952
 				return "", "", fmt.Errorf("Gid %d has no entry in /etc/passwd: %v", groupID, err)
953 953
 			}
954 954
 			groupname = lgrp.Name
955 955
 		} else {
956 956
 			// not a number; attempt a lookup
957
-			if _, err := user.LookupGroup(idparts[1]); err != nil {
957
+			if _, err := idtools.LookupGroup(idparts[1]); err != nil {
958 958
 				return "", "", fmt.Errorf("Error during groupname lookup for %q: %v", idparts[1], err)
959 959
 			}
960 960
 			groupname = idparts[1]
... ...
@@ -3,10 +3,22 @@
3 3
 package idtools
4 4
 
5 5
 import (
6
+	"bytes"
7
+	"fmt"
8
+	"io"
6 9
 	"os"
7 10
 	"path/filepath"
11
+	"strings"
12
+	"sync"
8 13
 
14
+	"github.com/docker/docker/pkg/integration/cmd"
9 15
 	"github.com/docker/docker/pkg/system"
16
+	"github.com/opencontainers/runc/libcontainer/user"
17
+)
18
+
19
+var (
20
+	entOnce   sync.Once
21
+	getentCmd string
10 22
 )
11 23
 
12 24
 func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error {
... ...
@@ -84,3 +96,113 @@ func accessible(isOwner, isGroup bool, perms os.FileMode) bool {
84 84
 	}
85 85
 	return false
86 86
 }
87
+
88
+// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username,
89
+// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
90
+func LookupUser(username string) (user.User, error) {
91
+	// first try a local system files lookup using existing capabilities
92
+	usr, err := user.LookupUser(username)
93
+	if err == nil {
94
+		return usr, nil
95
+	}
96
+	// local files lookup failed; attempt to call `getent` to query configured passwd dbs
97
+	usr, err = getentUser(fmt.Sprintf("%s %s", "passwd", username))
98
+	if err != nil {
99
+		return user.User{}, err
100
+	}
101
+	return usr, nil
102
+}
103
+
104
+// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid,
105
+// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
106
+func LookupUID(uid int) (user.User, error) {
107
+	// first try a local system files lookup using existing capabilities
108
+	usr, err := user.LookupUid(uid)
109
+	if err == nil {
110
+		return usr, nil
111
+	}
112
+	// local files lookup failed; attempt to call `getent` to query configured passwd dbs
113
+	return getentUser(fmt.Sprintf("%s %d", "passwd", uid))
114
+}
115
+
116
+func getentUser(args string) (user.User, error) {
117
+	reader, err := callGetent(args)
118
+	if err != nil {
119
+		return user.User{}, err
120
+	}
121
+	users, err := user.ParsePasswd(reader)
122
+	if err != nil {
123
+		return user.User{}, err
124
+	}
125
+	if len(users) == 0 {
126
+		return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", strings.Split(args, " ")[1])
127
+	}
128
+	return users[0], nil
129
+}
130
+
131
+// LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name,
132
+// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
133
+func LookupGroup(groupname string) (user.Group, error) {
134
+	// first try a local system files lookup using existing capabilities
135
+	group, err := user.LookupGroup(groupname)
136
+	if err == nil {
137
+		return group, nil
138
+	}
139
+	// local files lookup failed; attempt to call `getent` to query configured group dbs
140
+	return getentGroup(fmt.Sprintf("%s %s", "group", groupname))
141
+}
142
+
143
+// LookupGID uses traditional local system files lookup (from libcontainer/user) on a group ID,
144
+// followed by a call to `getent` for supporting host configured non-files passwd and group dbs
145
+func LookupGID(gid int) (user.Group, error) {
146
+	// first try a local system files lookup using existing capabilities
147
+	group, err := user.LookupGid(gid)
148
+	if err == nil {
149
+		return group, nil
150
+	}
151
+	// local files lookup failed; attempt to call `getent` to query configured group dbs
152
+	return getentGroup(fmt.Sprintf("%s %d", "group", gid))
153
+}
154
+
155
+func getentGroup(args string) (user.Group, error) {
156
+	reader, err := callGetent(args)
157
+	if err != nil {
158
+		return user.Group{}, err
159
+	}
160
+	groups, err := user.ParseGroup(reader)
161
+	if err != nil {
162
+		return user.Group{}, err
163
+	}
164
+	if len(groups) == 0 {
165
+		return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", strings.Split(args, " ")[1])
166
+	}
167
+	return groups[0], nil
168
+}
169
+
170
+func callGetent(args string) (io.Reader, error) {
171
+	entOnce.Do(func() { getentCmd, _ = resolveBinary("getent") })
172
+	// if no `getent` command on host, can't do anything else
173
+	if getentCmd == "" {
174
+		return nil, fmt.Errorf("")
175
+	}
176
+	out, err := execCmd(getentCmd, args)
177
+	if err != nil {
178
+		exitCode, errC := cmd.GetExitCode(err)
179
+		if errC != nil {
180
+			return nil, err
181
+		}
182
+		switch exitCode {
183
+		case 1:
184
+			return nil, fmt.Errorf("getent reported invalid parameters/database unknown")
185
+		case 2:
186
+			terms := strings.Split(args, " ")
187
+			return nil, fmt.Errorf("getent unable to find entry %q in %s database", terms[1], terms[0])
188
+		case 3:
189
+			return nil, fmt.Errorf("getent database doesn't support enumeration")
190
+		default:
191
+			return nil, err
192
+		}
193
+
194
+	}
195
+	return bytes.NewReader(out), nil
196
+}
... ...
@@ -2,8 +2,6 @@ package idtools
2 2
 
3 3
 import (
4 4
 	"fmt"
5
-	"os/exec"
6
-	"path/filepath"
7 5
 	"regexp"
8 6
 	"sort"
9 7
 	"strconv"
... ...
@@ -33,23 +31,6 @@ var (
33 33
 	userMod           = "usermod"
34 34
 )
35 35
 
36
-func resolveBinary(binname string) (string, error) {
37
-	binaryPath, err := exec.LookPath(binname)
38
-	if err != nil {
39
-		return "", err
40
-	}
41
-	resolvedPath, err := filepath.EvalSymlinks(binaryPath)
42
-	if err != nil {
43
-		return "", err
44
-	}
45
-	//only return no error if the final resolved binary basename
46
-	//matches what was searched for
47
-	if filepath.Base(resolvedPath) == binname {
48
-		return resolvedPath, nil
49
-	}
50
-	return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath)
51
-}
52
-
53 36
 // AddNamespaceRangesUser takes a username and uses the standard system
54 37
 // utility to create a system user/group pair used to hold the
55 38
 // /etc/sub{uid,gid} ranges which will be used for user namespace
... ...
@@ -181,8 +162,3 @@ func wouldOverlap(arange subIDRange, ID int) bool {
181 181
 	}
182 182
 	return false
183 183
 }
184
-
185
-func execCmd(cmd, args string) ([]byte, error) {
186
-	execCmd := exec.Command(cmd, strings.Split(args, " ")...)
187
-	return execCmd.CombinedOutput()
188
-}
189 184
new file mode 100644
... ...
@@ -0,0 +1,32 @@
0
+// +build !windows
1
+
2
+package idtools
3
+
4
+import (
5
+	"fmt"
6
+	"os/exec"
7
+	"path/filepath"
8
+	"strings"
9
+)
10
+
11
+func resolveBinary(binname string) (string, error) {
12
+	binaryPath, err := exec.LookPath(binname)
13
+	if err != nil {
14
+		return "", err
15
+	}
16
+	resolvedPath, err := filepath.EvalSymlinks(binaryPath)
17
+	if err != nil {
18
+		return "", err
19
+	}
20
+	//only return no error if the final resolved binary basename
21
+	//matches what was searched for
22
+	if filepath.Base(resolvedPath) == binname {
23
+		return resolvedPath, nil
24
+	}
25
+	return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath)
26
+}
27
+
28
+func execCmd(cmd, args string) ([]byte, error) {
29
+	execCmd := exec.Command(cmd, strings.Split(args, " ")...)
30
+	return execCmd.CombinedOutput()
31
+}