Skip to content

Commit ef3397c

Browse files
cyphartianon
authored andcommitted
user: *: refactor and expand libcontainer/user API
This patch refactors most of GetUserGroupSupplementaryHome and its signature, to make using it much simpler. The private parsing ftunctions have also been exposed (parsePasswdFile, parseGroupFile) to allow custom data source to be used (increasing the versatility of the user/ tools). In addition, file path wrappers around the formerly private API functions have been added to make usage of the API for callers easier if the files that are being parsed are on the filesystem (while the io.Reader APIs are exposed for non-traditional usecases). Signed-off-by: Aleksa Sarai <[email protected]> (github: cyphar)
1 parent e70ad14 commit ef3397c

2 files changed

Lines changed: 144 additions & 52 deletions

File tree

user/user.go

Lines changed: 142 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,36 @@ func parseLine(line string, v ...interface{}) {
6969
}
7070
}
7171

72-
func ParsePasswd() ([]*User, error) {
73-
return ParsePasswdFilter(nil)
72+
func ParsePasswdFile(path string) ([]User, error) {
73+
passwd, err := os.Open(path)
74+
if err != nil {
75+
return nil, err
76+
}
77+
defer passwd.Close()
78+
return ParsePasswd(passwd)
79+
}
80+
81+
func ParsePasswd(passwd io.Reader) ([]User, error) {
82+
return ParsePasswdFilter(passwd, nil)
7483
}
7584

76-
func ParsePasswdFilter(filter func(*User) bool) ([]*User, error) {
77-
f, err := os.Open("/etc/passwd")
85+
func ParsePasswdFileFilter(path string, filter func(User) bool) ([]User, error) {
86+
passwd, err := os.Open(path)
7887
if err != nil {
7988
return nil, err
8089
}
81-
defer f.Close()
82-
return parsePasswdFile(f, filter)
90+
defer passwd.Close()
91+
return ParsePasswdFilter(passwd, filter)
8392
}
8493

85-
func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
94+
func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) {
95+
if r == nil {
96+
return nil, fmt.Errorf("nil source for passwd-formatted data")
97+
}
98+
8699
var (
87100
s = bufio.NewScanner(r)
88-
out = []*User{}
101+
out = []User{}
89102
)
90103

91104
for s.Scan() {
@@ -103,7 +116,7 @@ func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
103116
// Name:Pass:Uid:Gid:Gecos:Home:Shell
104117
// root:x:0:0:root:/root:/bin/bash
105118
// adm:x:3:4:adm:/var/adm:/bin/false
106-
p := &User{}
119+
p := User{}
107120
parseLine(
108121
text,
109122
&p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell,
@@ -117,23 +130,36 @@ func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) {
117130
return out, nil
118131
}
119132

120-
func ParseGroup() ([]*Group, error) {
121-
return ParseGroupFilter(nil)
133+
func ParseGroupFile(path string) ([]Group, error) {
134+
group, err := os.Open(path)
135+
if err != nil {
136+
return nil, err
137+
}
138+
defer group.Close()
139+
return ParseGroup(group)
140+
}
141+
142+
func ParseGroup(group io.Reader) ([]Group, error) {
143+
return ParseGroupFilter(group, nil)
122144
}
123145

124-
func ParseGroupFilter(filter func(*Group) bool) ([]*Group, error) {
125-
f, err := os.Open("/etc/group")
146+
func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) {
147+
group, err := os.Open(path)
126148
if err != nil {
127149
return nil, err
128150
}
129-
defer f.Close()
130-
return parseGroupFile(f, filter)
151+
defer group.Close()
152+
return ParseGroupFilter(group, filter)
131153
}
132154

133-
func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
155+
func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
156+
if r == nil {
157+
return nil, fmt.Errorf("nil source for group-formatted data")
158+
}
159+
134160
var (
135161
s = bufio.NewScanner(r)
136-
out = []*Group{}
162+
out = []Group{}
137163
)
138164

139165
for s.Scan() {
@@ -151,7 +177,7 @@ func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
151177
// Name:Pass:Gid:List
152178
// root:x:0:root
153179
// adm:x:4:root,adm,daemon
154-
p := &Group{}
180+
p := Group{}
155181
parseLine(
156182
text,
157183
&p.Name, &p.Pass, &p.Gid, &p.List,
@@ -165,94 +191,160 @@ func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) {
165191
return out, nil
166192
}
167193

168-
// Given a string like "user", "1000", "user:group", "1000:1000", returns the uid, gid, list of supplementary group IDs, and home directory, if available and/or applicable.
169-
func GetUserGroupSupplementaryHome(userSpec string, defaultUid, defaultGid int, defaultHome string) (int, int, []int, string, error) {
170-
var (
171-
uid = defaultUid
172-
gid = defaultGid
173-
suppGids = []int{}
174-
home = defaultHome
194+
type ExecUser struct {
195+
Uid, Gid int
196+
Sgids []int
197+
Home string
198+
}
175199

200+
// GetExecUserFile is a wrapper for GetExecUser. It reads data from each of the
201+
// given file paths and uses that data as the arguments to GetExecUser. If the
202+
// files cannot be opened for any reason, the error is ignored and a nil
203+
// io.Reader is passed instead.
204+
func GetExecUserFile(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) {
205+
passwd, err := os.Open(passwdPath)
206+
if err != nil {
207+
passwd = nil
208+
} else {
209+
defer passwd.Close()
210+
}
211+
212+
group, err := os.Open(groupPath)
213+
if err != nil {
214+
group = nil
215+
} else {
216+
defer group.Close()
217+
}
218+
219+
return GetExecUser(userSpec, defaults, passwd, group)
220+
}
221+
222+
// GetExecUser parses a user specification string (using the passwd and group
223+
// readers as sources for /etc/passwd and /etc/group data, respectively). In
224+
// the case of blank fields or missing data from the sources, the values in
225+
// defaults is used.
226+
//
227+
// GetExecUser will return an error if a user or group literal could not be
228+
// found in any entry in passwd and group respectively.
229+
//
230+
// Examples of valid user specifications are:
231+
// * ""
232+
// * "user"
233+
// * "uid"
234+
// * "user:group"
235+
// * "uid:gid
236+
// * "user:gid"
237+
// * "uid:group"
238+
func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) {
239+
var (
176240
userArg, groupArg string
241+
name string
177242
)
178243

244+
if defaults == nil {
245+
defaults = new(ExecUser)
246+
}
247+
248+
// Copy over defaults.
249+
user := &ExecUser{
250+
Uid: defaults.Uid,
251+
Gid: defaults.Gid,
252+
Sgids: defaults.Sgids,
253+
Home: defaults.Home,
254+
}
255+
256+
// Sgids slice *cannot* be nil.
257+
if user.Sgids == nil {
258+
user.Sgids = []int{}
259+
}
260+
179261
// allow for userArg to have either "user" syntax, or optionally "user:group" syntax
180262
parseLine(userSpec, &userArg, &groupArg)
181263

182-
users, err := ParsePasswdFilter(func(u *User) bool {
264+
users, err := ParsePasswdFilter(passwd, func(u User) bool {
183265
if userArg == "" {
184-
return u.Uid == uid
266+
return u.Uid == user.Uid
185267
}
186268
return u.Name == userArg || strconv.Itoa(u.Uid) == userArg
187269
})
188-
if err != nil && !os.IsNotExist(err) {
270+
if err != nil && passwd != nil {
189271
if userArg == "" {
190-
userArg = strconv.Itoa(uid)
272+
userArg = strconv.Itoa(user.Uid)
191273
}
192-
return 0, 0, nil, "", fmt.Errorf("Unable to find user %v: %v", userArg, err)
274+
return nil, fmt.Errorf("Unable to find user %v: %v", userArg, err)
193275
}
194276

195277
haveUser := users != nil && len(users) > 0
196278
if haveUser {
197279
// if we found any user entries that matched our filter, let's take the first one as "correct"
198-
uid = users[0].Uid
199-
gid = users[0].Gid
200-
home = users[0].Home
280+
name = users[0].Name
281+
user.Uid = users[0].Uid
282+
user.Gid = users[0].Gid
283+
user.Home = users[0].Home
201284
} else if userArg != "" {
202285
// we asked for a user but didn't find them... let's check to see if we wanted a numeric user
203-
uid, err = strconv.Atoi(userArg)
286+
user.Uid, err = strconv.Atoi(userArg)
204287
if err != nil {
205288
// not numeric - we have to bail
206-
return 0, 0, nil, "", fmt.Errorf("Unable to find user %v", userArg)
289+
return nil, fmt.Errorf("Unable to find user %v", userArg)
207290
}
208-
if uid < minId || uid > maxId {
209-
return 0, 0, nil, "", ErrRange
291+
292+
// Must be inside valid uid range.
293+
if user.Uid < minId || user.Uid > maxId {
294+
return nil, ErrRange
210295
}
211296

212297
// if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit
213298
}
214299

215-
if groupArg != "" || (haveUser && users[0].Name != "") {
216-
groups, err := ParseGroupFilter(func(g *Group) bool {
300+
if groupArg != "" || name != "" {
301+
groups, err := ParseGroupFilter(group, func(g Group) bool {
302+
// Explicit group format takes precedence.
217303
if groupArg != "" {
218304
return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg
219305
}
306+
307+
// Check if user is a member.
220308
for _, u := range g.List {
221-
if u == users[0].Name {
309+
if u == name {
222310
return true
223311
}
224312
}
313+
225314
return false
226315
})
227-
if err != nil && !os.IsNotExist(err) {
228-
return 0, 0, nil, "", fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
316+
if err != nil && group != nil {
317+
return nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err)
229318
}
230319

231320
haveGroup := groups != nil && len(groups) > 0
232321
if groupArg != "" {
233322
if haveGroup {
234323
// if we found any group entries that matched our filter, let's take the first one as "correct"
235-
gid = groups[0].Gid
324+
user.Gid = groups[0].Gid
236325
} else {
237326
// we asked for a group but didn't find id... let's check to see if we wanted a numeric group
238-
gid, err = strconv.Atoi(groupArg)
327+
user.Gid, err = strconv.Atoi(groupArg)
239328
if err != nil {
240329
// not numeric - we have to bail
241-
return 0, 0, nil, "", fmt.Errorf("Unable to find group %v", groupArg)
330+
return nil, fmt.Errorf("Unable to find group %v", groupArg)
242331
}
243-
if gid < minId || gid > maxId {
244-
return 0, 0, nil, "", ErrRange
332+
333+
// Ensure gid is inside gid range.
334+
if user.Gid < minId || user.Gid > maxId {
335+
return nil, ErrRange
245336
}
246337

247338
// if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit
248339
}
249340
} else if haveGroup {
250-
suppGids = make([]int, len(groups))
341+
// If implicit group format, fill supplementary gids.
342+
user.Sgids = make([]int, len(groups))
251343
for i, group := range groups {
252-
suppGids[i] = group.Gid
344+
user.Sgids[i] = group.Gid
253345
}
254346
}
255347
}
256348

257-
return uid, gid, suppGids, home, nil
349+
return user, nil
258350
}

user/user_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func TestUserParseLine(t *testing.T) {
5454
}
5555

5656
func TestUserParsePasswd(t *testing.T) {
57-
users, err := parsePasswdFile(strings.NewReader(`
57+
users, err := ParsePasswdFilter(strings.NewReader(`
5858
root:x:0:0:root:/root:/bin/bash
5959
adm:x:3:4:adm:/var/adm:/bin/false
6060
this is just some garbage data
@@ -74,7 +74,7 @@ this is just some garbage data
7474
}
7575

7676
func TestUserParseGroup(t *testing.T) {
77-
groups, err := parseGroupFile(strings.NewReader(`
77+
groups, err := ParseGroupFilter(strings.NewReader(`
7878
root:x:0:root
7979
adm:x:4:root,adm,daemon
8080
this is just some garbage data

0 commit comments

Comments
 (0)