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 unless their pattern starts with '+' 69case "$1" in 70 refs/tags/*) 71 git rev-parse --verify -q "$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 *) noff=y; info "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 test -f "$allowed_users_file" 99then 100 rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' | 101 while read heads user_patterns 102 do 103 # does this rule apply to us? 104 head_pattern=${heads#+} 105 matchlen=$(expr "$1" : "${head_pattern#+}") 106 test "$matchlen" = ${#1} || continue 107 108 # if non-ff, $heads must be with the '+' prefix 109 test -n "$noff" && 110 test "$head_pattern" = "$heads" && continue 111 112 info "Found matching head pattern: '$head_pattern'" 113 for user_pattern in $user_patterns; do 114 info "Checking user: '$username' against pattern: '$user_pattern'" 115 matchlen=$(expr "$username" : "$user_pattern") 116 if test "$matchlen" = "${#username}" 117 then 118 grant "Allowing user: '$username' with pattern: '$user_pattern'" 119 fi 120 done 121 deny "The user is not in the access list for this branch" 122 done 123 ) 124 case "$rc" in 125 grant) grant >/dev/null "Granting access based on $allowed_users_file" ;; 126 deny) deny >/dev/null "Denying access based on $allowed_users_file" ;; 127 *) ;; 128 esac 129fi 130 131allowed_groups_file=$GIT_DIR/info/allowed-groups 132groups=$(id -G -n) 133info "The user belongs to the following groups:" 134info "'$groups'" 135 136if test -f "$allowed_groups_file" 137then 138 rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' | 139 while read heads group_patterns 140 do 141 # does this rule apply to us? 142 head_pattern=${heads#+} 143 matchlen=$(expr "$1" : "${head_pattern#+}") 144 test "$matchlen" = ${#1} || continue 145 146 # if non-ff, $heads must be with the '+' prefix 147 test -n "$noff" && 148 test "$head_pattern" = "$heads" && continue 149 150 info "Found matching head pattern: '$head_pattern'" 151 for group_pattern in $group_patterns; do 152 for groupname in $groups; do 153 info "Checking group: '$groupname' against pattern: '$group_pattern'" 154 matchlen=$(expr "$groupname" : "$group_pattern") 155 if test "$matchlen" = "${#groupname}" 156 then 157 grant "Allowing group: '$groupname' with pattern: '$group_pattern'" 158 fi 159 done 160 done 161 deny "None of the user's groups are in the access list for this branch" 162 done 163 ) 164 case "$rc" in 165 grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;; 166 deny) deny >/dev/null "Denying access based on $allowed_groups_file" ;; 167 *) ;; 168 esac 169fi 170 171deny >/dev/null "There are no more rules to check. Denying access" 172 173-- >8 -- end of script -- >8 -- 174 175This uses two files, $GIT_DIR/info/allowed-users and 176allowed-groups, to describe which heads can be pushed into by 177whom. The format of each file would look like this: 178 179 refs/heads/master junio 180 +refs/heads/pu junio 181 refs/heads/cogito$ pasky 182 refs/heads/bw/.* linus 183 refs/heads/tmp/.* .* 184 refs/tags/v[0-9].* junio 185 186With this, Linus can push or create "bw/penguin" or "bw/zebra" 187or "bw/panda" branches, Pasky can do only "cogito", and JC can 188do master and pu branches and make versioned tags. And anybody 189can do tmp/blah branches. The '+' sign at the pu record means 190that JC can make non-fast-forward pushes on it. 191 192------------