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.
69 case "$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 ;;
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 [ -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
120 fi
122 allowed_groups_file=$GIT_DIR/info/allowed-groups
123 groups=$(id -G -n)
124 info "The user belongs to the following groups:"
125 info "'$groups'"
127 if [ -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
151 fi
153 deny >/dev/null "There are no more rules to check. Denying access"
155 -- >8 -- end of script -- >8 --
157 This uses two files, $GIT_DIR/info/allowed-users and
158 allowed-groups, to describe which heads can be pushed into by
159 whom. The format of each file would look like this:
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
167 With this, Linus can push or create "bw/penguin" or "bw/zebra"
168 or "bw/panda" branches, Pasky can do only "cogito", and JC can
169 do master branch and make versioned tags. And anybody can do
170 tmp/blah branches.
172 ------------