1 From: Junio C Hamano <gitster@pobox.com> 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 --
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.
47 # Default shell globbing messes things up downstream
51 $verbose && echo >&2 "-Grant- $1"
57 $verbose && echo >&2 "-Deny- $1"
63 $verbose && echo >&2 "-Info- $1"
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 '+'
71 git rev-parse --verify -q "$1" &&
72 deny >/dev/null "You can't overwrite an existing tag"
75 # No rebasing or rewinding
76 if expr "$2" : '0*$' >/dev/null; then
77 info "The branch '$1' is new..."
79 # updating -- make sure it is a fast forward
80 mb=$(git-merge-base "$2" "$3")
82 "$2,$mb") info "Update is fast-forward" ;;
83 *) noff=y; info "This is not a fast-forward update.";;
89 "Branch is not under refs/heads or refs/tags. What are you trying to do?"
93 # Implement per-branch controls based on username
94 allowed_users_file=$GIT_DIR/info/allowed-users
96 info "The user is: '$username'"
98 if test -f "$allowed_users_file"
100 rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' |
101 while read heads user_patterns
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
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}"
118 grant "Allowing user: '$username' with pattern: '$user_pattern'"
121 deny "The user is not in the access list for this branch"
125 grant) grant >/dev/null "Granting access based on $allowed_users_file" ;;
126 deny) deny >/dev/null "Denying access based on $allowed_users_file" ;;
131 allowed_groups_file=$GIT_DIR/info/allowed-groups
133 info "The user belongs to the following groups:"
136 if test -f "$allowed_groups_file"
138 rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' |
139 while read heads group_patterns
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
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}"
157 grant "Allowing group: '$groupname' with pattern: '$group_pattern'"
161 deny "None of the user's groups are in the access list for this branch"
165 grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;;
166 deny) deny >/dev/null "Denying access based on $allowed_groups_file" ;;
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
181 refs/heads/cogito$ pasky
182 refs/heads/bw/.* linus
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.