summaryrefslogtreecommitdiff
path: root/Mailman/bin/check_perms.py
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/bin/check_perms.py')
-rwxr-xr-xMailman/bin/check_perms.py412
1 files changed, 412 insertions, 0 deletions
diff --git a/Mailman/bin/check_perms.py b/Mailman/bin/check_perms.py
new file mode 100755
index 000000000..c56171b49
--- /dev/null
+++ b/Mailman/bin/check_perms.py
@@ -0,0 +1,412 @@
+# Copyright (C) 1998-2006 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 optparse
+
+from stat import *
+
+from Mailman import Version
+from Mailman.configuration import config
+from Mailman.i18n import _
+
+__i18n_templates__ = True
+
+
+# XXX Need to check the archives/private/*/database/* files
+
+
+
+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
+MBOXPERMS = S_IRGRP | S_IWGRP | S_IRUSR | S_IWUSR
+
+
+
+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 _(' checking gid and mode for $path')
+ 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 = '<anon gid %d>' % gid
+ arg.ERRORS += 1
+ print _(
+ '$path bad group (has: $groupname, expected $MAILMAN_GROUP)'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chown(path, -1, MAILMAN_GID)
+ else:
+ print
+ # All directories must be at least rwxrwsr-x. Don't check the private
+ # archive directory or database directory themselves since these are
+ # checked in checkarchives() and checkarchivedbs() below.
+ private = config.PRIVATE_ARCHIVE_FILE_DIR
+ if path == private or (os.path.commonprefix((path, private)) == private
+ and os.path.split(path)[1] == 'database'):
+ continue
+ # The directories under qfiles should have a more limited permission
+ if os.path.commonprefix((path, config.QUEUE_DIR)) == config.QUEUE_DIR:
+ targetperms = QFILEPERMS
+ octperms = oct(targetperms)
+ else:
+ targetperms = DIRPERMS
+ octperms = oct(targetperms)
+ if S_ISDIR(mode) and (mode & targetperms) <> targetperms:
+ arg.ERRORS += 1
+ print _('directory permissions must be $octperms: $path'),
+ if STATE.FIX:
+ print _('(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 _('source perms must be $octperms: $path'),
+ arg.ERRORS += 1
+ if STATE.FIX:
+ print _('(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 _('article db files must be $octperms: $path'),
+ arg.ERRORS += 1
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | ARTICLEFILEPERMS)
+ else:
+ print
+
+
+
+def checkall():
+ # first check PREFIX
+ if STATE.VERBOSE:
+ prefix = config.PREFIX
+ print _('checking mode for $prefix')
+ dirs = {}
+ for d in (config.PREFIX, config.EXEC_PREFIX, config.VAR_PREFIX,
+ config.LOG_DIR):
+ dirs[d] = True
+ for d in dirs.keys():
+ try:
+ mode = statmode(d)
+ except OSError, e:
+ if e.errno <> errno.ENOENT: raise
+ print _('WARNING: directory does not exist: $d')
+ continue
+ if (mode & DIRPERMS) <> DIRPERMS:
+ STATE.ERRORS += 1
+ print _('directory must be at least 02775: $d'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(d, mode | DIRPERMS)
+ else:
+ print
+ # check all subdirs
+ os.path.walk(d, checkwalk, STATE)
+
+
+
+def checkarchives():
+ private = config.PRIVATE_ARCHIVE_FILE_DIR
+ if STATE.VERBOSE:
+ print _('checking perms on $private')
+ # private archives must not be other readable
+ mode = statmode(private)
+ if mode & S_IROTH:
+ STATE.ERRORS += 1
+ print _('$private must not be other-readable'),
+ if STATE.FIX:
+ print _('(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.""")
+
+
+
+def checkmboxfile(mboxdir):
+ absdir = os.path.join(config.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 _('mbox file must be at least 0660:'), mboxfile
+ if STATE.FIX:
+ print _('(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(config.PRIVATE_ARCHIVE_FILE_DIR):
+ if dir.endswith('.mbox'):
+ checkmboxfile(dir)
+ dbdir = os.path.join(config.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 _('$dbdir "other" perms must be 000'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(dbdir, mode & ~S_IRWXO)
+ else:
+ print
+
+
+
+def checkcgi():
+ cgidir = os.path.join(config.EXEC_PREFIX, 'cgi-bin')
+ if STATE.VERBOSE:
+ print _('checking cgi-bin permissions')
+ exes = os.listdir(cgidir)
+ for f in exes:
+ path = os.path.join(cgidir, f)
+ if STATE.VERBOSE:
+ print _(' checking set-gid for $path')
+ mode = statmode(path)
+ if mode & S_IXGRP and not mode & S_ISGID:
+ STATE.ERRORS += 1
+ print _('$path must be set-gid'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | S_ISGID)
+ else:
+ print
+
+
+
+def checkmail():
+ wrapper = os.path.join(config.WRAPPER_DIR, 'mailman')
+ if STATE.VERBOSE:
+ print _('checking set-gid for $wrapper')
+ mode = statmode(wrapper)
+ if not mode & S_ISGID:
+ STATE.ERRORS += 1
+ print _('$wrapper must be set-gid'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(wrapper, mode | S_ISGID)
+
+
+
+def checkadminpw():
+ for pwfile in (os.path.join(config.DATA_DIR, 'adm.pw'),
+ os.path.join(config.DATA_DIR, 'creator.pw')):
+ targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP
+ if STATE.VERBOSE:
+ print _('checking permissions on $pwfile')
+ try:
+ mode = statmode(pwfile)
+ except OSError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ return
+ if mode <> targetmode:
+ STATE.ERRORS += 1
+ octmode = oct(mode)
+ print _('$pwfile permissions must be exactly 0640 (got $octmode)'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(pwfile, targetmode)
+ else:
+ print
+
+
+def checkmta():
+ if config.MTA:
+ modname = 'Mailman.MTA.' + config.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',
+ 'request.db', 'request.db.tmp')
+ if STATE.VERBOSE:
+ print _('checking permissions on list data')
+ for dir in os.listdir(config.LIST_DATA_DIR):
+ for file in checkfiles:
+ path = os.path.join(config.LIST_DATA_DIR, dir, file)
+ if STATE.VERBOSE:
+ print _(' checking permissions on: $path')
+ try:
+ mode = statmode(path)
+ except OSError, e:
+ if e.errno <> errno.ENOENT:
+ raise
+ continue
+ if (mode & targetmode) <> targetmode:
+ STATE.ERRORS += 1
+ print _('file permissions must be at least 660: $path'),
+ if STATE.FIX:
+ print _('(fixing)')
+ os.chmod(path, mode | targetmode)
+ else:
+ print
+
+
+
+def parseargs():
+ parser = optparse.OptionParser(version=Version.MAILMAN_VERSION,
+ usage=_("""\
+%prog [options]
+
+Check the permissions of all Mailman files. With no options, just report the
+permission and ownership problems found."""))
+ parser.add_option('-f', '--fix',
+ default=False, action='store_true',
+ help=_("""\
+Fix all permission and ownership problems found. With this option, you must
+run check_perms as root."""))
+ parser.add_option('-v', '--verbose',
+ default=False, action='store_true',
+ help=_('Produce more verbose output'))
+ parser.add_option('-C', '--config',
+ help=_('Alternative configuration file to use'))
+ opts, args = parser.parse_args()
+ if args:
+ parser.print_help()
+ print >> sys.stderr, _('Unexpected arguments')
+ sys.exit(1)
+ return parser, opts, args
+
+
+
+def main():
+ global MAILMAN_USER, MAILMAN_GROUP, MAILMAN_UID, MAILMAN_GID
+
+ parser, opts, args = parseargs()
+ STATE.FIX = opts.fix
+ STATE.VERBOSE = opts.verbose
+
+ config.load(opts.config)
+
+ MAILMAN_USER = config.MAILMAN_USER
+ MAILMAN_GROUP = config.MAILMAN_GROUP
+ # Let KeyErrors percolate
+ MAILMAN_GID = grp.getgrnam(MAILMAN_GROUP).gr_gid
+ MAILMAN_UID = pwd.getpwnam(MAILMAN_USER).pw_uid
+
+ checkall()
+ checkarchives()
+ checkarchivedbs()
+ checkcgi()
+ checkmail()
+ checkdata()
+ checkadminpw()
+ checkmta()
+
+ if not STATE.ERRORS:
+ print _('No problems found')
+ else:
+ print _('Problems found:'), STATE.ERRORS
+ print _('Re-run as $MAILMAN_USER (or root) with -f flag to fix')
+
+
+if __name__ == '__main__':
+ main()