github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/osutil/group.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017-2019 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package osutil 21 22 import ( 23 "bytes" 24 "fmt" 25 "os/exec" 26 "os/user" 27 "strconv" 28 ) 29 30 // FindUid returns the identifier of the given UNIX user name. It will 31 // automatically fallback to use "getent" if needed. 32 var FindUid = findUid 33 34 // FindGid returns the identifier of the given UNIX group name. It will 35 // automatically fallback to use "getent" if needed. 36 var FindGid = findGid 37 38 // getent returns the identifier of the given UNIX user or group name as 39 // determined by the specified database 40 func getent(database, name string) (uint64, error) { 41 if database != "passwd" && database != "group" { 42 return 0, fmt.Errorf(`unsupported getent database "%q"`, database) 43 } 44 45 cmdStr := []string{ 46 "getent", 47 database, 48 name, 49 } 50 cmd := exec.Command(cmdStr[0], cmdStr[1:]...) 51 output, err := cmd.CombinedOutput() 52 if err != nil { 53 // according to getent(1) the exit value of "2" means: 54 // "One or more supplied key could not be found in the 55 // database." 56 exitCode, _ := ExitCode(err) 57 if exitCode == 2 { 58 if database == "passwd" { 59 return 0, user.UnknownUserError(name) 60 } 61 return 0, user.UnknownGroupError(name) 62 } 63 return 0, fmt.Errorf("getent failed with: %v", OutputErr(output, err)) 64 } 65 66 // passwd has 7 entries and group 4. In both cases, parts[2] is the id 67 parts := bytes.Split(output, []byte(":")) 68 if len(parts) < 4 { 69 return 0, fmt.Errorf("malformed entry: %q", output) 70 } 71 72 return strconv.ParseUint(string(parts[2]), 10, 64) 73 } 74 75 // TODO: both findUidNoGetentFallback and findGidNoGetentFallback should return 76 // a more qualified default value than uint64, because currently the 77 // default return value for findUid is "0" as per Go conventions, which is 78 // unfortunately also the uid of root, so if a caller ignored the error 79 // from this function and used that to perform access authorization, then 80 // the caller would accidentally provide the same access level as root in 81 // the error case. This is excaberated by the fact that the error case is 82 // very difficult to positively identify correctly as "not found", see the 83 // comments inside the functions for more details. 84 // Note: there is a similar implementation in overlord/snapshotstate which 85 // should be similarly adjusted when resolving the above TODO. 86 87 var findUidNoGetentFallback = func(username string) (uint64, error) { 88 myuser, err := user.Lookup(username) 89 if err != nil { 90 // Treat all non-nil errors as user.Unknown{User,Group}Error's, as 91 // currently Go's handling of returned errno from get{pw,gr}nam_r 92 // in the cgo implementation of user.Lookup is lacking, and thus 93 // user.Unknown{User,Group}Error is returned only when errno is 0 94 // and the list of users/groups is empty, but as per the man page 95 // for get{pw,gr}nam_r, there are many other errno's that typical 96 // systems could return to indicate that the user/group wasn't 97 // found, however unfortunately the POSIX standard does not actually 98 // dictate what errno should be used to indicate "user/group not 99 // found", and so even if Go is more robust, it may not ever be 100 // fully robust. See from the man page: 101 // 102 // > It [POSIX.1-2001] does not call "not found" an error, hence 103 // > does not specify what value errno might have in this situation. 104 // > But that makes it impossible to recognize errors. 105 // 106 // See upstream Go issue: https://github.com/golang/go/issues/40334 107 108 // if there is a real problem finding the user/group then presumably 109 // other things will fail upon trying to create the user, etc. which 110 // will give more useful and specific errors 111 return 0, user.UnknownUserError(username) 112 } 113 114 return strconv.ParseUint(myuser.Uid, 10, 64) 115 } 116 117 var findGidNoGetentFallback = func(groupname string) (uint64, error) { 118 group, err := user.LookupGroup(groupname) 119 if err != nil { 120 // Treat all non-nil errors as user.Unknown{User,Group}Error's, as 121 // currently Go's handling of returned errno from get{pw,gr}nam_r 122 // in the cgo implementation of user.Lookup is lacking, and thus 123 // user.Unknown{User,Group}Error is returned only when errno is 0 124 // and the list of users/groups is empty, but as per the man page 125 // for get{pw,gr}nam_r, there are many other errno's that typical 126 // systems could return to indicate that the user/group wasn't 127 // found, however unfortunately the POSIX standard does not actually 128 // dictate what errno should be used to indicate "user/group not 129 // found", and so even if Go is more robust, it may not ever be 130 // fully robust. See from the man page: 131 // 132 // > It [POSIX.1-2001] does not call "not found" an error, hence 133 // > does not specify what value errno might have in this situation. 134 // > But that makes it impossible to recognize errors. 135 // 136 // See upstream Go issue: https://github.com/golang/go/issues/40334 137 138 // if there is a real problem finding the user/group then presumably 139 // other things will fail upon trying to create the user, etc. which 140 // will give more useful and specific errors 141 return 0, user.UnknownGroupError(groupname) 142 } 143 144 return strconv.ParseUint(group.Gid, 10, 64) 145 } 146 147 // findUidWithGetentFallback returns the identifier of the given UNIX user name with 148 // getent fallback 149 func findUidWithGetentFallback(username string) (uint64, error) { 150 // first do the cheap os/user lookup 151 myuser, err := findUidNoGetentFallback(username) 152 switch err.(type) { 153 case nil: 154 // found it! 155 return myuser, nil 156 case user.UnknownUserError: 157 // user unknown, let's try getent 158 return getent("passwd", username) 159 default: 160 // something weird happened with the lookup, just report it 161 return 0, err 162 } 163 } 164 165 // findGidWithGetentFallback returns the identifier of the given UNIX group name with 166 // getent fallback 167 func findGidWithGetentFallback(groupname string) (uint64, error) { 168 // first do the cheap os/user lookup 169 group, err := findGidNoGetentFallback(groupname) 170 switch err.(type) { 171 case nil: 172 // found it! 173 return group, nil 174 case user.UnknownGroupError: 175 // group unknown, let's try getent 176 return getent("group", groupname) 177 default: 178 // something weird happened with the lookup, just report it 179 return 0, err 180 } 181 } 182 183 func IsUnknownUser(err error) bool { 184 _, ok := err.(user.UnknownUserError) 185 return ok 186 } 187 188 func IsUnknownGroup(err error) bool { 189 _, ok := err.(user.UnknownGroupError) 190 return ok 191 }