package user import ( "bufio" "fmt" "io" "os" "strconv" "strings" ) const ( minId = 0 maxId = 1<<31 - 1 //for 32-bit systems compatibility ) var ( ErrRange = fmt.Errorf("Uids and gids must be in range %d-%d", minId, maxId) ) type User struct { Name string Pass string Uid int Gid int Gecos string Home string Shell string } type Group struct { Name string Pass string Gid int List []string } func parseLine(line string, v ...interface{}) { if line == "" { return } parts := strings.Split(line, ":") for i, p := range parts { if len(v) <= i { // if we have more "parts" than we have places to put them, bail for great "tolerance" of naughty configuration files break } switch e := v[i].(type) { case *string: // "root", "adm", "/bin/bash" *e = p case *int: // "0", "4", "1000" // ignore string to int conversion errors, for great "tolerance" of naughty configuration files *e, _ = strconv.Atoi(p) case *[]string: // "", "root", "root,adm,daemon" if p != "" { *e = strings.Split(p, ",") } else { *e = []string{} } default: // panic, because this is a programming/logic error, not a runtime one panic("parseLine expects only pointers! argument " + strconv.Itoa(i) + " is not a pointer!") } } } func ParsePasswd() ([]*User, error) { return ParsePasswdFilter(nil) } func ParsePasswdFilter(filter func(*User) bool) ([]*User, error) { f, err := os.Open("/etc/passwd") if err != nil { return nil, err } defer f.Close() return parsePasswdFile(f, filter) } func parsePasswdFile(r io.Reader, filter func(*User) bool) ([]*User, error) { var ( s = bufio.NewScanner(r) out = []*User{} ) for s.Scan() { if err := s.Err(); err != nil { return nil, err } text := strings.TrimSpace(s.Text()) if text == "" { continue } // see: man 5 passwd // name:password:UID:GID:GECOS:directory:shell // Name:Pass:Uid:Gid:Gecos:Home:Shell // root:x:0:0:root:/root:/bin/bash // adm:x:3:4:adm:/var/adm:/bin/false p := &User{} parseLine( text, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell, ) if filter == nil || filter(p) { out = append(out, p) } } return out, nil } func ParseGroup() ([]*Group, error) { return ParseGroupFilter(nil) } func ParseGroupFilter(filter func(*Group) bool) ([]*Group, error) { f, err := os.Open("/etc/group") if err != nil { return nil, err } defer f.Close() return parseGroupFile(f, filter) } func parseGroupFile(r io.Reader, filter func(*Group) bool) ([]*Group, error) { var ( s = bufio.NewScanner(r) out = []*Group{} ) for s.Scan() { if err := s.Err(); err != nil { return nil, err } text := s.Text() if text == "" { continue } // see: man 5 group // group_name:password:GID:user_list // Name:Pass:Gid:List // root:x:0:root // adm:x:4:root,adm,daemon p := &Group{} parseLine( text, &p.Name, &p.Pass, &p.Gid, &p.List, ) if filter == nil || filter(p) { out = append(out, p) } } return out, nil } // Given a string like "user", "1000", "user:group", "1000:1000", returns the uid, gid, and list of supplementary group IDs, if possible. func GetUserGroupSupplementary(userSpec string, defaultUid int, defaultGid int) (int, int, []int, error) { var ( uid = defaultUid gid = defaultGid suppGids = []int{} userArg, groupArg string ) // allow for userArg to have either "user" syntax, or optionally "user:group" syntax parseLine(userSpec, &userArg, &groupArg) users, err := ParsePasswdFilter(func(u *User) bool { if userArg == "" { return u.Uid == uid } return u.Name == userArg || strconv.Itoa(u.Uid) == userArg }) if err != nil && !os.IsNotExist(err) { if userArg == "" { userArg = strconv.Itoa(uid) } return 0, 0, nil, fmt.Errorf("Unable to find user %v: %v", userArg, err) } haveUser := users != nil && len(users) > 0 if haveUser { // if we found any user entries that matched our filter, let's take the first one as "correct" uid = users[0].Uid gid = users[0].Gid } else if userArg != "" { // we asked for a user but didn't find them... let's check to see if we wanted a numeric user uid, err = strconv.Atoi(userArg) if err != nil { // not numeric - we have to bail return 0, 0, nil, fmt.Errorf("Unable to find user %v", userArg) } if uid < minId || uid > maxId { return 0, 0, nil, ErrRange } // if userArg couldn't be found in /etc/passwd but is numeric, just roll with it - this is legit } if groupArg != "" || (haveUser && users[0].Name != "") { groups, err := ParseGroupFilter(func(g *Group) bool { if groupArg != "" { return g.Name == groupArg || strconv.Itoa(g.Gid) == groupArg } for _, u := range g.List { if u == users[0].Name { return true } } return false }) if err != nil && !os.IsNotExist(err) { return 0, 0, nil, fmt.Errorf("Unable to find groups for user %v: %v", users[0].Name, err) } haveGroup := groups != nil && len(groups) > 0 if groupArg != "" { if haveGroup { // if we found any group entries that matched our filter, let's take the first one as "correct" gid = groups[0].Gid } else { // we asked for a group but didn't find id... let's check to see if we wanted a numeric group gid, err = strconv.Atoi(groupArg) if err != nil { // not numeric - we have to bail return 0, 0, nil, fmt.Errorf("Unable to find group %v", groupArg) } if gid < minId || gid > maxId { return 0, 0, nil, ErrRange } // if groupArg couldn't be found in /etc/group but is numeric, just roll with it - this is legit } } else if haveGroup { suppGids = make([]int, len(groups)) for i, group := range groups { suppGids[i] = group.Gid } } } return uid, gid, suppGids, nil }