github.com/wtsi-ssg/wrstat/v4@v4.5.1/ch/ch.go (about)

     1  /*******************************************************************************
     2   * Copyright (c) 2021 Genome Research Ltd.
     3   *
     4   * Author: Sendu Bala <sb10@sanger.ac.uk>
     5   *
     6   * Permission is hereby granted, free of charge, to any person obtaining
     7   * a copy of this software and associated documentation files (the
     8   * "Software"), to deal in the Software without restriction, including
     9   * without limitation the rights to use, copy, modify, merge, publish,
    10   * distribute, sublicense, and/or sell copies of the Software, and to
    11   * permit persons to whom the Software is furnished to do so, subject to
    12   * the following conditions:
    13   *
    14   * The above copyright notice and this permission notice shall be included
    15   * in all copies or substantial portions of the Software.
    16   *
    17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    18   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    19   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    20   * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    21   * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    22   * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    23   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    24   ******************************************************************************/
    25  
    26  // package ch is used to do chmod and chown on certain files, to correct for
    27  // group and user permissions and ownership being wrong.
    28  
    29  package ch
    30  
    31  import (
    32  	"errors"
    33  	"io/fs"
    34  	"os"
    35  	"os/user"
    36  	"strconv"
    37  	"syscall"
    38  
    39  	"github.com/hashicorp/go-multierror"
    40  	"github.com/inconshreveable/log15"
    41  )
    42  
    43  const (
    44  	modePermUser              = 0700
    45  	modePermGroup             = 0070
    46  	modePermUserToGroupShift  = 3
    47  	modeUserExecutable        = 0100
    48  	modeGroupExecutable       = 0010
    49  	modeUserGroupReadWritable = 0660
    50  )
    51  
    52  // Ch is used to chmod and chown files such that they match their desired group.
    53  type Ch struct {
    54  	rs     *RulesStore
    55  	logger log15.Logger
    56  }
    57  
    58  // New returns a Ch what will use your RulesStore to see what work needs to
    59  // be done on the paths this Ch will receive when Do() is called on it.
    60  //
    61  // Changes made will be logged to the given logger.
    62  func New(rs *RulesStore, logger log15.Logger) *Ch {
    63  	return &Ch{
    64  		rs:     rs,
    65  		logger: logger,
    66  	}
    67  }
    68  
    69  // Do is a github.com/wtsi-ssg/wrstat/stat Operation that passes path to our
    70  // PathCheck callback, and if it returns true, does the following chmod and
    71  // chown-type behaviours, making use of the supplied Lstat info to avoid doing
    72  // unnecessary repeated work:
    73  //
    74  // 1. Ensures that the GID of the path is the returned GID.
    75  // 2. If path is a directory, ensures it has setgid applied (group sticky).
    76  // 3. Ensures that User execute permission is set if group execute was set.
    77  // 4. Ensures that group permissions match user permissions.
    78  // 5. Forces user and group read and writeability.
    79  //
    80  // Any errors are returned without logging them, except for "not exists" errors
    81  // which are silently ignored since these are expected.
    82  //
    83  // Any changes we do on disk are logged to our logger.
    84  func (c *Ch) Do(path string, info fs.FileInfo) error {
    85  	rule := c.rs.Get(path)
    86  	if rule == nil {
    87  		return nil
    88  	}
    89  
    90  	chain := &chain{}
    91  
    92  	chain.Call(func() error {
    93  		return c.chown(rule, path, info)
    94  	})
    95  
    96  	chain.Call(func() error {
    97  		return c.chmod(rule, path, info)
    98  	})
    99  
   100  	return chain.merr
   101  }
   102  
   103  // chain lets you call a chain of functions and combine their errors.
   104  type chain struct {
   105  	merr error
   106  	stop bool
   107  }
   108  
   109  // Call will run your function and append any error to our merr, except for
   110  // os.ErrNotExist, which instead result in future Call()s to no-op.
   111  func (c *chain) Call(f func() error) {
   112  	if c.stop {
   113  		return
   114  	}
   115  
   116  	if err := f(); err != nil {
   117  		if errors.Is(err, os.ErrNotExist) {
   118  			c.stop = true
   119  
   120  			return
   121  		}
   122  
   123  		c.merr = multierror.Append(c.merr, err)
   124  	}
   125  }
   126  
   127  func (c *Ch) chown(rule *Rule, path string, info fs.FileInfo) error {
   128  	currentUID, currentGID := getIDsFromFileInfo(info)
   129  	desiredUID := rule.DesiredUser(currentUID)
   130  	desiredGID := rule.DesiredGroup(currentGID)
   131  
   132  	if currentUID == desiredUID && currentGID == desiredGID {
   133  		return nil
   134  	}
   135  
   136  	if err := os.Lchown(path, int(desiredUID), int(desiredGID)); err != nil {
   137  		return err
   138  	}
   139  
   140  	return c.logChown(path, currentUID, desiredUID, currentGID, desiredGID)
   141  }
   142  
   143  func (c *Ch) logChown(path string, currentUID, desiredUID, currentGID, desiredGID uint32) error {
   144  	origUName, err := userName(int(currentUID))
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	newUName, err := userName(int(desiredUID))
   150  	if err != nil {
   151  		return err
   152  	}
   153  
   154  	origGName, err := groupName(int(currentGID))
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	newGName, err := groupName(int(desiredGID))
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	c.logger.Info("changed ownership", "path", path,
   165  		"origUser", origUName, "newUser", newUName,
   166  		"origGroup", origGName, "newGroup", newGName)
   167  
   168  	return nil
   169  }
   170  
   171  // getIDsFromFileInfo extracts the UID and GID from a FileInfo. NB: this will
   172  // only work on linux.
   173  func getIDsFromFileInfo(info fs.FileInfo) (uint32, uint32) {
   174  	stat, ok := info.Sys().(*syscall.Stat_t)
   175  	if !ok {
   176  		return 0, 0
   177  	}
   178  
   179  	return stat.Uid, stat.Gid
   180  }
   181  
   182  // userName returns the username of the user with the given UID.
   183  func userName(uid int) (string, error) {
   184  	u, err := user.LookupId(strconv.Itoa(uid))
   185  	if err != nil {
   186  		return "", err
   187  	}
   188  
   189  	return u.Username, err
   190  }
   191  
   192  // groupName returns the name of the group with the given GID.
   193  func groupName(gid int) (string, error) {
   194  	g, err := user.LookupGroupId(strconv.Itoa(gid))
   195  	if err != nil {
   196  		return "", err
   197  	}
   198  
   199  	return g.Name, err
   200  }
   201  
   202  func (c *Ch) chmod(rule *Rule, path string, info fs.FileInfo) error {
   203  	currentPerms := info.Mode()
   204  
   205  	var desiredPerms fs.FileMode
   206  
   207  	if info.IsDir() {
   208  		desiredPerms = rule.DesiredDirPerms(currentPerms)
   209  	} else {
   210  		desiredPerms = rule.DesiredFilePerms(currentPerms)
   211  	}
   212  
   213  	if currentPerms == desiredPerms {
   214  		return nil
   215  	}
   216  
   217  	if err := chmod(info, path, desiredPerms); err != nil {
   218  		return err
   219  	}
   220  
   221  	c.logger.Info("set permissions", "path", path, "old", currentPerms, "new", desiredPerms)
   222  
   223  	return nil
   224  }
   225  
   226  // chmod is like os.Chmod, but checks the given info to do nothing if this is a
   227  // symlink.
   228  func chmod(info fs.FileInfo, path string, mode fs.FileMode) error {
   229  	if info.Mode()&os.ModeSymlink == os.ModeSymlink {
   230  		return nil
   231  	}
   232  
   233  	return os.Chmod(path, mode)
   234  }