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 }