In my previous post i talked about how to easily manage write permissions on a git
repository.
As i said, the script could easily be extented to provide more granularity.
Here comes an improved version!
Features:
- Support groups
- Support nested groups
- Let's you name the admin group
- Let's you give full rights to admin group
- Let's you give user the right to push to his/her own branch - branch named after user name
Here's an example git.acl
:
[global]
# Name of the admin group
admin_group = admin
# Is admin group able to push everything? 0|1
full_admin = 1
# Is user able to push in his named branch? 0|1
owner_branch = 1
[groups]
admin = olivier joe
commiters = @admin jane jim
project1 = @admin @commiters @project2
project2 = @admin john mickael
[branches]
master = @admin
project1 = @project1
test_branch = sam
And here is the updated update
hook:
#!/bin/env python2
import os
import sys
import subprocess
import ConfigParser
#from pprint import pprint
# Exit on error
def error_exit(string):
print string
sys.exit(1)
# Exit on success
def ok_exit(string):
print string
sys.exit(0)
# Recursively get all users from a group
def expand_nested_groups(groups, group, user, parsed_groups=[]):
parsed_groups.append(group)
if user.startswith('@'):
group_name = user[1:]
if group_name in parsed_groups:
return []
res = set()
for u in groups[group_name]:
res.update(expand_nested_groups(
groups,
group_name,
u,
parsed_groups))
return res
return [user]
# Verify that acl_file exists before reading it
def _config():
# Fix acl file path if needed
exec_path = os.getcwd()
if '/hooks/' in exec_path:
acl_file = './git.acl'
else:
acl_file = './hooks/git.acl'
# Open and read acl file if exists
if os.path.isfile(acl_file):
config = ConfigParser.RawConfigParser()
if not config.read(acl_file):
error_exit(acl_file+" doesn't seem to be a valid config file!")
return config
else:
error_exit(acl_file+" is missing!")
# Returns global settings
def _config_global(config, option, is_int):
# Verify if global section exists
if config.has_section('global'):
if config.has_option('global', option):
if is_int:
return config.getint('global', option)
else:
return config.get('global', option)
else:
error_exit(str(option)+" option doesn't exists in 'global' section!")
else:
error_exit("'[global]' section not found in your acl file!")
# Retuns a dict of groups
def _config_groups(config):
if config.has_section('groups'):
group_list = config.items('groups')
if group_list:
group_dict = dict((e[0], e[1].split()) for e in group_list)
#pprint(group_dict)
# Let's expand groups
expanded_groups = {}
for group, users in group_dict.iteritems():
expanded_groups[group] = set()
for user in users:
expanded_users = expand_nested_groups(
group_dict,
group,
user,
[])
expanded_groups[group].update(expanded_users)
return expanded_groups
else:
error_exit("Your '[groups]' section seems to have no options...")
else:
error_exit("'[groups]' section not found in your acl file!")
# Returns a list of users allowed for a branch
def _config_branch(config, branch):
if config.has_section('branches'):
if config.has_option('branches', branch):
return config.get('branches', branch).split()
else:
error_exit("This branch doesn't have any right set!")
else:
error_exit("'[branches]' section not found in your acl file!")
def main():
# Get arguments
refname = sys.argv[1]
oldrev = sys.argv[2]
newrev = sys.argv[3]
gituser = os.getenv('USER')
# Declare object types
objtype = ['commit', 'delete', 'tag']
# Init acl file reading
acl_config = _config()
# Get global settings
admin_group = _config_global(acl_config, 'admin_group', 0)
full_admin = _config_global(acl_config, 'full_admin', 1)
owner_branch = _config_global(acl_config, 'owner_branch', 1)
# Get group list
g = _config_groups(acl_config)
#pprint(g)
# Get branch name
exp_refname = refname.split('/')
branch_name = exp_refname[2]
# Printing this only for information
print "branch="+branch_name
print "oldrev="+oldrev
print "newrev="+newrev
print "user="+gituser
# Get object type
cmd = 'git cat-file -t %s' % (newrev)
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=None, shell=True)
output = process.communicate()
thistype = output[0][:-1]
print "object="+thistype
# Reject unknown object types
if thistype not in objtype:
error_exit("You can't push this type of objects")
### Access right checking ###
# Admin processing
if full_admin and gituser in g[admin_group]:
ok_exit('ADMIN ACCESS GRANTED')
# user writes to his own branch
if owner_branch and gituser == branch_name:
ok_exit('BRANCH OWNER ACCESS GRANTED')
# Not admin nor branch owner
# Get branch access rights
branch_access = _config_branch(acl_config, branch_name)
# Branch exists in git.acl check if user has rights
# Start with group
for right in branch_access:
if '@' in right and gituser in g[right.strip('@')]:
ok_exit('BRANCH ACCESS GRANTED')
# Not in a group so do a basic check
if gituser in branch_access:
ok_exit('BRANCH ACCESS GRANTED')
else:
error_exit('BRANCH ACCESS DENIED')
# Catchall exit, means we haven't match any case...
sys.exit(1)
if __name__ == '__main__':
main()
Thanks to Steeve Chailloux for his group expansion function which was way better than mine :)
Next time let's support rights on tags ;)