0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
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    # suid/sgid modes give this mask in user, group, other mode:
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    # This zeros-out full_mask parts of the current mode:
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        # @@: Should delete?
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            # @@: This should correct the symlink or something:
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()