Add check-perms script
This commit is contained in:
parent
af2099be02
commit
7f75064348
1 changed files with 419 additions and 0 deletions
419
roles/gitolite/check_fedmsg_hooks/files/check-perms.py
Executable file
419
roles/gitolite/check_fedmsg_hooks/files/check-perms.py
Executable file
|
@ -0,0 +1,419 @@
|
|||
#!/usr/bin/python -tt
|
||||
"""Check permissions of a tree of git repositories, optionally fixing any
|
||||
problems found.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import optparse
|
||||
from stat import *
|
||||
from subprocess import call, PIPE, Popen
|
||||
|
||||
ALL_CHECKS = ['bare', 'shared', 'mail-hook', 'fedmsg-hook', 'perms', 'post-update-hook']
|
||||
DEFAULT_CHECKS = ['bare', 'shared', 'perms', 'post-update-hook']
|
||||
|
||||
OBJECT_RE = re.compile('[0-9a-z]{40}')
|
||||
|
||||
def error(msg):
|
||||
print >> sys.stderr, msg
|
||||
|
||||
def is_object(path):
|
||||
"""Check if a path is a git object."""
|
||||
parts = path.split(os.path.sep)
|
||||
if 'objects' in parts and len(parts) > 2 and \
|
||||
OBJECT_RE.match(''.join(path.split(os.path.sep)[-2:])):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_bare_repo(gitdir):
|
||||
"""Check if a git repository is bare."""
|
||||
cmd = ['git', '--git-dir', gitdir, 'config', '--bool', 'core.bare']
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
bare, error = p.communicate()
|
||||
if bare.rstrip() != 'true' or p.returncode:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_shared_repo(gitdir):
|
||||
"""Check if a git repository is shared."""
|
||||
cmd = ['git', '--git-dir', gitdir, 'config', 'core.sharedRepository']
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
shared, error = p.communicate()
|
||||
sharedmodes = ['1', 'group', 'true', '2', 'all', 'world', 'everybody']
|
||||
if shared.rstrip() not in sharedmodes or p.returncode:
|
||||
return False
|
||||
return True
|
||||
|
||||
def uses_version1_mail_hook(gitdir):
|
||||
"""Check if a git repository uses the old fedora-git-commit-mail-hook."""
|
||||
hook = os.path.join(gitdir, 'hooks/update')
|
||||
oldpath = '/usr/bin/fedora-git-commit-mail-hook'
|
||||
return os.path.realpath(hook) == oldpath
|
||||
|
||||
def uses_version2_mail_hook(gitdir):
|
||||
"""Check if a git repository uses the pre-fedmsg mail-hook setup."""
|
||||
hook = os.path.join(gitdir, 'hooks/post-receive')
|
||||
oldpath = '/usr/share/git-core/mail-hooks/gnome-post-receive-email'
|
||||
return os.path.realpath(hook) == oldpath
|
||||
|
||||
def check_post_update_hook(gitdir, fix=False):
|
||||
"""Check if a repo's post-update hook is setup correctly."""
|
||||
hook = os.path.join(gitdir, 'hooks/post-update')
|
||||
realpath = os.path.realpath(hook)
|
||||
goodpath = '/usr/share/git-core/templates/hooks/post-update.sample'
|
||||
badpath = '/usr/bin/git-update-server-info'
|
||||
|
||||
if realpath == goodpath:
|
||||
return True
|
||||
|
||||
errmsg = ''
|
||||
if realpath == badpath:
|
||||
errmsg = '%s: symlinked to %s' % (hook, badpath)
|
||||
elif not os.path.exists(hook):
|
||||
errmsg = '%s: does not exist' % hook
|
||||
elif not os.access(hook, os.X_OK):
|
||||
errmsg = '%s: is not executable' % hook
|
||||
elif not os.path.islink(hook):
|
||||
errmsg = '%s: not a symlink' % hook
|
||||
else:
|
||||
errmsg = '%s: symlinked to %s' % (hook, realpath)
|
||||
|
||||
error(errmsg)
|
||||
|
||||
if not fix:
|
||||
return False
|
||||
|
||||
if not os.path.exists(goodpath):
|
||||
error('%s: post-update hook (%s) does not exist.' % (gitdir, goodpath))
|
||||
return False
|
||||
|
||||
if os.path.exists(hook):
|
||||
try:
|
||||
os.rename(hook, '%s~' % hook)
|
||||
except (IOError, OSError), err:
|
||||
error('%s: Error renaming %s: %s' % (gitdir, hook, err.strerror))
|
||||
return False
|
||||
try:
|
||||
os.symlink(goodpath, hook)
|
||||
except (IOError, OSError), err:
|
||||
error('%s: Error creating %s symlink: %s' % (gitdir, hook, err.strerror))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def set_bare_repo(gitdir):
|
||||
"""Set core.bare for a git repository."""
|
||||
cmd = ['git', '--git-dir', gitdir, 'config', '--bool', 'core.bare', 'true']
|
||||
ret = call(cmd)
|
||||
if ret:
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_shared_repo(gitdir, value='group'):
|
||||
"""Set core.sharedRepository for a git repository."""
|
||||
mode_re = re.compile('06[0-7]{2}')
|
||||
if value in [0, 'false', 'umask']:
|
||||
value = 'umask'
|
||||
elif value in [1, 'true', 'group']:
|
||||
value = 'group'
|
||||
elif value in [2, 'all', 'world', 'everybody']:
|
||||
value = 'all'
|
||||
elif mode_re.match(value):
|
||||
pass
|
||||
else:
|
||||
raise SystemExit('Bogus core.sharedRepository value "%s"' % value)
|
||||
cmd = ['git', '--git-dir', gitdir, 'config', 'core.sharedRepository',
|
||||
value]
|
||||
ret = call(cmd)
|
||||
if ret:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def set_post_receive_hook_version2(gitdir):
|
||||
"""Configure a git repository to use the gnome mail hook without fedmsg."""
|
||||
|
||||
# Get recipients from the commit-list file.
|
||||
commit_list = os.path.join(gitdir, 'commit-list')
|
||||
if not os.path.exists(commit_list):
|
||||
error('%s: No commit-list file found' % gitdir)
|
||||
return False
|
||||
try:
|
||||
addrs = open(commit_list).read().strip()
|
||||
addrs = ', '.join(addrs.split())
|
||||
except:
|
||||
error('%s: Unable to read commit-list file' % gitdir)
|
||||
return False
|
||||
|
||||
# Set hooks.mailinglist
|
||||
if '@' not in addrs:
|
||||
addrs = '%s@lists.fedorahosted.org'
|
||||
cmd = ['git', '--git-dir', gitdir, 'config', 'hooks.mailinglist', addrs]
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode:
|
||||
error('%s: Error setting hooks.mailinglist: %s' % (gitdir, stderr))
|
||||
return False
|
||||
|
||||
# Set hooks.maildomain
|
||||
cmd = ['git', '--git-dir', gitdir, 'config', 'hooks.maildomain',
|
||||
'fedoraproject.org']
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode:
|
||||
error('%s: Error setting hooks.maildomain: %s' % (gitdir, stderr))
|
||||
return False
|
||||
|
||||
# Symlink mail notification script to post-receive hook
|
||||
script = '/usr/share/git-core/mail-hooks/gnome-post-receive-email'
|
||||
if not os.path.exists(script):
|
||||
error('%s: Mail hook (%s) does not exist.' % (gitdir, script))
|
||||
return False
|
||||
|
||||
hook = os.path.join(gitdir, 'hooks', 'post-receive')
|
||||
if os.path.exists(hook):
|
||||
try:
|
||||
os.remove(hook)
|
||||
except Exception, e:
|
||||
errstr = hasattr(e, 'strerror') and e.strerror or e
|
||||
error('%s: Error removing %s: %s' % (gitdir, hook, errstr))
|
||||
return False
|
||||
try:
|
||||
os.symlink(script, hook)
|
||||
except Exception, e:
|
||||
errstr = hasattr(e, 'strerror') and e.strerror or e
|
||||
error('%s: Error creating %s symlink: %s' % (gitdir, hook, errstr))
|
||||
return False
|
||||
|
||||
# Clean up commit-list file and old update hook link
|
||||
try:
|
||||
os.rename(commit_list, '%s~' % commit_list)
|
||||
except (IOError, OSError), err:
|
||||
error('%s: Unable to backup commit-list: %s' % (gitdir, err.strerror))
|
||||
return False
|
||||
try:
|
||||
oldhook = os.path.join(gitdir, 'hooks/update')
|
||||
os.remove(oldhook)
|
||||
except (IOError, OSError), err:
|
||||
error('%s: Unable to backup commit-list: %s' % (gitdir, err.strerror))
|
||||
return False
|
||||
|
||||
# We ran the gauntlet.
|
||||
return True
|
||||
|
||||
|
||||
def set_post_receive_hook_version3(gitdir):
|
||||
"""Configure a git repository to use the fedmsg+gnome-mail hooks."""
|
||||
|
||||
if not uses_version2_mail_hook(gitdir):
|
||||
error('%s: Not yet on version2 mail hook; do --fix=mail-hook' % gitdir)
|
||||
return False
|
||||
|
||||
# Check that the destination is 'okay'
|
||||
dest_prefix = os.path.join(gitdir, 'hooks', 'post-receive-chained.d')
|
||||
|
||||
if not os.path.exists(dest_prefix):
|
||||
os.mkdir(dest_prefix)
|
||||
|
||||
if not os.path.isdir(dest_prefix):
|
||||
error('%s: %s is not a directory.' % (gitdir, dest_prefix))
|
||||
return False
|
||||
|
||||
# Symlink mail notification and fedmsg scripts to post-receive hook
|
||||
scripts = {
|
||||
'/usr/share/git-core/mail-hooks/gnome-post-receive-email':
|
||||
os.path.join(dest_prefix, 'post-receive-email'),
|
||||
'/usr/share/git-core/post-receive-fedmsg':
|
||||
os.path.join(dest_prefix, 'post-receive-fedmsg'),
|
||||
|
||||
# This one kicks off all the others.
|
||||
'/usr/share/git-core/post-receive-chained':
|
||||
os.path.join(gitdir, 'hooks', 'post-receive'),
|
||||
|
||||
}
|
||||
|
||||
for script, hook in scripts.items():
|
||||
if not os.path.exists(script):
|
||||
error('%s: Hook (%s) does not exist.' % (gitdir, script))
|
||||
return False
|
||||
|
||||
if os.path.exists(hook):
|
||||
try:
|
||||
os.remove(hook)
|
||||
except Exception, e:
|
||||
errstr = hasattr(e, 'strerror') and e.strerror or e
|
||||
error('%s: Error removing %s: %s' % (gitdir, hook, errstr))
|
||||
return False
|
||||
try:
|
||||
os.symlink(script, hook)
|
||||
except Exception, e:
|
||||
errstr = hasattr(e, 'strerror') and e.strerror or e
|
||||
error('%s: Error creating %s symlink: %s' % (gitdir, hook, errstr))
|
||||
return False
|
||||
|
||||
# We ran the gauntlet.
|
||||
return True
|
||||
|
||||
|
||||
def list_checks():
|
||||
print 'Available checks: %s' % ', '.join(ALL_CHECKS)
|
||||
print 'Default checks: %s' % ', '.join(DEFAULT_CHECKS)
|
||||
|
||||
|
||||
def check_git_perms(path, fix=False):
|
||||
"""Check if permissions on a git repo are correct.
|
||||
|
||||
If fix is true, problems found are corrected.
|
||||
"""
|
||||
object_mode = S_IRUSR | S_IRGRP | S_IROTH
|
||||
oldmode = mode = S_IMODE(os.lstat(path)[ST_MODE])
|
||||
errors = []
|
||||
if os.path.isdir(path):
|
||||
newmode = mode | S_ISGID
|
||||
if mode != newmode:
|
||||
msg = 'Not SETGID (should be "%s")' % oct(newmode)
|
||||
errors.append(msg)
|
||||
mode = newmode
|
||||
elif is_object(path) and mode ^ object_mode:
|
||||
msg = 'Wrong object mode "%s" (should be "%s")' % (
|
||||
oct(mode), oct(object_mode))
|
||||
errors.append(msg)
|
||||
mode = object_mode
|
||||
if mode & S_IWUSR and not is_object(path):
|
||||
newmode = mode | S_IWGRP
|
||||
exempt = \
|
||||
any(map(path.endswith, ['commit-list', 'gl-conf'])) or \
|
||||
any(map(path.__contains__, ['/hooks/']))
|
||||
|
||||
if mode != newmode and not exempt:
|
||||
msg = 'Not group writable (should be "%s")' % oct(newmode)
|
||||
errors.append(msg)
|
||||
mode = newmode
|
||||
if mode != oldmode and not os.path.islink(path):
|
||||
errmsg = '%s:' % path
|
||||
errmsg += ', '.join(['%s' % e for e in errors])
|
||||
error(errmsg)
|
||||
if not fix:
|
||||
return False
|
||||
try:
|
||||
os.chmod(path, mode)
|
||||
return True
|
||||
except Exception, e:
|
||||
errstr = hasattr(e, 'strerror') and e.strerror or e
|
||||
mode = oct(mode)
|
||||
error('%s: Error setting "%s" mode on %s: %s' % (gitdir,
|
||||
mode, path, errstr))
|
||||
return False
|
||||
return True
|
||||
|
||||
def main():
|
||||
usage = '%prog [options] [gitroot]'
|
||||
parser = optparse.OptionParser(usage=usage)
|
||||
parser.add_option('-f', '--fix', action='store_true', default=False,
|
||||
help='Correct any problems [%default]')
|
||||
parser.add_option('-l', '--list-checks', action='store_true',
|
||||
help='List default checks')
|
||||
parser.add_option('-c', '--check', dest='checks', action='append',
|
||||
default=[], metavar='check',
|
||||
help='Add a check, may be used multiple times')
|
||||
parser.add_option('-s', '--skip', action='append', default=[],
|
||||
metavar='check',
|
||||
help='Skip a check, may be used multiple times')
|
||||
parser.add_option('-r', '--repo', default=None,
|
||||
help="Check only a certain repo, not all of them.")
|
||||
opts, args = parser.parse_args()
|
||||
|
||||
# Check options
|
||||
if opts.list_checks:
|
||||
list_checks()
|
||||
raise SystemExit
|
||||
|
||||
if opts.checks:
|
||||
checks = set(opts.checks)
|
||||
bad_check_opts = checks.difference(set(ALL_CHECKS))
|
||||
if bad_check_opts:
|
||||
msg = 'Bad check(s): %s' % ', '.join(sorted(bad_check_opts))
|
||||
msg += '\nAvailable checks: %s' % ', '.join(ALL_CHECKS)
|
||||
raise SystemExit(msg)
|
||||
else:
|
||||
bad_skip_opts = set(opts.skip).difference(set(ALL_CHECKS))
|
||||
if bad_skip_opts:
|
||||
msg = 'Bad skip option(s): %s' % ', '.join(sorted(bad_skip_opts))
|
||||
msg += '\nAvailable checks: %s' % ', '.join(ALL_CHECKS)
|
||||
raise SystemExit(msg)
|
||||
checks = set()
|
||||
for check in DEFAULT_CHECKS:
|
||||
if check not in opts.skip:
|
||||
checks.add(check)
|
||||
|
||||
# Check args
|
||||
if len(args) > 1:
|
||||
raise SystemExit(parser.get_usage().strip())
|
||||
|
||||
gitroot = args and args[0] or '/git'
|
||||
|
||||
if not os.path.isdir(gitroot):
|
||||
raise SystemExit('%s does not exist or is not a directory' % gitroot)
|
||||
|
||||
if opts.repo:
|
||||
gitdirs = ['/'.join([gitroot, opts.repo])]
|
||||
else:
|
||||
gitdirs = []
|
||||
for path, dirs, files in os.walk(gitroot):
|
||||
if path in gitdirs:
|
||||
continue
|
||||
if 'description' in os.listdir(path):
|
||||
gitdirs.append(path)
|
||||
|
||||
problems = []
|
||||
for gitdir in sorted(gitdirs):
|
||||
if 'bare' in checks and not is_bare_repo(gitdir):
|
||||
error('%s: core.bare not true' % gitdir)
|
||||
if not opts.fix or not set_bare_repo(gitdir):
|
||||
problems.append(gitdir)
|
||||
if 'shared' in checks and not is_shared_repo(gitdir):
|
||||
error('%s: core.sharedRepository not set' % gitdir)
|
||||
if not opts.fix or not set_shared_repo(gitdir):
|
||||
problems.append(gitdir)
|
||||
|
||||
if 'mail-hook' in checks and uses_version1_mail_hook(gitdir):
|
||||
error('%s: uses old mail hook' % gitdir)
|
||||
if not opts.fix or not set_post_receive_hook_version2(gitdir):
|
||||
problems.append(gitdir)
|
||||
|
||||
if 'fedmsg-hook' in checks and (uses_version1_mail_hook(gitdir) or
|
||||
uses_version2_mail_hook(gitdir)):
|
||||
error('%s: uses the gnome mail hook or older' % gitdir)
|
||||
if not opts.fix or not set_post_receive_hook_version3(gitdir):
|
||||
problems.append(gitdir)
|
||||
|
||||
if 'post-update-hook' in checks and not check_post_update_hook(gitdir,
|
||||
opts.fix):
|
||||
problems.append(gitdir)
|
||||
|
||||
if 'perms' in checks:
|
||||
paths = []
|
||||
for path, dirs, files in os.walk(gitdir):
|
||||
for d in dirs:
|
||||
d = os.path.join(path, d)
|
||||
if d not in paths:
|
||||
paths.append(d)
|
||||
for f in files:
|
||||
f = os.path.join(path, f)
|
||||
if f not in paths:
|
||||
paths.append(f)
|
||||
for path in paths:
|
||||
if not check_git_perms(path, fix=opts.fix):
|
||||
problems.append(path)
|
||||
|
||||
if problems:
|
||||
raise SystemExit('%d problems remain unfixed' % len(problems))
|
||||
|
||||
raise SystemExit()
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
raise SystemExit('\nExiting on user cancel (Ctrl-C)')
|
Loading…
Add table
Add a link
Reference in a new issue