github.com/hernad/nomad@v1.6.112/helper/users/lookup.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package users
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"os"
    10  	"os/user"
    11  	"strconv"
    12  	"sync"
    13  
    14  	"github.com/hashicorp/go-hclog"
    15  	"github.com/hashicorp/go-multierror"
    16  )
    17  
    18  var globalCache = newCache()
    19  
    20  // Lookup returns the user.User entry associated with the given username.
    21  //
    22  // Values are cached up to 1 hour, or 1 minute for failure cases.
    23  func Lookup(username string) (*user.User, error) {
    24  	return globalCache.GetUser(username)
    25  }
    26  
    27  // lock is used to serialize all user lookup at the process level, because
    28  // some NSS implementations are not concurrency safe
    29  var lock sync.Mutex
    30  
    31  // internalLookupUser username while holding a global process lock.
    32  func internalLookupUser(username string) (*user.User, error) {
    33  	lock.Lock()
    34  	defer lock.Unlock()
    35  	return user.Lookup(username)
    36  }
    37  
    38  // Current returns the current user, acquired while holding a global process
    39  // lock.
    40  func Current() (*user.User, error) {
    41  	lock.Lock()
    42  	defer lock.Unlock()
    43  	return user.Current()
    44  }
    45  
    46  // UIDforUser returns the UID for the specified username or returns an error.
    47  //
    48  // Will always fail on Windows and Plan 9.
    49  func UIDforUser(username string) (int, error) {
    50  	u, err := Lookup(username)
    51  	if err != nil {
    52  		return 0, err
    53  	}
    54  
    55  	uid, err := strconv.Atoi(u.Uid)
    56  	if err != nil {
    57  		return 0, fmt.Errorf("error parsing uid: %w", err)
    58  	}
    59  
    60  	return uid, nil
    61  }
    62  
    63  // WriteFileFor is like os.WriteFile except if possible it chowns the file to
    64  // the specified user (possibly from Task.User) and sets the permissions to
    65  // 0o600.
    66  //
    67  // If chowning fails (either due to OS or Nomad being unprivileged), the file
    68  // will be left world readable (0o666).
    69  //
    70  // On failure a multierror with both the original and fallback errors will be
    71  // returned.
    72  //
    73  // See SocketFileFor if writing a unix socket file.
    74  func WriteFileFor(path string, contents []byte, username string) error {
    75  	// Don't even bother trying to chown to an empty username
    76  	var origErr error
    77  	if username != "" {
    78  		origErr := writeFileFor(path, contents, username)
    79  		if origErr == nil {
    80  			// Success!
    81  			return nil
    82  		}
    83  	}
    84  
    85  	// Fallback to world readable
    86  	if err := os.WriteFile(path, contents, 0o666); err != nil {
    87  		if origErr != nil {
    88  			// Return both errors
    89  			return &multierror.Error{
    90  				Errors: []error{origErr, err},
    91  			}
    92  		} else {
    93  			return err
    94  		}
    95  	}
    96  
    97  	return nil
    98  }
    99  
   100  func writeFileFor(path string, contents []byte, username string) error {
   101  	uid, err := UIDforUser(username)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	if err := os.WriteFile(path, contents, 0o600); err != nil {
   107  		return err
   108  	}
   109  
   110  	if err := os.Chown(path, uid, -1); err != nil {
   111  		// Delete the file so that the fallback method properly resets
   112  		// permissions.
   113  		_ = os.Remove(path)
   114  		return err
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  // SocketFileFor creates a unix domain socket file on the specified path and,
   121  // if possible, makes it usable by only the specified user. Failing that it
   122  // will leave the socket open to all users. Non-fatal errors are logged.
   123  //
   124  // See WriteFileFor if writing a regular file.
   125  func SocketFileFor(logger hclog.Logger, path, username string) (net.Listener, error) {
   126  	if err := os.RemoveAll(path); err != nil {
   127  		logger.Warn("error removing socket", "path", path, "error", err)
   128  	}
   129  
   130  	udsln, err := net.Listen("unix", path)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	if username != "" {
   136  		// Try to set perms on socket file to least privileges.
   137  		if err := setSocketOwner(path, username); err == nil {
   138  			// Success! Exit early
   139  			return udsln, nil
   140  		}
   141  
   142  		// This error is expected to always occur in some environments (Windows,
   143  		// non-root agents), so don't log above Trace.
   144  		logger.Trace("failed to set user on socket", "path", path, "user", username, "error", err)
   145  	}
   146  
   147  	// Opportunistic least privileges failed above, so make sure anyone can use
   148  	// the socket.
   149  	if err := os.Chmod(path, 0o666); err != nil {
   150  		logger.Warn("error setting socket permissions", "path", path, "error", err)
   151  	}
   152  
   153  	return udsln, nil
   154  }
   155  
   156  func setSocketOwner(path, username string) error {
   157  	uid, err := UIDforUser(username)
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	if err := os.Chown(path, uid, -1); err != nil {
   163  		return err
   164  	}
   165  
   166  	if err := os.Chmod(path, 0o600); err != nil {
   167  		// Awkward situation that is hopefully impossible to reach where we could
   168  		// chown the socket but not change its mode.
   169  		return err
   170  	}
   171  
   172  	return nil
   173  }