github.com/wtsi-ssg/wrstat@v1.1.4-0.20221008232152-3030622a8cf8/ch/ch_test.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
    27  
    28  import (
    29  	"bytes"
    30  	"fmt"
    31  	"io/fs"
    32  	"os"
    33  	"os/user"
    34  	"path/filepath"
    35  	"strconv"
    36  	"syscall"
    37  	"testing"
    38  	"time"
    39  
    40  	"github.com/inconshreveable/log15"
    41  	. "github.com/smartystreets/goconvey/convey"
    42  )
    43  
    44  const longBasename = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
    45  	"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
    46  	"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
    47  	"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    48  
    49  func TestCh(t *testing.T) {
    50  	primaryGID, otherGIDs := getGIDs(t)
    51  
    52  	if len(otherGIDs) == 0 {
    53  		SkipConvey("Can't test Ch since you don't belong to multiple groups", t, func() {})
    54  
    55  		return
    56  	}
    57  
    58  	otherGID := otherGIDs[0]
    59  	unchangedGIDs := []int{primaryGID, otherGID, primaryGID, primaryGID}
    60  	primaryName := testGroupName(t, primaryGID)
    61  	otherName := testGroupName(t, otherGID)
    62  	invalidPath := "/foo/bar"
    63  
    64  	Convey("groupName seems to do something reasonable", t, func() {
    65  		name, err := groupName(primaryGID)
    66  		So(err, ShouldBeNil)
    67  		So(name, ShouldNotBeBlank)
    68  
    69  		name, err = groupName(-1)
    70  		So(err, ShouldNotBeNil)
    71  		So(name, ShouldBeBlank)
    72  	})
    73  
    74  	Convey("extractUserAsGroupPermissions works when there are no user permissions", t, func() {
    75  		mode := extractUserAsGroupPermissions(0040)
    76  		So(mode, ShouldEqual, 0070)
    77  	})
    78  
    79  	Convey("Given a Ch", t, func() {
    80  		buff, l := newLogger()
    81  		cbChange := false
    82  		cbGID := otherGID
    83  		cb := func(string) (bool, int) {
    84  			return cbChange, cbGID
    85  		}
    86  		ch := New(cb, l)
    87  		So(ch, ShouldNotBeNil)
    88  
    89  		paths, infos := createTestFiles(t, primaryGID, otherGID)
    90  
    91  		Convey("Do does nothing if the cb returns false", func() {
    92  			for i, path := range paths {
    93  				err := ch.Do(path, infos[i])
    94  				So(err, ShouldBeNil)
    95  			}
    96  
    97  			gids := getPathGIDs(t, paths)
    98  			So(gids, ShouldResemble, unchangedGIDs)
    99  			So(buff.String(), ShouldBeBlank)
   100  
   101  			So(testSetgidApplied(t, paths[2]), ShouldBeTrue)
   102  			So(testSetgidApplied(t, paths[3]), ShouldBeFalse)
   103  
   104  			So(is660(t, paths[0]), ShouldBeTrue)
   105  			So(is660(t, paths[1]), ShouldBeFalse)
   106  		})
   107  
   108  		Convey("Do makes the desired changes if cb returns true", func() {
   109  			cbChange = true
   110  			for i, path := range paths {
   111  				err := ch.Do(path, infos[i])
   112  				So(err, ShouldBeNil)
   113  			}
   114  
   115  			gids := getPathGIDs(t, paths)
   116  			So(gids, ShouldResemble, []int{otherGID, otherGID, otherGID, otherGID})
   117  			So(buff.String(), ShouldContainSubstring, `lvl=info msg="changed group" path=`+paths[0])
   118  			So(buff.String(), ShouldContainSubstring, fmt.Sprintf("orig=%s new=%s", primaryName, otherName))
   119  
   120  			So(testSetgidApplied(t, paths[2]), ShouldBeTrue)
   121  			So(testSetgidApplied(t, paths[3]), ShouldBeTrue)
   122  			So(buff.String(), ShouldContainSubstring, `lvl=info msg="applied setgid" path=`+paths[3])
   123  			So(buff.String(), ShouldNotContainSubstring, `lvl=info msg="applied setgid" path=`+paths[2])
   124  
   125  			So(is660(t, paths[0]), ShouldBeTrue)
   126  			So(is660(t, paths[1]), ShouldBeTrue)
   127  			So(buff.String(), ShouldContainSubstring, `lvl=info msg="matched group permissions to user" path=`+paths[1])
   128  			So(buff.String(), ShouldNotContainSubstring, `lvl=info msg="matched group permissions to user" path=`+paths[0])
   129  		})
   130  
   131  		Convey("Do corrects -rw-rwxr-x to -rwxrwxr-x", func() {
   132  			cbChange = true
   133  			perm := createAndDoTestFile(t, otherGID, 0675, ch)
   134  
   135  			So(perm, ShouldEqual, "-rwxrwxr-x")
   136  			So(buff.String(), ShouldContainSubstring, `lvl=info msg="set user x to match group" path=`)
   137  		})
   138  
   139  		Convey("Do corrects -rwxrw-r-x to -rwxrwxr-x", func() {
   140  			cbChange = true
   141  			perm := createAndDoTestFile(t, otherGID, 0765, ch)
   142  
   143  			So(perm, ShouldEqual, "-rwxrwxr-x")
   144  			So(buff.String(), ShouldContainSubstring, `lvl=info msg="matched group permissions to user" path=`)
   145  		})
   146  
   147  		Convey("Do forces non-rw to ug+rw", func() {
   148  			cbChange = true
   149  
   150  			perm := createAndDoTestFile(t, otherGID, 0440, ch)
   151  			So(perm, ShouldEqual, "-rw-rw----")
   152  			So(buff.String(), ShouldContainSubstring, `lvl=info msg="forced ug+rw" path=`)
   153  
   154  			perm = createAndDoTestFile(t, otherGID, 0220, ch)
   155  			So(perm, ShouldEqual, "-rw-rw----")
   156  
   157  			perm = createAndDoTestFile(t, otherGID, 0235, ch)
   158  			So(perm, ShouldEqual, "-rwxrwxr-x")
   159  		})
   160  
   161  		Convey("Do on a non-existent path does nothing", func() {
   162  			cbChange = true
   163  			err := ch.Do(invalidPath, infos[2])
   164  			So(err, ShouldBeNil)
   165  
   166  			cbGID = primaryGID
   167  			err = ch.Do(invalidPath, infos[3])
   168  			So(err, ShouldBeNil)
   169  			cbGID = otherGID
   170  
   171  			err = ch.Do(invalidPath, infos[1])
   172  			So(err, ShouldBeNil)
   173  
   174  			So(buff.String(), ShouldBeBlank)
   175  		})
   176  
   177  		Convey("Do on a bad path returns a set of errors", func() {
   178  			badPath := createBadPath(t)
   179  
   180  			cbChange = true
   181  			cbGID = -2
   182  			err := ch.Do(badPath, &badInfo{isDir: false})
   183  			So(err, ShouldNotBeNil)
   184  			So(err.Error(), ShouldContainSubstring, "1 error occurred")
   185  
   186  			err = ch.Do(badPath, &badInfo{isDir: true})
   187  			So(err, ShouldNotBeNil)
   188  			So(err.Error(), ShouldContainSubstring, "2 errors occurred")
   189  
   190  			err = ch.Do(badPath, &badInfo{isDir: false, perm: 9999})
   191  			So(err, ShouldNotBeNil)
   192  			So(err.Error(), ShouldContainSubstring, "2 errors occurred")
   193  
   194  			cbGID = 0
   195  			err = ch.Do(badPath, &badInfo{isDir: false, perm: 9999})
   196  			So(err, ShouldNotBeNil)
   197  			So(err.Error(), ShouldContainSubstring, "1 error occurred")
   198  
   199  			err = ch.Do(badPath, &badInfo{isDir: false, perm: 0444})
   200  			So(err, ShouldNotBeNil)
   201  			So(err.Error(), ShouldContainSubstring, "1 error occurred")
   202  		})
   203  
   204  		Convey("chownGroup returns an error with invalid paths or GIDs", func() {
   205  			err := ch.chownGroup(invalidPath, primaryGID, otherGID)
   206  			So(err, ShouldNotBeNil)
   207  
   208  			err = ch.chownGroup(paths[0], -1, otherGID)
   209  			So(err, ShouldNotBeNil)
   210  
   211  			err = ch.chownGroup(paths[0], primaryGID, -1)
   212  			So(err, ShouldNotBeNil)
   213  		})
   214  
   215  		Convey("chownGroup applies to symlinks themselves, not their targets", func() {
   216  			dir := t.TempDir()
   217  			path := filepath.Join(dir, "a")
   218  			slink := filepath.Join(dir, "b")
   219  
   220  			createTestFile(t, path, primaryGID, 0660)
   221  			err := os.Symlink(path, slink)
   222  			So(err, ShouldBeNil)
   223  
   224  			err = ch.chownGroup(slink, primaryGID, otherGID)
   225  			So(err, ShouldBeNil)
   226  
   227  			info, err := os.Lstat(slink)
   228  			So(err, ShouldBeNil)
   229  			So(getGIDFromFileInfo(info), ShouldEqual, otherGID)
   230  
   231  			Convey("chmod ignores symlinks but works on real files", func() {
   232  				err = chmod(info, slink, 0670)
   233  				So(err, ShouldBeNil)
   234  
   235  				info, err = os.Lstat(path)
   236  				So(info.Mode().Perm(), ShouldEqual, fs.FileMode(0660))
   237  
   238  				err = chmod(info, path, 0670)
   239  				So(err, ShouldBeNil)
   240  
   241  				info, err = os.Lstat(path)
   242  				So(info.Mode().Perm(), ShouldEqual, fs.FileMode(0670))
   243  			})
   244  		})
   245  	})
   246  }
   247  
   248  // getGIDs finds our primary GID and other GIDs of groups we belong to, so that
   249  // we can test changing groups.
   250  func getGIDs(t *testing.T) (int, []int) {
   251  	t.Helper()
   252  
   253  	primaryGID := os.Getgid()
   254  
   255  	return primaryGID, getOtherGIDs(t, primaryGID)
   256  }
   257  
   258  // getOtherGIDs get's the current users's GroupIDs and returns those that
   259  // aren't the same as the given GID.
   260  func getOtherGIDs(t *testing.T, primaryGID int) []int {
   261  	t.Helper()
   262  
   263  	u, err := user.Current()
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  
   268  	ugids, err := u.GroupIds()
   269  	if err != nil {
   270  		t.Fatal(err)
   271  	}
   272  
   273  	var gids []int
   274  
   275  	for _, gid := range ugids {
   276  		gid, err := strconv.ParseInt(gid, 10, 32)
   277  		if err != nil {
   278  			t.Fatal(err)
   279  		}
   280  
   281  		if int(gid) != primaryGID {
   282  			gids = append(gids, int(gid))
   283  		}
   284  	}
   285  
   286  	return gids
   287  }
   288  
   289  // testGroupName is a convienience function that calls groupName and Fatals on
   290  // error.
   291  func testGroupName(t *testing.T, gid int) string {
   292  	t.Helper()
   293  
   294  	name, err := groupName(gid)
   295  	if err != nil {
   296  		t.Fatal(err)
   297  	}
   298  
   299  	return name
   300  }
   301  
   302  // createTestFiles creates some files in a temp dir and returns their paths and
   303  // stats. The first belongs to primaryGID and has permissions 0660, the second
   304  // belongs to otherGID and has permissions 0600, the 3rd is a directory that has
   305  // the group sticky bit set, and the 4th is one that doesn't.
   306  func createTestFiles(t *testing.T, primaryGID, otherGID int) ([]string, []fs.FileInfo) {
   307  	t.Helper()
   308  	dir := t.TempDir()
   309  	p1 := filepath.Join(dir, "a")
   310  	p2 := filepath.Join(dir, "b")
   311  	p3 := filepath.Join(dir, "c")
   312  	p4 := filepath.Join(dir, "d")
   313  
   314  	i1 := createTestFile(t, p1, primaryGID, 0660)
   315  	i2 := createTestFile(t, p2, otherGID, 0600)
   316  	i3 := createTestDir(t, p3, true)
   317  	i4 := createTestDir(t, p4, false)
   318  
   319  	return []string{p1, p2, p3, p4}, []fs.FileInfo{i1, i2, i3, i4}
   320  }
   321  
   322  // createTestFile creates the given empty file and sets its group to to the
   323  // given GID and applies the given perms. Returns stat of the file created.
   324  // Fatal on error.
   325  func createTestFile(t *testing.T, path string, gid int, perms fs.FileMode) fs.FileInfo {
   326  	t.Helper()
   327  
   328  	f, err := os.Create(path)
   329  	if err != nil {
   330  		t.Fatal(err)
   331  	}
   332  
   333  	if err = f.Close(); err != nil {
   334  		t.Fatal(err)
   335  	}
   336  
   337  	if err = os.Chown(path, -1, gid); err != nil {
   338  		t.Fatal(err)
   339  	}
   340  
   341  	if err := os.Chmod(path, perms); err != nil {
   342  		t.Fatal(err)
   343  	}
   344  
   345  	return statFile(t, path)
   346  }
   347  
   348  // statFile stats the given file. Fatal on error.
   349  func statFile(t *testing.T, path string) fs.FileInfo {
   350  	t.Helper()
   351  
   352  	stat, err := os.Lstat(path)
   353  	if err != nil {
   354  		t.Fatal(err)
   355  	}
   356  
   357  	return stat
   358  }
   359  
   360  // createTestDir creates the given directory and sets its group sticky bit if
   361  // bool is true. Returns stat of the dir created. Fatal on error.
   362  func createTestDir(t *testing.T, path string, sticky bool) fs.FileInfo {
   363  	t.Helper()
   364  
   365  	if err := os.Mkdir(path, os.ModePerm); err != nil {
   366  		t.Fatal(err)
   367  	}
   368  
   369  	mode := os.ModePerm
   370  	if sticky {
   371  		mode |= os.ModeSetgid
   372  	}
   373  
   374  	if err := os.Chmod(path, mode); err != nil {
   375  		t.Fatal(err)
   376  	}
   377  
   378  	return statFile(t, path)
   379  }
   380  
   381  // getPathGIDs gets the GIDs of the given paths.
   382  func getPathGIDs(t *testing.T, paths []string) []int {
   383  	t.Helper()
   384  
   385  	gids := make([]int, len(paths))
   386  
   387  	for i, path := range paths {
   388  		gids[i] = getPathGID(t, path)
   389  	}
   390  
   391  	return gids
   392  }
   393  
   394  // getPathGID gets the GID of the given path.
   395  func getPathGID(t *testing.T, path string) int {
   396  	t.Helper()
   397  
   398  	info, err := os.Stat(path)
   399  	if err != nil {
   400  		t.Fatal(err)
   401  	}
   402  
   403  	sys := info.Sys()
   404  	stat, ok := sys.(*syscall.Stat_t)
   405  
   406  	if !ok {
   407  		t.Fatal("could not get syscall.Stat_t out of Stat attempt")
   408  	}
   409  
   410  	return int(stat.Gid)
   411  }
   412  
   413  // testSetgidApplied calls setgidApplied() by statting the given path first.
   414  // Fatal on error.
   415  func testSetgidApplied(t *testing.T, path string) bool {
   416  	t.Helper()
   417  
   418  	info, err := os.Stat(path)
   419  	if err != nil {
   420  		t.Fatal(err)
   421  	}
   422  
   423  	return setgidApplied(info)
   424  }
   425  
   426  // is660 tests if the file is user and group read/writable.
   427  func is660(t *testing.T, path string) bool {
   428  	t.Helper()
   429  
   430  	info, err := os.Stat(path)
   431  	if err != nil {
   432  		t.Fatal(err)
   433  	}
   434  
   435  	return info.Mode().Perm() == 0660
   436  }
   437  
   438  // newLogger returns a logger that logs to the returned buffer.
   439  func newLogger() (*bytes.Buffer, log15.Logger) {
   440  	buff := new(bytes.Buffer)
   441  	l := log15.New()
   442  	l.SetHandler(log15.StreamHandler(buff, log15.LogfmtFormat()))
   443  
   444  	return buff, l
   445  }
   446  
   447  // createBadPath creates a directory with a path length greater than 4096, which
   448  // should cause issues.
   449  func createBadPath(t *testing.T) string {
   450  	t.Helper()
   451  
   452  	dir := t.TempDir()
   453  
   454  	wd, err := os.Getwd()
   455  	if err != nil {
   456  		t.Fatal(err)
   457  	}
   458  
   459  	defer func() {
   460  		err = os.Chdir(wd)
   461  		if err != nil {
   462  			t.Fatal(err)
   463  		}
   464  	}()
   465  
   466  	badPath := dir
   467  
   468  	for i := 0; i < 17; i++ {
   469  		err = os.Chdir(dir)
   470  		if err != nil {
   471  			t.Fatal(err)
   472  		}
   473  
   474  		err = os.Mkdir(longBasename, os.ModePerm)
   475  		if err != nil {
   476  			t.Fatal(err)
   477  		}
   478  
   479  		dir = longBasename
   480  		badPath = filepath.Join(badPath, dir)
   481  	}
   482  
   483  	return badPath
   484  }
   485  
   486  // badInfo is an fs.FileInfo that has nonsense data.
   487  type badInfo struct {
   488  	isDir bool
   489  	perm  int
   490  }
   491  
   492  func (b *badInfo) Name() string { return "foo" }
   493  
   494  func (b *badInfo) Size() int64 { return -1 }
   495  
   496  func (b *badInfo) Mode() fs.FileMode {
   497  	if b.perm != 0 {
   498  		return fs.FileMode(b.perm)
   499  	}
   500  
   501  	return os.ModePerm
   502  }
   503  
   504  func (b *badInfo) ModTime() time.Time { return time.Now() }
   505  
   506  func (b *badInfo) IsDir() bool { return b.isDir }
   507  
   508  func (b *badInfo) Sys() interface{} { return &syscall.Stat_t{Gid: 0} }
   509  
   510  // createAndDoTestFile creates a temp file with given gid and perms,
   511  // and calls ch.Do() on it. Set your callback to return true before calling
   512  // this. Returns file permissions as a string afterwards.
   513  func createAndDoTestFile(t *testing.T, otherGID int, perms fs.FileMode, ch *Ch) string {
   514  	t.Helper()
   515  
   516  	dir := t.TempDir()
   517  	path := filepath.Join(dir, "a")
   518  	info := createTestFile(t, path, otherGID, perms)
   519  
   520  	err := ch.Do(path, info)
   521  	So(err, ShouldBeNil)
   522  
   523  	return getFilePermissions(t, path)
   524  }
   525  
   526  func getFilePermissions(t *testing.T, path string) string {
   527  	t.Helper()
   528  
   529  	info, err := os.Stat(path)
   530  	So(err, ShouldBeNil)
   531  
   532  	return info.Mode().Perm().String()
   533  }