blob: 3a33696f004493ac55124ece8b3088673b553c3c [file] [log] [blame]
Junio C Hamano1a4e8412005-12-27 08:17:231From: 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.
69case "$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 ;;
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 [ -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
120fi
121
122allowed_groups_file=$GIT_DIR/info/allowed-groups
123groups=$(id -G -n)
124info "The user belongs to the following groups:"
125info "'$groups'"
126
127if [ -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
151fi
152
153deny >/dev/null "There are no more rules to check. Denying access"
154
155-- >8 -- end of script -- >8 --
156
157This uses two files, $GIT_DIR/info/allowed-users and
158allowed-groups, to describe which heads can be pushed into by
159whom. The format of each file would look like this:
160
161refs/heads/master junio
162 refs/heads/cogito$ pasky
163refs/heads/bw/ linus
164 refs/heads/tmp/ *
165 refs/tags/v[0-9]* junio
166
167With this, Linus can push or create "bw/penguin" or "bw/zebra"
168or "bw/panda" branches, Pasky can do only "cogito", and JC can
169do master branch and make versioned tags. And anybody can do
170tmp/blah branches.
171
172------------