#! @PYTHON@ # # Copyright (C) 1998-2009 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. """Check the permissions for the Mailman installation. Usage: %(PROGRAM)s [-f] [-v] [-h] With no arguments, just check and report all the files that have bogus permissions or group ownership. With -f (and run as root), fix all the permission problems found. With -v be verbose. """ import os import sys import pwd import grp import errno import getopt from stat import * try: import paths except ImportError: print '''Could not import paths! This probably means that you are trying to run check_perms from the source directory. You must run this from the installation directory instead. ''' raise from Mailman import mm_cfg from Mailman.mm_cfg import MAILMAN_USER, MAILMAN_GROUP from Mailman.i18n import C_ # Let KeyErrors percolate MAILMAN_GID = grp.getgrnam(MAILMAN_GROUP)[2] MAILMAN_UID = pwd.getpwnam(MAILMAN_USER)[2] PROGRAM = sys.argv[0] # Gotta check the archives/private/*/database/* files try: True, False except NameError: True = 1 False = 0 class State: FIX = False VERBOSE = False ERRORS = 0 STATE = State() DIRPERMS = S_ISGID | S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH QFILEPERMS = S_ISGID | S_IRWXU | S_IRWXG PYFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH ARTICLEFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP PRIVATEPERMS = QFILEPERMS def statmode(path): return os.stat(path)[ST_MODE] def statgidmode(path): stat = os.stat(path) return stat[ST_MODE], stat[ST_GID] seen = {} # libc's getgrgid re-opens /etc/group each time :( _gidcache = {} def getgrgid(gid): data = _gidcache.get(gid) if data is None: data = grp.getgrgid(gid) _gidcache[gid] = data return data def checkwalk(arg, dirname, names): # Short-circuit duplicates if seen.has_key(dirname): return seen[dirname] = True for name in names: path = os.path.join(dirname, name) if arg.VERBOSE: print C_(' checking gid and mode for %(path)s') try: mode, gid = statgidmode(path) except OSError, e: if e.errno <> errno.ENOENT: raise continue if gid <> MAILMAN_GID: try: groupname = getgrgid(gid)[0] except KeyError: groupname = '' % gid arg.ERRORS += 1 print C_('%(path)s bad group (has: %(groupname)s, ' 'expected %(MAILMAN_GROUP)s)'), if STATE.FIX: print C_('(fixing)') os.chown(path, -1, MAILMAN_GID) else: print # Most directories must be at least rwxrwsr-x. # The private archive directory and database directory must be at # least rwxrws---. Their 'other' permissions are checked in # checkarchives() and checkarchivedbs() below. Their 'user' and # 'group' permissions are checked here. # The directories under qfiles should be rwxrws---. Their 'user' and # 'group' permissions are checked here. Their 'other' permissions # aren't checked. private = mm_cfg.PRIVATE_ARCHIVE_FILE_DIR if path == private or ( os.path.commonprefix((path, private)) == private and os.path.split(path)[1] == 'database'): # then... targetperms = PRIVATEPERMS elif (os.path.commonprefix((path, mm_cfg.QUEUE_DIR)) == mm_cfg.QUEUE_DIR): targetperms = QFILEPERMS else: targetperms = DIRPERMS octperms = oct(targetperms) if S_ISDIR(mode) and (mode & targetperms) <> targetperms: arg.ERRORS += 1 print C_('directory permissions must be %(octperms)s: %(path)s'), if STATE.FIX: print C_('(fixing)') os.chmod(path, mode | targetperms) else: print elif os.path.splitext(path)[1] in ('.py', '.pyc', '.pyo'): octperms = oct(PYFILEPERMS) if mode & PYFILEPERMS <> PYFILEPERMS: print C_('source perms must be %(octperms)s: %(path)s'), arg.ERRORS += 1 if STATE.FIX: print C_('(fixing)') os.chmod(path, mode | PYFILEPERMS) else: print elif path.endswith('-article'): # Article files must be group writeable octperms = oct(ARTICLEFILEPERMS) if mode & ARTICLEFILEPERMS <> ARTICLEFILEPERMS: print C_('article db files must be %(octperms)s: %(path)s'), arg.ERRORS += 1 if STATE.FIX: print C_('(fixing)') os.chmod(path, mode | ARTICLEFILEPERMS) else: print def checkall(): # first check PREFIX if STATE.VERBOSE: prefix = mm_cfg.PREFIX print C_('checking mode for %(prefix)s') dirs = {} for d in (mm_cfg.PREFIX, mm_cfg.EXEC_PREFIX, mm_cfg.VAR_PREFIX, mm_cfg.LOG_DIR): dirs[d] = True for d in dirs.keys(): try: mode = statmode(d) except OSError, e: if e.errno <> errno.ENOENT: raise print C_('WARNING: directory does not exist: %(d)s') continue if (mode & DIRPERMS) <> DIRPERMS: STATE.ERRORS += 1 print C_('directory must be at least 02775: %(d)s'), if STATE.FIX: print C_('(fixing)') os.chmod(d, mode | DIRPERMS) else: print # check all subdirs os.path.walk(d, checkwalk, STATE) def checkarchives(): private = mm_cfg.PRIVATE_ARCHIVE_FILE_DIR if STATE.VERBOSE: print C_('checking perms on %(private)s') # private archives must not be other readable mode = statmode(private) if mode & S_IROTH: STATE.ERRORS += 1 print C_('%(private)s must not be other-readable'), if STATE.FIX: print C_('(fixing)') os.chmod(private, mode & ~S_IROTH) else: print # In addition, on a multiuser system you may want to hide the private # archives so other users can't read them. if mode & S_IXOTH: print _("""\ Warning: Private archive directory is other-executable (o+x). This could allow other users on your system to read private archives. If you're on a shared multiuser system, you should consult the installation manual on how to fix this.""") MBOXPERMS = S_IRGRP | S_IWGRP | S_IRUSR | S_IWUSR def checkmboxfile(mboxdir): absdir = os.path.join(mm_cfg.PRIVATE_ARCHIVE_FILE_DIR, mboxdir) for f in os.listdir(absdir): if not f.endswith('.mbox'): continue mboxfile = os.path.join(absdir, f) mode = statmode(mboxfile) if (mode & MBOXPERMS) <> MBOXPERMS: STATE.ERRORS = STATE.ERRORS + 1 print C_('mbox file must be at least 0660:'), mboxfile if STATE.FIX: print C_('(fixing)') os.chmod(mboxfile, mode | MBOXPERMS) else: print def checkarchivedbs(): # The archives/private/listname/database file must not be other readable # or executable otherwise those files will be accessible when the archives # are public. That may not be a horrible breach, but let's close this off # anyway. for dir in os.listdir(mm_cfg.PRIVATE_ARCHIVE_FILE_DIR): if dir.endswith('.mbox'): checkmboxfile(dir) dbdir = os.path.join(mm_cfg.PRIVATE_ARCHIVE_FILE_DIR, dir, 'database') try: mode = statmode(dbdir) except OSError, e: if e.errno not in (errno.ENOENT, errno.ENOTDIR): raise continue if mode & S_IRWXO: STATE.ERRORS += 1 print C_('%(dbdir)s "other" perms must be 000'), if STATE.FIX: print C_('(fixing)') os.chmod(dbdir, mode & ~S_IRWXO) else: print def checkcgi(): cgidir = os.path.join(mm_cfg.EXEC_PREFIX, 'cgi-bin') if STATE.VERBOSE: print C_('checking cgi-bin permissions') exes = os.listdir(cgidir) for f in exes: path = os.path.join(cgidir, f) if STATE.VERBOSE: print C_(' checking set-gid for %(path)s') mode = statmode(path) if mode & S_IXGRP and not mode & S_ISGID: STATE.ERRORS += 1 print C_('%(path)s must be set-gid'), if STATE.FIX: print C_('(fixing)') os.chmod(path, mode | S_ISGID) else: print def checkmail(): wrapper = os.path.join(mm_cfg.WRAPPER_DIR, 'mailman') if STATE.VERBOSE: print C_('checking set-gid for %(wrapper)s') mode = statmode(wrapper) if not mode & S_ISGID: STATE.ERRORS += 1 print C_('%(wrapper)s must be set-gid'), if STATE.FIX: print C_('(fixing)') os.chmod(wrapper, mode | S_ISGID) def checkadminpw(): for pwfile in (os.path.join(mm_cfg.DATA_DIR, 'adm.pw'), os.path.join(mm_cfg.DATA_DIR, 'creator.pw')): targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP if STATE.VERBOSE: print C_('checking permissions on %(pwfile)s') try: mode = statmode(pwfile) except OSError, e: if e.errno <> errno.ENOENT: raise return if mode <> targetmode: STATE.ERRORS += 1 octmode = oct(mode) print C_('%(pwfile)s permissions must be exactly 0640 ' '(got %(octmode)s)'), if STATE.FIX: print C_('(fixing)') os.chmod(pwfile, targetmode) else: print def checkmta(): if mm_cfg.MTA: modname = 'Mailman.MTA.' + mm_cfg.MTA __import__(modname) try: sys.modules[modname].checkperms(STATE) except AttributeError: pass def checkdata(): targetmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP checkfiles = ('config.pck', 'config.pck.last', 'config.db', 'config.db.last', 'next-digest', 'next-digest-topics', 'digest.mbox', 'pending.pck', 'request.db', 'request.db.tmp') if STATE.VERBOSE: print C_('checking permissions on list data') # BAW: This needs to be converted to the Site module abstraction for dir in os.listdir(mm_cfg.LIST_DATA_DIR): if not os.path.isdir(os.path.join(mm_cfg.LIST_DATA_DIR, dir)): continue for file in checkfiles: path = os.path.join(mm_cfg.LIST_DATA_DIR, dir, file) if STATE.VERBOSE: print C_(' checking permissions on: %(path)s') try: mode = statmode(path) except OSError, e: if e.errno <> errno.ENOENT: raise continue if (mode & targetmode) <> targetmode: STATE.ERRORS += 1 print C_('file permissions must be at least 660: %(path)s'), if STATE.FIX: print C_('(fixing)') os.chmod(path, mode | targetmode) else: print def usage(code, msg=''): if code: fd = sys.stderr else: fd = sys.stdout print >> fd, C_(__doc__) if msg: print >> fd, msg sys.exit(code) if __name__ == '__main__': try: opts, args = getopt.getopt(sys.argv[1:], 'fvh', ['fix', 'verbose', 'help']) except getopt.error, msg: usage(1, msg) for opt, arg in opts: if opt in ('-h', '--help'): usage(0) elif opt in ('-f', '--fix'): STATE.FIX = True elif opt in ('-v', '--verbose'): STATE.VERBOSE = True checkall() checkarchives() checkarchivedbs() checkcgi() checkmail() checkdata() checkadminpw() checkmta() if not STATE.ERRORS: print C_('No problems found') else: print C_('Problems found:'), STATE.ERRORS print C_('Re-run as %(MAILMAN_USER)s (or root) with -f flag to fix')