1From: Junio C Hamano <junkio@cox.net> and Carl Baldwin <cnb@fc.hp.com> 2Subject: control access to branches. 3Date: Thu, 17 Nov 2005 23:55:32 -0800 4Message-ID: <7vfypumlu3.fsf@assigned-by-dhcp.cox.net> 5Abstract: An example hooks/update script is presented to 6 implement repository maintenance policies, such as who can push 7 into which branch and who can make a tag. 8 9When your developer runs git-push into the repository, 10git-receive-pack is run (either locally or over ssh) as that 11developer, so is hooks/update script. Quoting from the relevant 12section of the documentation: 13 14 Before each ref is updated, if $GIT_DIR/hooks/update file exists 15 and executable, it is called with three parameters: 16 17 $GIT_DIR/hooks/update refname sha1-old sha1-new 18 19 The refname parameter is relative to $GIT_DIR; e.g. for the 20 master head this is "refs/heads/master". Two sha1 are the 21 object names for the refname before and after the update. Note 22 that the hook is called before the refname is updated, so either 23 sha1-old is 0{40} (meaning there is no such ref yet), or it 24 should match what is recorded in refname. 25 26So if your policy is (1) always require fast-forward push 27(i.e. never allow "git-push repo +branch:branch"), (2) you 28have a list of users allowed to update each branch, and (3) you 29do not let tags to be overwritten, then you can use something 30like this as your hooks/update script. 31 32[jc: editorial note. This is a much improved version by Carl 33since I posted the original outline] 34 35-- >8 -- beginning of script -- >8 -- 36 37#!/bin/bash 38 39umask 002 40 41# If you are having trouble with this access control hook script 42# you can try setting this to true. It will tell you exactly 43# why a user is being allowed/denied access. 44 45verbose=false 46 47# Default shell globbing messes things up downstream 48GLOBIGNORE=* 49 50function grant { 51 $verbose && echo >&2 "-Grant- $1" 52 echo grant 53 exit 0 54} 55 56function deny { 57 $verbose && echo >&2 "-Deny- $1" 58 echo deny 59 exit 1 60} 61 62function info { 63 $verbose && echo >&2 "-Info- $1" 64} 65 66# Implement generic branch and tag policies. 67# - Tags should not be updated once created. 68# - Branches should only be fast-forwarded. 69case "$1" in 70 refs/tags/*) 71 [ -f "$GIT_DIR/$1" ] && 72 deny >/dev/null "You can't overwrite an existing tag" 73 ;; 74 refs/heads/*) 75 # No rebasing or rewinding 76 if expr "$2" : '0*$' >/dev/null; then 77 info "The branch '$1' is new..." 78 else 79 # updating -- make sure it is a fast forward 80 mb=$(git-merge-base "$2" "$3") 81 case "$mb,$2" in 82 "$2,$mb") info "Update is fast-forward" ;; 83 *) deny >/dev/null "This is not a fast-forward update." ;; 84 esac 85 fi 86 ;; 87 *) 88 deny >/dev/null \ 89 "Branch is not under refs/heads or refs/tags. What are you trying to do?" 90 ;; 91esac 92 93# Implement per-branch controls based on username 94allowed_users_file=$GIT_DIR/info/allowed-users 95username=$(id -u -n) 96info "The user is: '$username'" 97 98if [ -f "$allowed_users_file" ]; then 99 rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' | 100 while read head_pattern user_patterns; do 101 matchlen=$(expr "$1" : "$head_pattern") 102 if [ "$matchlen" == "${#1}" ]; then 103 info "Found matching head pattern: '$head_pattern'" 104 for user_pattern in $user_patterns; do 105 info "Checking user: '$username' against pattern: '$user_pattern'" 106 matchlen=$(expr "$username" : "$user_pattern") 107 if [ "$matchlen" == "${#username}" ]; then 108 grant "Allowing user: '$username' with pattern: '$user_pattern'" 109 fi 110 done 111 deny "The user is not in the access list for this branch" 112 fi 113 done 114 ) 115 case "$rc" in 116 grant) grant >/dev/null "Granting access based on $allowed_users_file" ;; 117 deny) deny >/dev/null "Denying access based on $allowed_users_file" ;; 118 *) ;; 119 esac 120fi 121 122allowed_groups_file=$GIT_DIR/info/allowed-groups 123groups=$(id -G -n) 124info "The user belongs to the following groups:" 125info "'$groups'" 126 127if [ -f "$allowed_groups_file" ]; then 128 rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' | 129 while read head_pattern group_patterns; do 130 matchlen=$(expr "$1" : "$head_pattern") 131 if [ "$matchlen" == "${#1}" ]; then 132 info "Found matching head pattern: '$head_pattern'" 133 for group_pattern in $group_patterns; do 134 for groupname in $groups; do 135 info "Checking group: '$groupname' against pattern: '$group_pattern'" 136 matchlen=$(expr "$groupname" : "$group_pattern") 137 if [ "$matchlen" == "${#groupname}" ]; then 138 grant "Allowing group: '$groupname' with pattern: '$group_pattern'" 139 fi 140 done 141 done 142 deny "None of the user's groups are in the access list for this branch" 143 fi 144 done 145 ) 146 case "$rc" in 147 grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;; 148 deny) deny >/dev/null "Denying access based on $allowed_groups_file" ;; 149 *) ;; 150 esac 151fi 152 153deny >/dev/null "There are no more rules to check. Denying access" 154 155-- >8 -- end of script -- >8 -- 156 157This uses two files, $GIT_DIR/info/allowed-users and 158allowed-groups, to describe which heads can be pushed into by 159whom. The format of each file would look like this: 160 161 refs/heads/master junio 162 refs/heads/cogito$ pasky 163 refs/heads/bw/ linus 164 refs/heads/tmp/ * 165 refs/tags/v[0-9]* junio 166 167With this, Linus can push or create "bw/penguin" or "bw/zebra" 168or "bw/panda" branches, Pasky can do only "cogito", and JC can 169do master branch and make versioned tags. And anybody can do 170tmp/blah branches. 171 172------------