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 }