Commit | Line | Data |
---|---|---|
59eb68aa | 1 | From: Junio C Hamano <gitster@pobox.com> and Carl Baldwin <cnb@fc.hp.com> |
13cfdfd5 JH |
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. | |
1797e5c5 TA |
8 | Content-type: text/asciidoc |
9 | ||
10 | How to use the update hook | |
11 | ========================== | |
13cfdfd5 JH |
12 | |
13 | When your developer runs git-push into the repository, | |
14 | git-receive-pack is run (either locally or over ssh) as that | |
15 | developer, so is hooks/update script. Quoting from the relevant | |
16 | section 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 | ||
30 | So if your policy is (1) always require fast-forward push | |
31 | (i.e. never allow "git-push repo +branch:branch"), (2) you | |
32 | have a list of users allowed to update each branch, and (3) you | |
dc5f9239 JH |
33 | do not let tags to be overwritten, then you can use something |
34 | like this as your hooks/update script. | |
35 | ||
36 | [jc: editorial note. This is a much improved version by Carl | |
37 | since I posted the original outline] | |
38 | ||
1797e5c5 | 39 | ---------------------------------------------------- |
dc5f9239 JH |
40 | #!/bin/bash |
41 | ||
42 | umask 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 | ||
48 | verbose=false | |
49 | ||
50 | # Default shell globbing messes things up downstream | |
51 | GLOBIGNORE=* | |
52 | ||
53 | function grant { | |
54 | $verbose && echo >&2 "-Grant- $1" | |
55 | echo grant | |
56 | exit 0 | |
57 | } | |
58 | ||
59 | function deny { | |
60 | $verbose && echo >&2 "-Deny- $1" | |
61 | echo deny | |
62 | exit 1 | |
63 | } | |
64 | ||
65 | function info { | |
66 | $verbose && echo >&2 "-Info- $1" | |
67 | } | |
68 | ||
69 | # Implement generic branch and tag policies. | |
70 | # - Tags should not be updated once created. | |
f9a08f61 | 71 | # - Branches should only be fast-forwarded unless their pattern starts with '+' |
dc5f9239 JH |
72 | case "$1" in |
73 | refs/tags/*) | |
df79b9fd | 74 | git rev-parse --verify -q "$1" && |
dc5f9239 JH |
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 | |
a75d7b54 | 82 | # updating -- make sure it is a fast-forward |
dc5f9239 JH |
83 | mb=$(git-merge-base "$2" "$3") |
84 | case "$mb,$2" in | |
85 | "$2,$mb") info "Update is fast-forward" ;; | |
f9a08f61 | 86 | *) noff=y; info "This is not a fast-forward update.";; |
dc5f9239 JH |
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 | ;; | |
94 | esac | |
95 | ||
96 | # Implement per-branch controls based on username | |
97 | allowed_users_file=$GIT_DIR/info/allowed-users | |
98 | username=$(id -u -n) | |
99 | info "The user is: '$username'" | |
100 | ||
f9a08f61 DP |
101 | if test -f "$allowed_users_file" |
102 | then | |
dc5f9239 | 103 | rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' | |
f9a08f61 DP |
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 | |
1797e5c5 TA |
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 | |
f9a08f61 DP |
123 | done |
124 | deny "The user is not in the access list for this branch" | |
dc5f9239 JH |
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 | |
132 | fi | |
133 | ||
134 | allowed_groups_file=$GIT_DIR/info/allowed-groups | |
135 | groups=$(id -G -n) | |
136 | info "The user belongs to the following groups:" | |
137 | info "'$groups'" | |
138 | ||
f9a08f61 DP |
139 | if test -f "$allowed_groups_file" |
140 | then | |
dc5f9239 | 141 | rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' | |
f9a08f61 DP |
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 | |
1797e5c5 TA |
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 | |
dc5f9239 | 162 | done |
f9a08f61 DP |
163 | done |
164 | deny "None of the user's groups are in the access list for this branch" | |
dc5f9239 JH |
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 | |
172 | fi | |
173 | ||
174 | deny >/dev/null "There are no more rules to check. Denying access" | |
1797e5c5 | 175 | ---------------------------------------------------- |
dc5f9239 JH |
176 | |
177 | This uses two files, $GIT_DIR/info/allowed-users and | |
178 | allowed-groups, to describe which heads can be pushed into by | |
179 | whom. The format of each file would look like this: | |
13cfdfd5 | 180 | |
1797e5c5 TA |
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 | |
13cfdfd5 JH |
187 | |
188 | With this, Linus can push or create "bw/penguin" or "bw/zebra" | |
dc5f9239 | 189 | or "bw/panda" branches, Pasky can do only "cogito", and JC can |
f9a08f61 DP |
190 | do master and pu branches and make versioned tags. And anybody |
191 | can do tmp/blah branches. The '+' sign at the pu record means | |
192 | that JC can make non-fast-forward pushes on it. |