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  }