github.com/wtsi-ssg/wrstat@v1.1.4-0.20221008232152-3030622a8cf8/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  // PathChecker is a callback used by Ch that will receive the absolute path to a
    53  // file or directory and should return a boolean if this path is eligible for
    54  // changing, and the desired group ID of this path.
    55  type PathChecker func(path string) (change bool, gid int)
    56  
    57  // Ch is used to chmod and chown files such that they match their desired group.
    58  type Ch struct {
    59  	pc     PathChecker
    60  	logger log15.Logger
    61  }
    62  
    63  // New returns a Ch what will check your pc callback to see what work needs to
    64  // be done on the paths this Ch will receive when Do() is called on it.
    65  //
    66  // Changes made will be logged to the given logger.
    67  func New(pc PathChecker, logger log15.Logger) *Ch {
    68  	return &Ch{
    69  		pc:     pc,
    70  		logger: logger,
    71  	}
    72  }
    73  
    74  // Do is a github.com/wtsi-ssg/wrstat/stat Operation that passes path to our
    75  // PathCheck callback, and if it returns true, does the following chmod and
    76  // chown-type behaviours, making use of the supplied Lstat info to avoid doing
    77  // unnecessary repeated work:
    78  //
    79  // 1. Ensures that the GID of the path is the returned GID.
    80  // 2. If path is a directory, ensures it has setgid applied (group sticky).
    81  // 3. Ensures that User execute permission is set if group execute was set.
    82  // 4. Ensures that group permissions match user permissions.
    83  // 5. Forces user and group read and writeability.
    84  //
    85  // Any errors are returned without logging them, except for "not exists" errors
    86  // which are silently ignored since these are expected.
    87  //
    88  // Any changes we do on disk are logged to our logger.
    89  func (c *Ch) Do(path string, info fs.FileInfo) error {
    90  	change, gid := c.pc(path)
    91  	if !change {
    92  		return nil
    93  	}
    94  
    95  	chain := &chain{}
    96  
    97  	chain.Call(func() error {
    98  		return c.chownGroup(path, getGIDFromFileInfo(info), gid)
    99  	})
   100  
   101  	chain.Call(func() error {
   102  		return c.setgid(path, info)
   103  	})
   104  
   105  	chain.Call(func() error {
   106  		return c.matchPermissionsAndMakeUGRW(path, info)
   107  	})
   108  
   109  	return chain.merr
   110  }
   111  
   112  // chain lets you call a chain of functions and combine their errors.
   113  type chain struct {
   114  	merr error
   115  	stop bool
   116  }
   117  
   118  // Call will run your function and append any error to our merr, except for
   119  // os.ErrNotExist, which instead result in future Call()s to no-op.
   120  func (c *chain) Call(f func() error) {
   121  	if c.stop {
   122  		return
   123  	}
   124  
   125  	if err := f(); err != nil {
   126  		if errors.Is(err, os.ErrNotExist) {
   127  			c.stop = true
   128  
   129  			return
   130  		}
   131  
   132  		c.merr = multierror.Append(c.merr, err)
   133  	}
   134  }
   135  
   136  // getGIDFromFileInfo extracts the GID from a FileInfo. NB: this will only work
   137  // on linux.
   138  func getGIDFromFileInfo(info fs.FileInfo) int {
   139  	return int(info.Sys().(*syscall.Stat_t).Gid) //nolint:forcetypeassert
   140  }
   141  
   142  // chownGroup chown's path to have newGID as its group owner, if newGID is
   143  // different to origGID. If a change is made, logs it.
   144  func (c *Ch) chownGroup(path string, origGID, newGID int) error {
   145  	if origGID == newGID {
   146  		return nil
   147  	}
   148  
   149  	if err := os.Lchown(path, -1, newGID); err != nil {
   150  		return err
   151  	}
   152  
   153  	origName, err := groupName(origGID)
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	newName, err := groupName(newGID)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	c.logger.Info("changed group", "path", path, "orig", origName, "new", newName)
   164  
   165  	return nil
   166  }
   167  
   168  // groupName returns the name of the group with the given GID.
   169  func groupName(gid int) (string, error) {
   170  	g, err := user.LookupGroupId(strconv.Itoa(gid))
   171  	if err != nil {
   172  		return "", err
   173  	}
   174  
   175  	return g.Name, err
   176  }
   177  
   178  // setgid sets group sticky bit on path if path is a dir and didn't already have
   179  // group sticky bit set. If a change is made, logs it.
   180  func (c *Ch) setgid(path string, info fs.FileInfo) error {
   181  	if !info.IsDir() || setgidApplied(info) {
   182  		return nil
   183  	}
   184  
   185  	err := chmod(info, path, info.Mode()|os.ModeSetgid)
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	c.logger.Info("applied setgid", "path", path)
   191  
   192  	return nil
   193  }
   194  
   195  // setgidApplied reports if the setgid bits are set on the given FileInfo.
   196  func setgidApplied(info fs.FileInfo) bool {
   197  	return (info.Mode() & os.ModeSetgid) != 0
   198  }
   199  
   200  // chmod is like os.Chmod, but checks the given info to do nothing if this is a
   201  // symlink.
   202  func chmod(info fs.FileInfo, path string, mode fs.FileMode) error {
   203  	if info.Mode()&os.ModeSymlink == os.ModeSymlink {
   204  		return nil
   205  	}
   206  
   207  	return os.Chmod(path, mode)
   208  }
   209  
   210  // matchPermissionsAndMakeUGRW:
   211  //
   212  // 1) Sets u+x if g+x.
   213  // 2) Sets group permissions to match user permissions if they're different.
   214  // 3) Sets ug+rx if not already.
   215  //
   216  // If any changes are made, logs them.
   217  func (c *Ch) matchPermissionsAndMakeUGRW(path string, info fs.FileInfo) error {
   218  	mode, err := c.copyGroupXToUser(path, info)
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	mode, err = c.matchPermissions(path, info, mode)
   224  	if err != nil {
   225  		return err
   226  	}
   227  
   228  	return c.makeUGRW(path, info, mode)
   229  }
   230  
   231  // copyGroupXToUser makes the file user executable if it is group executable. If
   232  // a change is made, logs it and returns the new mode.
   233  func (c *Ch) copyGroupXToUser(path string, info fs.FileInfo) (fs.FileMode, error) {
   234  	mode := info.Mode()
   235  
   236  	if !(mode&modeGroupExecutable != 0 && mode&modeUserExecutable == 0) {
   237  		return mode, nil
   238  	}
   239  
   240  	err := chmod(info, path, mode^modeUserExecutable)
   241  	if err != nil {
   242  		return mode, err
   243  	}
   244  
   245  	c.logger.Info("set user x to match group", "path", path)
   246  
   247  	return mode ^ modeUserExecutable, nil
   248  }
   249  
   250  // matchPermissions sets group permissions to match user permissions if they're
   251  // different. If a change is made, logs it and returns the new mode.
   252  func (c *Ch) matchPermissions(path string, info fs.FileInfo, mode fs.FileMode) (fs.FileMode, error) {
   253  	userAsGroupPerms := extractUserAsGroupPermissions(mode)
   254  
   255  	if userAsGroupPerms == extractGroupPermissions(mode) {
   256  		return mode, nil
   257  	}
   258  
   259  	err := chmod(info, path, mode|userAsGroupPerms)
   260  	if err != nil {
   261  		return mode, err
   262  	}
   263  
   264  	c.logger.Info("matched group permissions to user", "path", path, "old", mode, "new", mode|userAsGroupPerms)
   265  
   266  	return mode | userAsGroupPerms, nil
   267  }
   268  
   269  // extractUserAsGroupPermissions returns the user permission bits of the given
   270  // mode, shifted as if they were group permissions. If there were no user
   271  // permissions, treated as full permissions.
   272  func extractUserAsGroupPermissions(mode fs.FileMode) fs.FileMode {
   273  	user := mode & modePermUser
   274  	if user == 0 {
   275  		user = modePermUser
   276  	}
   277  
   278  	return user >> modePermUserToGroupShift
   279  }
   280  
   281  // extractGroupPermissions returns the user permission bits of the given mode.
   282  func extractGroupPermissions(mode fs.FileMode) fs.FileMode {
   283  	return mode & modePermGroup
   284  }
   285  
   286  // makeUGRW forces ug+rw on the file. If a change is made, logs it.
   287  func (c *Ch) makeUGRW(path string, info fs.FileInfo, mode fs.FileMode) error {
   288  	if !(mode&modeUserGroupReadWritable != modeUserGroupReadWritable) {
   289  		return nil
   290  	}
   291  
   292  	if err := chmod(info, path, mode|modeUserGroupReadWritable); err != nil {
   293  		return err
   294  	}
   295  
   296  	c.logger.Info("forced ug+rw", "path", path, "old", mode)
   297  
   298  	return nil
   299  }