0001
0002
0003"""
0004This is a module to check the filesystem for the presence and
0005permissions of certain files. It can also be used to correct the
0006permissions (but not existance) of those files.
0007
0008Currently only supports Posix systems (with Posixy permissions).
0009Permission stuff can probably be stubbed out later.
0010"""
0011import os
0012import pwd
0013import grp
0014
0015def read_perm_spec(spec):
0016 """
0017 Reads a spec like 'rw-r--r--' into a octal number suitable for
0018 chmod. That is characters in groups of three -- first group is
0019 user, second for group, third for other (all other people). The
0020 characters are r (read), w (write), and x (executable), though the
0021 executable can also be s (sticky). Files in sticky directories
0022 get the directories permission setting.
0023
0024 Examples::
0025
0026 >>> print oct(read_perm_spec('rw-r--r--'))
0027 0644
0028 >>> print oct(read_perm_spec('rw-rwsr--'))
0029 02664
0030 >>> print oct(read_perm_spec('r-xr--r--'))
0031 0544
0032 >>> print oct(read_perm_spec('r--------'))
0033 0400
0034 """
0035 total_mask = 0
0036
0037 set_bits = (04000, 02000, 0)
0038 pieces = (spec[0:3], spec[3:6], spec[6:9])
0039 for i, (mode, set_bit) in enumerate(zip(pieces, set_bits)):
0040 mask = 0
0041 read, write, exe = list(mode)
0042 if read == 'r':
0043 mask = mask | 4
0044 elif read != '-':
0045 raise ValueError, (
0046 "Character %r unexpected (should be '-' or 'r')"
0047 % read)
0048 if write == 'w':
0049 mask = mask | 2
0050 elif write != '-':
0051 raise ValueError, (
0052 "Character %r unexpected (should be '-' or 'w')"
0053 % write)
0054 if exe == 'x':
0055 mask = mask | 1
0056 elif exe not in ('s', '-'):
0057 raise ValueError, (
0058 "Character %r unexpected (should be '-', 'x', or 's')"
0059 % exe)
0060 if exe == 's' and i == 2:
0061 raise ValueError, (
0062 "The 'other' executable setting cannot be suid/sgid ('s')")
0063 mask = mask << ((2-i)*3)
0064 if exe == 's':
0065 mask = mask | set_bit
0066 total_mask = total_mask | mask
0067 return total_mask
0068
0069modes = [
0070 (04000, 'setuid bit',
0071 'setuid bit: make contents owned by directory owner'),
0072 (02000, 'setgid bit',
0073 'setgid bit: make contents inherit permissions from directory'),
0074 (01000, 'sticky bit',
0075 'sticky bit: append-only directory'),
0076 (00400, 'read by owner', 'read by owner'),
0077 (00200, 'write by owner', 'write by owner'),
0078 (00100, 'execute by owner', 'owner can search directory'),
0079 (00040, 'allow read by group members',
0080 'allow read by group members',),
0081 (00020, 'allow write by group members',
0082 'allow write by group members'),
0083 (00010, 'execute by group members',
0084 'group members can search directory'),
0085 (00004, 'read by others', 'read by others'),
0086 (00002, 'write by others', 'write by others'),
0087 (00001, 'execution by others', 'others can search directory'),
0088 ]
0089
0090exe_bits = [0100, 0010, 0001]
0091exe_mask = 0111
0092full_mask = 07777
0093
0094def mode_diff(filename, mode, **kw):
0095 """
0096 Returns the differences calculated using ``calc_mode_diff``
0097 """
0098 cur_mode = os.stat(filename).st_mode
0099 return calc_mode_diff(cur_mode, mode, **kw)
0100
0101def calc_mode_diff(cur_mode, mode, keep_exe=True,
0102 not_set='not set: ',
0103 set='set: '):
0104 """
0105 Gives the difference between the actual mode of the file and the
0106 given mode. If ``keep_exe`` is true, then if the mode doesn't
0107 include any executable information the executable information will
0108 simply be ignored. High bits are also always ignored (except
0109 suid/sgid and sticky bit).
0110
0111 Returns a list of differences (empty list if no differences)
0112 """
0113 for exe_bit in exe_bits:
0114 if mode & exe_bit:
0115 keep_exe = False
0116 diffs = []
0117 isdir = os.path.isdir(filename)
0118 for bit, file_desc, dir_desc in modes:
0119 if keep_exe and bit in exe_bits:
0120 continue
0121 if isdir:
0122 desc = dir_desc
0123 else:
0124 desc = file_desc
0125 if (mode & bit) and not (cur_mode & bit):
0126 diffs.append(not_set + desc)
0127 if not (mode & bit) and (cur_mode & bit):
0128 diffs.append(set + desc)
0129 return diffs
0130
0131def calc_set_mode(cur_mode, mode, keep_exe=True):
0132 """
0133 Calculates the new mode given the current node ``cur_mode`` and
0134 the mode spec ``mode`` and if ``keep_exe`` is true then also keep
0135 the executable bits in ``cur_mode`` if ``mode`` has no executable
0136 bits in it. Return the new mode.
0137
0138 Examples::
0139
0140 >>> print oct(calc_set_mode(0775, 0644))
0141 0755
0142 >>> print oct(calc_set_mode(0775, 0744))
0143 0744
0144 >>> print oct(calc_set_mode(010600, 0644))
0145 010644
0146 >>> print oct(calc_set_mode(0775, 0644, False))
0147 0644
0148 """
0149 for exe_bit in exe_bits:
0150 if mode & exe_bit:
0151 keep_exe = False
0152
0153 keep_parts = (cur_mode | full_mask) ^ full_mask
0154 if keep_exe:
0155 keep_parts = keep_parts | (cur_mode & exe_mask)
0156 new_mode = keep_parts | mode
0157 return new_mode
0158
0159def set_mode(filename, mode, **kw):
0160 """
0161 Sets the mode on ``filename`` using ``calc_set_mode``
0162 """
0163 cur_mode = os.stat(filename).st_mode
0164 new_mode = calc_set_mode(cur_mode, mode, **kw)
0165 os.chmod(filename, new_mode)
0166
0167def calc_ownership_spec(spec):
0168 """
0169 Calculates what a string spec means, returning (uid, username,
0170 gid, groupname), where there can be None values meaning no
0171 preference.
0172
0173 The spec is a string like ``owner:group``. It may use numbers
0174 instead of user/group names. It may leave out ``:group``. It may
0175 use '-' to mean any-user/any-group.
0176
0177 """
0178 user = group = None
0179 uid = gid = None
0180 if ':' in spec:
0181 user_spec, group_spec = spec.split(':', 1)
0182 else:
0183 user_spec, group_spec = spec, '-'
0184 if user_spec == '-':
0185 user_spec = '0'
0186 if group_spec == '-':
0187 group_spec = '0'
0188 try:
0189 uid = int(user_spec)
0190 except ValueError:
0191 uid = pwd.getpwnam(user_spec)
0192 user = user_spec
0193 else:
0194 if not uid:
0195 uid = user = None
0196 else:
0197 user = pwd.getpwuid(uid).pw_name
0198 try:
0199 gid = int(group_spec)
0200 except ValueError:
0201 gid = grp.getgrnam(group_spec)
0202 group = group_spec
0203 else:
0204 if not gid:
0205 gid = group = None
0206 else:
0207 group = grp.getgrgid(gid).gr_name
0208 return (uid, user, gid, group)
0209
0210def ownership_diff(filename, spec):
0211 """
0212 Return a list of differences between the ownership of ``filename``
0213 and the spec given.
0214 """
0215 diffs = []
0216 uid, user, gid, group = calc_ownership_spec(spec)
0217 st = os.stat(filename)
0218 if uid and uid != st.st_uid:
0219 diffs.append('owned by %s (should be %s)' %
0220 (pwd.getpwuid(st.st_uid).pw_name, user))
0221 if gid and gid != st.st_gid:
0222 diffs.append('group %s (should be %s)' %
0223 (grp.getgrgid(st.st_gid).gr_name, group))
0224 return diffs
0225
0226def set_ownership(filename, spec):
0227 """
0228 Set the ownership of ``filename`` given the spec.
0229 """
0230 uid, user, gid, group = calc_ownership_spec(spec)
0231 st = os.stat(filename)
0232 if not uid:
0233 uid = st.st_uid
0234 if not gid:
0235 gid = st.st_gid
0236 os.chmod(filename, uid, gid)
0237
0238class PermissionSpec(object):
0239 """
0240 Represents a set of specifications for permissions.
0241
0242 Typically reads from a file that looks like this::
0243
0244 rwxrwxrwx user:group filename
0245
0246 If the filename ends in /, then it expected to be a directory, and
0247 the directory is made executable automatically, and the contents
0248 of the directory are given the same permission (recursively). By
0249 default the executable bit on files is left as-is, unless the
0250 permissions specifically say it should be on in some way.
0251
0252 You can use 'nomodify filename' for permissions to say that any
0253 permission is okay, and permissions should not be changed.
0254
0255 Use 'noexist filename' to say that a specific file should not
0256 exist.
0257
0258 Use 'symlink filename symlinked_to' to assert a symlink destination
0259
0260 The entire file is read, and most specific rules are used for each
0261 file (i.e., a rule for a subdirectory overrides the rule for a
0262 superdirectory). Order does not matter.
0263 """
0264
0265 def __init__(self):
0266 self.paths = {}
0267
0268 def parsefile(self, filename):
0269 f = open(filename)
0270 lines = f.readlines()
0271 f.close()
0272 self.parselines(lines, filename=filename)
0273
0274 commands = {}
0275
0276 def parselines(self, lines, filename=None):
0277 for lineindex, line in enumerate(lines):
0278 line = line.strip()
0279 if not line or line.startswith('#'):
0280 continue
0281 parts = line.split()
0282 command = parts[0]
0283 if command in self.commands:
0284 cmd = self.commands[command](*parts[1:])
0285 else:
0286 cmd = self.commands['*'](*parts)
0287 self.paths[cmd.path] = cmd
0288
0289 def check(self):
0290 action = _Check(self)
0291 self.traverse(action)
0292
0293 def fix(self):
0294 action = _Fixer(self)
0295 self.traverse(action)
0296
0297 def traverse(self, action):
0298 paths = self.paths_sorted()
0299 checked = {}
0300 for path, checker in list(paths)[::-1]:
0301 self.check_tree(action, path, paths, checked)
0302 for path, checker in paths:
0303 if path not in checked:
0304 action.noexists(path, checker)
0305
0306 def traverse_tree(self, action, path, paths, checked):
0307 if path in checked:
0308 return
0309 self.traverse_path(action, path, paths, checked)
0310 if os.path.isdir(path):
0311 for fn in os.listdir(path):
0312 fn = os.path.join(path, fn)
0313 self.traverse_tree(action, fn, paths, checked)
0314
0315 def traverse_path(self, action, path, paths, checked):
0316 checked[path] = None
0317 for check_path, checker in paths:
0318 if path.startswith(check_path):
0319 action.check(check_path, checker)
0320 if not checker.inherit:
0321 break
0322
0323 def paths_sorted(self):
0324 paths = self.paths.items()
0325 paths.sort(lambda a, b: -cmp(len(a[0]), len(b[0])))
0326
0327class _Rule(object):
0328 class __metaclass__(type):
0329 def __new__(meta, class_name, bases, d):
0330 cls = type.__new__(meta, class_name, bases, d)
0331 PermissionSpec.commands[cls.__name__] = cls
0332 return cls
0333
0334 inherit = False
0335 def noexists(self):
0336 return ['Path %s does not exist' % path]
0337
0338class _NoModify(_Rule):
0339
0340 name = 'nomodify'
0341
0342 def __init__(self, path):
0343 self.path = path
0344
0345 def fix(self, path):
0346 pass
0347
0348class _NoExist(_Rule):
0349
0350 name = 'noexist'
0351
0352 def __init__(self, path):
0353 self.path = path
0354
0355 def check(self, path):
0356 return ['Path %s should not exist' % path]
0357
0358 def noexists(self, path):
0359 return []
0360
0361 def fix(self, path):
0362
0363 pass
0364
0365class _SymLink(_Rule):
0366
0367 name = 'symlink'
0368 inherit = True
0369
0370 def __init__(self, path, dest):
0371 self.path = path
0372 self.dest = dest
0373
0374 def check(self, path):
0375 assert path == self.path, (
0376 "_Symlink should only be passed specific path %s (not %s)"
0377 % (self.path, path))
0378 try:
0379 link = os.path.readlink(path)
0380 except OSError:
0381 if e.errno != 22:
0382 raise
0383 return ['Path %s is not a symlink (should point to %s)'
0384 % (path, self.dest)]
0385 if link != self.dest:
0386 return ['Path %s should symlink to %s, not %s'
0387 % (path, self.dest, link)]
0388 return []
0389
0390 def fix(self, path):
0391 assert path == self.path, (
0392 "_Symlink should only be passed specific path %s (not %s)"
0393 % (self.path, path))
0394 if not os.path.exists(path):
0395 os.symlink(path, self.dest)
0396 else:
0397
0398 print 'Not symlinking %s' % path
0399
0400class _Permission(_Rule):
0401
0402 name = '*'
0403
0404 def __init__(self, perm, owner, dir):
0405 self.perm_spec = read_perm_spec(perm)
0406 self.owner = owner
0407 self.dir = dir
0408
0409 def check(self, path):
0410 return mode_diff(path, self.perm_spec)
0411
0412 def fix(self, path):
0413 set_mode(path, self.perm_spec)
0414
0415class _Strategy(object):
0416
0417 def __init__(self, spec):
0418 self.spec = spec
0419
0420class _Check(_Strategy):
0421
0422 def noexists(self, path, checker):
0423 checker.noexists(path)
0424
0425 def check(self, path, checker):
0426 checker.check(path)
0427
0428class _Fixer(_Strategy):
0429
0430 def noexists(self, path, checker):
0431 pass
0432
0433 def check(self, path, checker):
0434 checker.fix(path)
0435
0436if __name__ == '__main__':
0437 import doctest
0438 doctest.testmod()