Apply 52_check_perms_lstat.patch
[mspang/vmailman.git] / bin / check_perms
1 #! @PYTHON@
2 #
3 # Copyright (C) 1998-2005 by the Free Software Foundation, Inc.
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
19 """Check the permissions for the Mailman installation.
20
21 Usage: %(PROGRAM)s [-f] [-v] [-h]
22
23 With no arguments, just check and report all the files that have bogus
24 permissions or group ownership.  With -f (and run as root), fix all the
25 permission problems found.  With -v be verbose.
26 """
27
28 import os
29 import sys
30 import pwd
31 import grp
32 import errno
33 import getopt
34 from stat import *
35
36 try:
37     import paths
38 except ImportError:
39     print '''Could not import paths!
40
41 This probably means that you are trying to run check_perms from the source
42 directory.  You must run this from the installation directory instead.
43 '''
44     raise
45 from Mailman import mm_cfg
46 from Mailman.mm_cfg import MAILMAN_USER, MAILMAN_GROUP
47 from Mailman.i18n import _
48
49 # Let KeyErrors percolate
50 MAILMAN_GID = grp.getgrnam(MAILMAN_GROUP)[2]
51 MAILMAN_UID = pwd.getpwnam(MAILMAN_USER)[2]
52
53 PROGRAM = sys.argv[0]
54
55 # Gotta check the archives/private/*/database/* files
56
57 try:
58     True, False
59 except NameError:
60     True = 1
61     False = 0
62
63
64 \f
65 class State:
66     FIX = False
67     VERBOSE = False
68     ERRORS = 0
69
70 STATE = State()
71
72 DIRPERMS = S_ISGID | S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH
73 QFILEPERMS = S_ISGID | S_IRWXU | S_IRWXG
74 PYFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
75 ARTICLEFILEPERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
76
77
78 \f
79 def statmode(path):
80     return os.stat(path)[ST_MODE]
81
82 def statgidmode(path):
83     stat = os.lstat(path)
84     return stat[ST_MODE], stat[ST_GID]
85
86 seen = {}
87
88 # libc's getgrgid re-opens /etc/group each time :(
89 _gidcache = {}
90
91 def getgrgid(gid):
92     data = _gidcache.get(gid)
93     if data is None:
94         data = grp.getgrgid(gid)
95         _gidcache[gid] = data
96     return data
97
98
99 \f
100 def checkwalk(arg, dirname, names):
101     # Short-circuit duplicates
102     if seen.has_key(dirname):
103         return
104     seen[dirname] = True
105     for name in names:
106         path = os.path.join(dirname, name)
107         if arg.VERBOSE:
108             print _('    checking gid and mode for %(path)s')
109         try:
110             mode, gid = statgidmode(path)
111         except OSError, e:
112             if e.errno <> errno.ENOENT: raise
113             continue
114         if gid <> MAILMAN_GID:
115             try:
116                 groupname = getgrgid(gid)[0]
117             except KeyError:
118                 groupname = '<anon gid %d>' % gid
119             arg.ERRORS += 1
120             print _('%(path)s bad group (has: %(groupname)s, '
121                     'expected %(MAILMAN_GROUP)s)'),
122             if STATE.FIX:
123                 print _('(fixing)')
124                 os.chown(path, -1, MAILMAN_GID)
125             else:
126                 print
127         # all directories must be at least rwxrwsr-x.  Don't check the private
128         # archive directory or database directory themselves since these are
129         # checked in checkarchives() and checkarchivedbs() below.
130         private = mm_cfg.PRIVATE_ARCHIVE_FILE_DIR
131         if path == private or (os.path.commonprefix((path, private)) == private
132                                and os.path.split(path)[1] == 'database'):
133             continue
134         # The directories under qfiles should have a more limited permission
135         if os.path.commonprefix((path, mm_cfg.QUEUE_DIR)) == mm_cfg.QUEUE_DIR:
136             targetperms = QFILEPERMS
137             octperms = oct(targetperms)
138         else:
139             targetperms = DIRPERMS
140             octperms = oct(targetperms)
141         if S_ISDIR(mode) and (mode & targetperms) <> targetperms:
142             arg.ERRORS += 1
143             print _('directory permissions must be %(octperms)s: %(path)s'),
144             if STATE.FIX:
145                 print _('(fixing)')
146                 os.chmod(path, mode | targetperms)
147             else:
148                 print
149         elif os.path.splitext(path)[1] in ('.py', '.pyc', '.pyo'):
150             octperms = oct(PYFILEPERMS)
151             if mode & PYFILEPERMS <> PYFILEPERMS:
152                 print _('source perms must be %(octperms)s: %(path)s'),
153                 arg.ERRORS += 1
154                 if STATE.FIX:
155                     print _('(fixing)')
156                     os.chmod(path, mode | PYFILEPERMS)
157                 else:
158                     print
159         elif path.endswith('-article'):
160             # Article files must be group writeable
161             octperms = oct(ARTICLEFILEPERMS)
162             if mode & ARTICLEFILEPERMS <> ARTICLEFILEPERMS:
163                 print _('article db files must be %(octperms)s: %(path)s'),
164                 arg.ERRORS += 1
165                 if STATE.FIX:
166                     print _('(fixing)')
167                     os.chmod(path, mode | ARTICLEFILEPERMS)
168                 else:
169                     print
170
171 def checkall():
172     # first check PREFIX
173     if STATE.VERBOSE:
174         prefix = mm_cfg.PREFIX
175         print _('checking mode for %(prefix)s')
176     dirs = {}
177     for d in (mm_cfg.PREFIX, mm_cfg.EXEC_PREFIX, mm_cfg.VAR_PREFIX,
178               mm_cfg.LOG_DIR):
179         dirs[d] = True
180     for d in dirs.keys():
181         try:
182             mode = statmode(d)
183         except OSError, e:
184             if e.errno <> errno.ENOENT: raise
185             print _('WARNING: directory does not exist: %(d)s')
186             continue
187         if (mode & DIRPERMS) <> DIRPERMS:
188             STATE.ERRORS += 1
189             print _('directory must be at least 02775: %(d)s'),
190             if STATE.FIX:
191                 print _('(fixing)')
192                 os.chmod(d, mode | DIRPERMS)
193             else:
194                 print
195         # check all subdirs
196         os.path.walk(d, checkwalk, STATE)
197
198 def checkarchives():
199     private = mm_cfg.PRIVATE_ARCHIVE_FILE_DIR
200     if STATE.VERBOSE:
201         print _('checking perms on %(private)s')
202     # private archives must not be other readable
203     mode = statmode(private)
204     if mode & S_IROTH:
205         STATE.ERRORS += 1
206         print _('%(private)s must not be other-readable'),
207         if STATE.FIX:
208             print _('(fixing)')
209             os.chmod(private, mode & ~S_IROTH)
210         else:
211             print
212     # In addition, on a multiuser system you may want to hide the private
213     # archives so other users can't read them.
214     if mode & S_IXOTH:
215         print _("""\
216 Warning: Private archive directory is other-executable (o+x).
217          This could allow other users on your system to read private archives.
218          If you're on a shared multiuser system, you should consult the
219          installation manual on how to fix this.""")
220
221 MBOXPERMS = S_IRGRP | S_IWGRP | S_IRUSR | S_IWUSR
222
223 def checkmboxfile(mboxdir):
224     absdir = os.path.join(mm_cfg.PRIVATE_ARCHIVE_FILE_DIR, mboxdir)
225     for f in os.listdir(absdir):
226         if not f.endswith('.mbox'):
227             continue
228         mboxfile = os.path.join(absdir, f)
229         mode = statmode(mboxfile)
230         if (mode & MBOXPERMS) <> MBOXPERMS:
231             STATE.ERRORS = STATE.ERRORS + 1
232             print _('mbox file must be at least 0660:'), mboxfile
233             if STATE.FIX:
234                 print _('(fixing)')
235                 os.chmod(mboxfile, mode | MBOXPERMS)
236             else:
237                 print
238
239 def checkarchivedbs():
240     # The archives/private/listname/database file must not be other readable
241     # or executable otherwise those files will be accessible when the archives
242     # are public.  That may not be a horrible breach, but let's close this off
243     # anyway.
244     for dir in os.listdir(mm_cfg.PRIVATE_ARCHIVE_FILE_DIR):
245         if dir.endswith('.mbox'):
246             checkmboxfile(dir)
247         dbdir = os.path.join(mm_cfg.PRIVATE_ARCHIVE_FILE_DIR, dir, 'database')
248         try:
249             mode = statmode(dbdir)
250         except OSError, e:
251             if e.errno not in (errno.ENOENT, errno.ENOTDIR): raise
252             continue
253         if mode & S_IRWXO:
254             STATE.ERRORS += 1
255             print _('%(dbdir)s "other" perms must be 000'),
256             if STATE.FIX:
257                 print _('(fixing)')
258                 os.chmod(dbdir, mode & ~S_IRWXO)
259             else:
260                 print
261
262 def checkcgi():
263     cgidir = os.path.join(mm_cfg.EXEC_PREFIX, 'cgi-bin')
264     if STATE.VERBOSE:
265         print _('checking cgi-bin permissions')
266     exes = os.listdir(cgidir)
267     for f in exes:
268         path = os.path.join(cgidir, f)
269         if STATE.VERBOSE:
270             print _('    checking set-gid for %(path)s')
271         mode = statmode(path)
272         if mode & S_IXGRP and not mode & S_ISGID:
273             STATE.ERRORS += 1
274             print _('%(path)s must be set-gid'),
275             if STATE.FIX:
276                 print _('(fixing)')
277                 os.chmod(path, mode | S_ISGID)
278             else:
279                 print
280
281 def checkmail():
282     wrapper = os.path.join(mm_cfg.WRAPPER_DIR, 'mailman')
283     if STATE.VERBOSE:
284         print _('checking set-gid for %(wrapper)s')
285     mode = statmode(wrapper)
286     if not mode & S_ISGID:
287         STATE.ERRORS += 1
288         print _('%(wrapper)s must be set-gid'),
289         if STATE.FIX:
290             print _('(fixing)')
291             os.chmod(wrapper, mode | S_ISGID)
292
293 def checkadminpw():
294     for pwfile in (os.path.join(mm_cfg.DATA_DIR, 'adm.pw'),
295                    os.path.join(mm_cfg.DATA_DIR, 'creator.pw')):
296         targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP
297         if STATE.VERBOSE:
298             print _('checking permissions on %(pwfile)s')
299         try:
300             mode = statmode(pwfile)
301         except OSError, e:
302             if e.errno <> errno.ENOENT: raise
303             return
304         if mode <> targetmode:
305             STATE.ERRORS += 1
306             octmode = oct(mode)
307             print _('%(pwfile)s permissions must be exactly 0640 '
308                     '(got %(octmode)s)'),
309             if STATE.FIX:
310                 print _('(fixing)')
311                 os.chmod(pwfile, targetmode)
312             else:
313                 print
314
315 def checkmta():
316     if mm_cfg.MTA:
317         modname = 'Mailman.MTA.' + mm_cfg.MTA
318         __import__(modname)
319         try:
320             sys.modules[modname].checkperms(STATE)
321         except AttributeError:
322             pass
323
324 def checkdata():
325     targetmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
326     checkfiles = ('config.pck', 'config.pck.last',
327                   'config.db', 'config.db.last',
328                   'next-digest', 'next-digest-topics',
329                   'request.db', 'request.db.tmp')
330     if STATE.VERBOSE:
331         print _('checking permissions on list data')
332     # BAW: This needs to be converted to the Site module abstraction
333     for dir in os.listdir(mm_cfg.LIST_DATA_DIR):
334         for file in checkfiles:
335             path = os.path.join(mm_cfg.LIST_DATA_DIR, dir, file)
336             if STATE.VERBOSE:
337                 print _('    checking permissions on: %(path)s')
338             try:
339                 mode = statmode(path)
340             except OSError, e:
341                 if e.errno <> errno.ENOENT: raise
342                 continue
343             if (mode & targetmode) <> targetmode:
344                 STATE.ERRORS += 1
345                 print _('file permissions must be at least 660: %(path)s'),
346                 if STATE.FIX:
347                     print _('(fixing)')
348                     os.chmod(path, mode | targetmode)
349                 else:
350                     print
351
352
353 \f
354 def usage(code, msg=''):
355     if code:
356         fd = sys.stderr
357     else:
358         fd = sys.stdout
359     print >> fd, _(__doc__)
360     if msg:
361         print >> fd, msg
362     sys.exit(code)
363
364
365 if __name__ == '__main__':
366     try:
367         opts, args = getopt.getopt(sys.argv[1:], 'fvh',
368                                    ['fix', 'verbose', 'help'])
369     except getopt.error, msg:
370         usage(1, msg)
371
372     for opt, arg in opts:
373         if opt in ('-h', '--help'):
374             usage(0)
375         elif opt in ('-f', '--fix'):
376             STATE.FIX = True
377         elif opt in ('-v', '--verbose'):
378             STATE.VERBOSE = True
379
380     checkall()
381     checkarchives()
382     checkarchivedbs()
383     checkcgi()
384     checkmail()
385     checkdata()
386     checkadminpw()
387     checkmta()
388
389     if not STATE.ERRORS:
390         print _('No problems found')
391     else:
392         print _('Problems found:'), STATE.ERRORS
393         print _('Re-run as %(MAILMAN_USER)s (or root) with -f flag to fix')