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