github.com/wtsi-ssg/wrstat/v3@v3.2.3/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 }