1 From: Junio C Hamano <junkio@cox.net> and Carl Baldwin <cnb@fc.hp.com>
2 Subject: control access to branches.
3 Date: Thu, 17 Nov 2005 23:55:32 -0800
4 Message-ID: <7vfypumlu3.fsf@assigned-by-dhcp.cox.net>
5 Abstract: 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.
9 When your developer runs git-push into the repository,
10 git-receive-pack is run (either locally or over ssh) as that
11 developer, so is hooks/update script. Quoting from the relevant
12 section of the documentation:
14 Before each ref is updated, if $GIT_DIR/hooks/update file exists
15 and executable, it is called with three parameters:
17 $GIT_DIR/hooks/update refname sha1-old sha1-new
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.
26 So if your policy is (1) always require fast-forward push
27 (i.e. never allow "git-push repo +branch:branch"), (2) you
28 have a list of users allowed to update each branch, and (3) you
29 do not let tags to be overwritten, then you can use something
30 like this as your hooks/update script.
32 [jc: editorial note. This is a much improved version by Carl
33 since I posted the original outline]
35 -- >8 -- beginning of script -- >8 --
37 #!/bin/bash
39 umask 002
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.
45 verbose=false
47 # Default shell globbing messes things up downstream
48 GLOBIGNORE=*
50 function grant {
51 $verbose && echo >&2 "-Grant- $1"
52 echo grant
53 exit 0
54 }
56 function deny {
57 $verbose && echo >&2 "-Deny- $1"
58 echo deny
59 exit 1
60 }
62 function info {
63 $verbose && echo >&2 "-Info- $1"
64 }
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 '+'
69 case "$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 ;;
91 esac
93 # Implement per-branch controls based on username
94 allowed_users_file=$GIT_DIR/info/allowed-users
95 username=$(id -u -n)
96 info "The user is: '$username'"
98 if test -f "$allowed_users_file"
99 then
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
108 # if non-ff, $heads must be with the '+' prefix
109 test -n "$noff" &&
110 test "$head_pattern" = "$heads" && continue
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
129 fi
131 allowed_groups_file=$GIT_DIR/info/allowed-groups
132 groups=$(id -G -n)
133 info "The user belongs to the following groups:"
134 info "'$groups'"
136 if test -f "$allowed_groups_file"
137 then
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
146 # if non-ff, $heads must be with the '+' prefix
147 test -n "$noff" &&
148 test "$head_pattern" = "$heads" && continue
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
169 fi
171 deny >/dev/null "There are no more rules to check. Denying access"
173 -- >8 -- end of script -- >8 --
175 This uses two files, $GIT_DIR/info/allowed-users and
176 allowed-groups, to describe which heads can be pushed into by
177 whom. The format of each file would look like this:
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
186 With this, Linus can push or create "bw/penguin" or "bw/zebra"
187 or "bw/panda" branches, Pasky can do only "cogito", and JC can
188 do master and pu branches and make versioned tags. And anybody
189 can do tmp/blah branches. The '+' sign at the pu record means
190 that JC can make non-fast-forward pushes on it.
192 ------------