[3] | 1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
---|
| 2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
---|
| 3 | """ |
---|
| 4 | This is a module to check the filesystem for the presence and |
---|
| 5 | permissions of certain files. It can also be used to correct the |
---|
| 6 | permissions (but not existance) of those files. |
---|
| 7 | |
---|
| 8 | Currently only supports Posix systems (with Posixy permissions). |
---|
| 9 | Permission stuff can probably be stubbed out later. |
---|
| 10 | """ |
---|
| 11 | import os |
---|
| 12 | |
---|
| 13 | def read_perm_spec(spec): |
---|
| 14 | """ |
---|
| 15 | Reads a spec like 'rw-r--r--' into a octal number suitable for |
---|
| 16 | chmod. That is characters in groups of three -- first group is |
---|
| 17 | user, second for group, third for other (all other people). The |
---|
| 18 | characters are r (read), w (write), and x (executable), though the |
---|
| 19 | executable can also be s (sticky). Files in sticky directories |
---|
| 20 | get the directories permission setting. |
---|
| 21 | |
---|
| 22 | Examples:: |
---|
| 23 | |
---|
| 24 | >>> print oct(read_perm_spec('rw-r--r--')) |
---|
| 25 | 0644 |
---|
| 26 | >>> print oct(read_perm_spec('rw-rwsr--')) |
---|
| 27 | 02664 |
---|
| 28 | >>> print oct(read_perm_spec('r-xr--r--')) |
---|
| 29 | 0544 |
---|
| 30 | >>> print oct(read_perm_spec('r--------')) |
---|
| 31 | 0400 |
---|
| 32 | """ |
---|
| 33 | total_mask = 0 |
---|
| 34 | # suid/sgid modes give this mask in user, group, other mode: |
---|
| 35 | set_bits = (04000, 02000, 0) |
---|
| 36 | pieces = (spec[0:3], spec[3:6], spec[6:9]) |
---|
| 37 | for i, (mode, set_bit) in enumerate(zip(pieces, set_bits)): |
---|
| 38 | mask = 0 |
---|
| 39 | read, write, exe = list(mode) |
---|
| 40 | if read == 'r': |
---|
| 41 | mask = mask | 4 |
---|
| 42 | elif read != '-': |
---|
| 43 | raise ValueError, ( |
---|
| 44 | "Character %r unexpected (should be '-' or 'r')" |
---|
| 45 | % read) |
---|
| 46 | if write == 'w': |
---|
| 47 | mask = mask | 2 |
---|
| 48 | elif write != '-': |
---|
| 49 | raise ValueError, ( |
---|
| 50 | "Character %r unexpected (should be '-' or 'w')" |
---|
| 51 | % write) |
---|
| 52 | if exe == 'x': |
---|
| 53 | mask = mask | 1 |
---|
| 54 | elif exe not in ('s', '-'): |
---|
| 55 | raise ValueError, ( |
---|
| 56 | "Character %r unexpected (should be '-', 'x', or 's')" |
---|
| 57 | % exe) |
---|
| 58 | if exe == 's' and i == 2: |
---|
| 59 | raise ValueError, ( |
---|
| 60 | "The 'other' executable setting cannot be suid/sgid ('s')") |
---|
| 61 | mask = mask << ((2-i)*3) |
---|
| 62 | if exe == 's': |
---|
| 63 | mask = mask | set_bit |
---|
| 64 | total_mask = total_mask | mask |
---|
| 65 | return total_mask |
---|
| 66 | |
---|
| 67 | modes = [ |
---|
| 68 | (04000, 'setuid bit', |
---|
| 69 | 'setuid bit: make contents owned by directory owner'), |
---|
| 70 | (02000, 'setgid bit', |
---|
| 71 | 'setgid bit: make contents inherit permissions from directory'), |
---|
| 72 | (01000, 'sticky bit', |
---|
| 73 | 'sticky bit: append-only directory'), |
---|
| 74 | (00400, 'read by owner', 'read by owner'), |
---|
| 75 | (00200, 'write by owner', 'write by owner'), |
---|
| 76 | (00100, 'execute by owner', 'owner can search directory'), |
---|
| 77 | (00040, 'allow read by group members', |
---|
| 78 | 'allow read by group members',), |
---|
| 79 | (00020, 'allow write by group members', |
---|
| 80 | 'allow write by group members'), |
---|
| 81 | (00010, 'execute by group members', |
---|
| 82 | 'group members can search directory'), |
---|
| 83 | (00004, 'read by others', 'read by others'), |
---|
| 84 | (00002, 'write by others', 'write by others'), |
---|
| 85 | (00001, 'execution by others', 'others can search directory'), |
---|
| 86 | ] |
---|
| 87 | |
---|
| 88 | exe_bits = [0100, 0010, 0001] |
---|
| 89 | exe_mask = 0111 |
---|
| 90 | full_mask = 07777 |
---|
| 91 | |
---|
| 92 | def mode_diff(filename, mode, **kw): |
---|
| 93 | """ |
---|
| 94 | Returns the differences calculated using ``calc_mode_diff`` |
---|
| 95 | """ |
---|
| 96 | cur_mode = os.stat(filename).st_mode |
---|
| 97 | return calc_mode_diff(cur_mode, mode, **kw) |
---|
| 98 | |
---|
| 99 | def calc_mode_diff(cur_mode, mode, keep_exe=True, |
---|
| 100 | not_set='not set: ', |
---|
| 101 | set='set: '): |
---|
| 102 | """ |
---|
| 103 | Gives the difference between the actual mode of the file and the |
---|
| 104 | given mode. If ``keep_exe`` is true, then if the mode doesn't |
---|
| 105 | include any executable information the executable information will |
---|
| 106 | simply be ignored. High bits are also always ignored (except |
---|
| 107 | suid/sgid and sticky bit). |
---|
| 108 | |
---|
| 109 | Returns a list of differences (empty list if no differences) |
---|
| 110 | """ |
---|
| 111 | for exe_bit in exe_bits: |
---|
| 112 | if mode & exe_bit: |
---|
| 113 | keep_exe = False |
---|
| 114 | diffs = [] |
---|
| 115 | isdir = os.path.isdir(filename) |
---|
| 116 | for bit, file_desc, dir_desc in modes: |
---|
| 117 | if keep_exe and bit in exe_bits: |
---|
| 118 | continue |
---|
| 119 | if isdir: |
---|
| 120 | desc = dir_desc |
---|
| 121 | else: |
---|
| 122 | desc = file_desc |
---|
| 123 | if (mode & bit) and not (cur_mode & bit): |
---|
| 124 | diffs.append(not_set + desc) |
---|
| 125 | if not (mode & bit) and (cur_mode & bit): |
---|
| 126 | diffs.append(set + desc) |
---|
| 127 | return diffs |
---|
| 128 | |
---|
| 129 | def calc_set_mode(cur_mode, mode, keep_exe=True): |
---|
| 130 | """ |
---|
| 131 | Calculates the new mode given the current node ``cur_mode`` and |
---|
| 132 | the mode spec ``mode`` and if ``keep_exe`` is true then also keep |
---|
| 133 | the executable bits in ``cur_mode`` if ``mode`` has no executable |
---|
| 134 | bits in it. Return the new mode. |
---|
| 135 | |
---|
| 136 | Examples:: |
---|
| 137 | |
---|
| 138 | >>> print oct(calc_set_mode(0775, 0644)) |
---|
| 139 | 0755 |
---|
| 140 | >>> print oct(calc_set_mode(0775, 0744)) |
---|
| 141 | 0744 |
---|
| 142 | >>> print oct(calc_set_mode(010600, 0644)) |
---|
| 143 | 010644 |
---|
| 144 | >>> print oct(calc_set_mode(0775, 0644, False)) |
---|
| 145 | 0644 |
---|
| 146 | """ |
---|
| 147 | for exe_bit in exe_bits: |
---|
| 148 | if mode & exe_bit: |
---|
| 149 | keep_exe = False |
---|
| 150 | # This zeros-out full_mask parts of the current mode: |
---|
| 151 | keep_parts = (cur_mode | full_mask) ^ full_mask |
---|
| 152 | if keep_exe: |
---|
| 153 | keep_parts = keep_parts | (cur_mode & exe_mask) |
---|
| 154 | new_mode = keep_parts | mode |
---|
| 155 | return new_mode |
---|
| 156 | |
---|
| 157 | def set_mode(filename, mode, **kw): |
---|
| 158 | """ |
---|
| 159 | Sets the mode on ``filename`` using ``calc_set_mode`` |
---|
| 160 | """ |
---|
| 161 | cur_mode = os.stat(filename).st_mode |
---|
| 162 | new_mode = calc_set_mode(cur_mode, mode, **kw) |
---|
| 163 | os.chmod(filename, new_mode) |
---|
| 164 | |
---|
| 165 | def calc_ownership_spec(spec): |
---|
| 166 | """ |
---|
| 167 | Calculates what a string spec means, returning (uid, username, |
---|
| 168 | gid, groupname), where there can be None values meaning no |
---|
| 169 | preference. |
---|
| 170 | |
---|
| 171 | The spec is a string like ``owner:group``. It may use numbers |
---|
| 172 | instead of user/group names. It may leave out ``:group``. It may |
---|
| 173 | use '-' to mean any-user/any-group. |
---|
| 174 | |
---|
| 175 | """ |
---|
| 176 | import grp |
---|
| 177 | import pwd |
---|
| 178 | user = group = None |
---|
| 179 | uid = gid = None |
---|
| 180 | if ':' in spec: |
---|
| 181 | user_spec, group_spec = spec.split(':', 1) |
---|
| 182 | else: |
---|
| 183 | user_spec, group_spec = spec, '-' |
---|
| 184 | if user_spec == '-': |
---|
| 185 | user_spec = '0' |
---|
| 186 | if group_spec == '-': |
---|
| 187 | group_spec = '0' |
---|
| 188 | try: |
---|
| 189 | uid = int(user_spec) |
---|
| 190 | except ValueError: |
---|
| 191 | uid = pwd.getpwnam(user_spec) |
---|
| 192 | user = user_spec |
---|
| 193 | else: |
---|
| 194 | if not uid: |
---|
| 195 | uid = user = None |
---|
| 196 | else: |
---|
| 197 | user = pwd.getpwuid(uid).pw_name |
---|
| 198 | try: |
---|
| 199 | gid = int(group_spec) |
---|
| 200 | except ValueError: |
---|
| 201 | gid = grp.getgrnam(group_spec) |
---|
| 202 | group = group_spec |
---|
| 203 | else: |
---|
| 204 | if not gid: |
---|
| 205 | gid = group = None |
---|
| 206 | else: |
---|
| 207 | group = grp.getgrgid(gid).gr_name |
---|
| 208 | return (uid, user, gid, group) |
---|
| 209 | |
---|
| 210 | def ownership_diff(filename, spec): |
---|
| 211 | """ |
---|
| 212 | Return a list of differences between the ownership of ``filename`` |
---|
| 213 | and the spec given. |
---|
| 214 | """ |
---|
| 215 | import grp |
---|
| 216 | import pwd |
---|
| 217 | diffs = [] |
---|
| 218 | uid, user, gid, group = calc_ownership_spec(spec) |
---|
| 219 | st = os.stat(filename) |
---|
| 220 | if uid and uid != st.st_uid: |
---|
| 221 | diffs.append('owned by %s (should be %s)' % |
---|
| 222 | (pwd.getpwuid(st.st_uid).pw_name, user)) |
---|
| 223 | if gid and gid != st.st_gid: |
---|
| 224 | diffs.append('group %s (should be %s)' % |
---|
| 225 | (grp.getgrgid(st.st_gid).gr_name, group)) |
---|
| 226 | return diffs |
---|
| 227 | |
---|
| 228 | def set_ownership(filename, spec): |
---|
| 229 | """ |
---|
| 230 | Set the ownership of ``filename`` given the spec. |
---|
| 231 | """ |
---|
| 232 | uid, user, gid, group = calc_ownership_spec(spec) |
---|
| 233 | st = os.stat(filename) |
---|
| 234 | if not uid: |
---|
| 235 | uid = st.st_uid |
---|
| 236 | if not gid: |
---|
| 237 | gid = st.st_gid |
---|
| 238 | os.chmod(filename, uid, gid) |
---|
| 239 | |
---|
| 240 | class PermissionSpec(object): |
---|
| 241 | """ |
---|
| 242 | Represents a set of specifications for permissions. |
---|
| 243 | |
---|
| 244 | Typically reads from a file that looks like this:: |
---|
| 245 | |
---|
| 246 | rwxrwxrwx user:group filename |
---|
| 247 | |
---|
| 248 | If the filename ends in /, then it expected to be a directory, and |
---|
| 249 | the directory is made executable automatically, and the contents |
---|
| 250 | of the directory are given the same permission (recursively). By |
---|
| 251 | default the executable bit on files is left as-is, unless the |
---|
| 252 | permissions specifically say it should be on in some way. |
---|
| 253 | |
---|
| 254 | You can use 'nomodify filename' for permissions to say that any |
---|
| 255 | permission is okay, and permissions should not be changed. |
---|
| 256 | |
---|
| 257 | Use 'noexist filename' to say that a specific file should not |
---|
| 258 | exist. |
---|
| 259 | |
---|
| 260 | Use 'symlink filename symlinked_to' to assert a symlink destination |
---|
| 261 | |
---|
| 262 | The entire file is read, and most specific rules are used for each |
---|
| 263 | file (i.e., a rule for a subdirectory overrides the rule for a |
---|
| 264 | superdirectory). Order does not matter. |
---|
| 265 | """ |
---|
| 266 | |
---|
| 267 | def __init__(self): |
---|
| 268 | self.paths = {} |
---|
| 269 | |
---|
| 270 | def parsefile(self, filename): |
---|
| 271 | f = open(filename) |
---|
| 272 | lines = f.readlines() |
---|
| 273 | f.close() |
---|
| 274 | self.parselines(lines, filename=filename) |
---|
| 275 | |
---|
| 276 | commands = {} |
---|
| 277 | |
---|
| 278 | def parselines(self, lines, filename=None): |
---|
| 279 | for lineindex, line in enumerate(lines): |
---|
| 280 | line = line.strip() |
---|
| 281 | if not line or line.startswith('#'): |
---|
| 282 | continue |
---|
| 283 | parts = line.split() |
---|
| 284 | command = parts[0] |
---|
| 285 | if command in self.commands: |
---|
| 286 | cmd = self.commands[command](*parts[1:]) |
---|
| 287 | else: |
---|
| 288 | cmd = self.commands['*'](*parts) |
---|
| 289 | self.paths[cmd.path] = cmd |
---|
| 290 | |
---|
| 291 | def check(self): |
---|
| 292 | action = _Check(self) |
---|
| 293 | self.traverse(action) |
---|
| 294 | |
---|
| 295 | def fix(self): |
---|
| 296 | action = _Fixer(self) |
---|
| 297 | self.traverse(action) |
---|
| 298 | |
---|
| 299 | def traverse(self, action): |
---|
| 300 | paths = self.paths_sorted() |
---|
| 301 | checked = {} |
---|
| 302 | for path, checker in list(paths)[::-1]: |
---|
| 303 | self.check_tree(action, path, paths, checked) |
---|
| 304 | for path, checker in paths: |
---|
| 305 | if path not in checked: |
---|
| 306 | action.noexists(path, checker) |
---|
| 307 | |
---|
| 308 | def traverse_tree(self, action, path, paths, checked): |
---|
| 309 | if path in checked: |
---|
| 310 | return |
---|
| 311 | self.traverse_path(action, path, paths, checked) |
---|
| 312 | if os.path.isdir(path): |
---|
| 313 | for fn in os.listdir(path): |
---|
| 314 | fn = os.path.join(path, fn) |
---|
| 315 | self.traverse_tree(action, fn, paths, checked) |
---|
| 316 | |
---|
| 317 | def traverse_path(self, action, path, paths, checked): |
---|
| 318 | checked[path] = None |
---|
| 319 | for check_path, checker in paths: |
---|
| 320 | if path.startswith(check_path): |
---|
| 321 | action.check(check_path, checker) |
---|
| 322 | if not checker.inherit: |
---|
| 323 | break |
---|
| 324 | |
---|
| 325 | def paths_sorted(self): |
---|
| 326 | paths = self.paths.items() |
---|
| 327 | paths.sort(lambda a, b: -cmp(len(a[0]), len(b[0]))) |
---|
| 328 | |
---|
| 329 | class _Rule(object): |
---|
| 330 | class __metaclass__(type): |
---|
| 331 | def __new__(meta, class_name, bases, d): |
---|
| 332 | cls = type.__new__(meta, class_name, bases, d) |
---|
| 333 | PermissionSpec.commands[cls.__name__] = cls |
---|
| 334 | return cls |
---|
| 335 | |
---|
| 336 | inherit = False |
---|
| 337 | def noexists(self): |
---|
| 338 | return ['Path %s does not exist' % path] |
---|
| 339 | |
---|
| 340 | class _NoModify(_Rule): |
---|
| 341 | |
---|
| 342 | name = 'nomodify' |
---|
| 343 | |
---|
| 344 | def __init__(self, path): |
---|
| 345 | self.path = path |
---|
| 346 | |
---|
| 347 | def fix(self, path): |
---|
| 348 | pass |
---|
| 349 | |
---|
| 350 | class _NoExist(_Rule): |
---|
| 351 | |
---|
| 352 | name = 'noexist' |
---|
| 353 | |
---|
| 354 | def __init__(self, path): |
---|
| 355 | self.path = path |
---|
| 356 | |
---|
| 357 | def check(self, path): |
---|
| 358 | return ['Path %s should not exist' % path] |
---|
| 359 | |
---|
| 360 | def noexists(self, path): |
---|
| 361 | return [] |
---|
| 362 | |
---|
| 363 | def fix(self, path): |
---|
| 364 | # @@: Should delete? |
---|
| 365 | pass |
---|
| 366 | |
---|
| 367 | class _SymLink(_Rule): |
---|
| 368 | |
---|
| 369 | name = 'symlink' |
---|
| 370 | inherit = True |
---|
| 371 | |
---|
| 372 | def __init__(self, path, dest): |
---|
| 373 | self.path = path |
---|
| 374 | self.dest = dest |
---|
| 375 | |
---|
| 376 | def check(self, path): |
---|
| 377 | assert path == self.path, ( |
---|
| 378 | "_Symlink should only be passed specific path %s (not %s)" |
---|
| 379 | % (self.path, path)) |
---|
| 380 | try: |
---|
| 381 | link = os.path.readlink(path) |
---|
| 382 | except OSError: |
---|
| 383 | if e.errno != 22: |
---|
| 384 | raise |
---|
| 385 | return ['Path %s is not a symlink (should point to %s)' |
---|
| 386 | % (path, self.dest)] |
---|
| 387 | if link != self.dest: |
---|
| 388 | return ['Path %s should symlink to %s, not %s' |
---|
| 389 | % (path, self.dest, link)] |
---|
| 390 | return [] |
---|
| 391 | |
---|
| 392 | def fix(self, path): |
---|
| 393 | assert path == self.path, ( |
---|
| 394 | "_Symlink should only be passed specific path %s (not %s)" |
---|
| 395 | % (self.path, path)) |
---|
| 396 | if not os.path.exists(path): |
---|
| 397 | os.symlink(path, self.dest) |
---|
| 398 | else: |
---|
| 399 | # @@: This should correct the symlink or something: |
---|
| 400 | print 'Not symlinking %s' % path |
---|
| 401 | |
---|
| 402 | class _Permission(_Rule): |
---|
| 403 | |
---|
| 404 | name = '*' |
---|
| 405 | |
---|
| 406 | def __init__(self, perm, owner, dir): |
---|
| 407 | self.perm_spec = read_perm_spec(perm) |
---|
| 408 | self.owner = owner |
---|
| 409 | self.dir = dir |
---|
| 410 | |
---|
| 411 | def check(self, path): |
---|
| 412 | return mode_diff(path, self.perm_spec) |
---|
| 413 | |
---|
| 414 | def fix(self, path): |
---|
| 415 | set_mode(path, self.perm_spec) |
---|
| 416 | |
---|
| 417 | class _Strategy(object): |
---|
| 418 | |
---|
| 419 | def __init__(self, spec): |
---|
| 420 | self.spec = spec |
---|
| 421 | |
---|
| 422 | class _Check(_Strategy): |
---|
| 423 | |
---|
| 424 | def noexists(self, path, checker): |
---|
| 425 | checker.noexists(path) |
---|
| 426 | |
---|
| 427 | def check(self, path, checker): |
---|
| 428 | checker.check(path) |
---|
| 429 | |
---|
| 430 | class _Fixer(_Strategy): |
---|
| 431 | |
---|
| 432 | def noexists(self, path, checker): |
---|
| 433 | pass |
---|
| 434 | |
---|
| 435 | def check(self, path, checker): |
---|
| 436 | checker.fix(path) |
---|
| 437 | |
---|
| 438 | if __name__ == '__main__': |
---|
| 439 | import doctest |
---|
| 440 | doctest.testmod() |
---|
| 441 | |
---|