#! @PYTHON@
#
# Copyright (C) 1998-2007 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 _
# 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 _(' 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 = '<anon gid %d>' % gid
arg.ERRORS += 1
print _('%(path)s bad group (has: %(groupname)s, '
'expected %(MAILMAN_GROUP)s)'),
if STATE.FIX:
print _('(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'):
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 _('directory permissions must be %(octperms)s: %(path)s'),
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)s: %(path)s'),
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)s: %(path)s'),
arg.ERRORS += 1
if STATE.FIX:
print _('(fixing)')
os.chmod(path, mode | ARTICLEFILEPERMS)
else:
print
def checkall():
# first check PREFIX
if STATE.VERBOSE:
prefix = mm_cfg.PREFIX
print _('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 _('WARNING: directory does not exist: %(d)s')
continue
if (mode & DIRPERMS) <> DIRPERMS:
STATE.ERRORS += 1
print _('directory must be at least 02775: %(d)s'),
if STATE.FIX:
print _('(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 _('checking perms on %(private)s')
# private archives must not be other readable
mode = statmode(private)
if mode & S_IROTH:
STATE.ERRORS += 1
print _('%(private)s 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.""")
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 _('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(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 _('%(dbdir)s "other" perms must be 000'),
if STATE.FIX:
print _('(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 _('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)s')
mode = statmode(path)
if mode & S_IXGRP and not mode & S_ISGID:
STATE.ERRORS += 1
print _('%(path)s must be set-gid'),
if STATE.FIX:
print _('(fixing)')
os.chmod(path, mode | S_ISGID)
else:
print
def checkmail():
wrapper = os.path.join(mm_cfg.WRAPPER_DIR, 'mailman')
if STATE.VERBOSE:
print _('checking set-gid for %(wrapper)s')
mode = statmode(wrapper)
if not mode & S_ISGID:
STATE.ERRORS += 1
print _('%(wrapper)s must be set-gid'),
if STATE.FIX:
print _('(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 _('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 _('%(pwfile)s permissions must be exactly 0640 '
'(got %(octmode)s)'),
if STATE.FIX:
print _('(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',
'request.db', 'request.db.tmp')
if STATE.VERBOSE:
print _('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):
for file in checkfiles:
path = os.path.join(mm_cfg.LIST_DATA_DIR, dir, file)
if STATE.VERBOSE:
print _(' 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 _('file permissions must be at least 660: %(path)s'),
if STATE.FIX:
print _('(fixing)')
os.chmod(path, mode | targetmode)
else:
print
def usage(code, msg=''):
if code:
fd = sys.stderr
else:
fd = sys.stdout
print >> fd, _(__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 _('No problems found')
else:
print _('Problems found:'), STATE.ERRORS
print _('Re-run as %(MAILMAN_USER)s (or root) with -f flag to fix')