Junio C Hamano | 0868a30 | 2008-07-22 09:20:44 | [diff] [blame] | 1 | From: Junio C Hamano <gitster@pobox.com> and Carl Baldwin <cnb@fc.hp.com> |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 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. |
Junio C Hamano | f2b7494 | 2012-11-20 21:06:26 | [diff] [blame] | 8 | Content-type: text/asciidoc |
| 9 | |
| 10 | How to use the update hook |
| 11 | ========================== |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 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 |
| 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 | |
Junio C Hamano | f2b7494 | 2012-11-20 21:06:26 | [diff] [blame] | 39 | ---------------------------------------------------- |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 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. |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 71 | # - Branches should only be fast-forwarded unless their pattern starts with '+' |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 72 | case "$1" in |
| 73 | refs/tags/*) |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 74 | git rev-parse --verify -q "$1" && |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 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 |
Junio C Hamano | 3f680f3 | 2009-11-16 02:10:54 | [diff] [blame] | 82 | # updating -- make sure it is a fast-forward |
Junio C Hamano | a4df8ed | 2018-10-26 06:37:13 | [diff] [blame] | 83 | mb=$(git merge-base "$2" "$3") |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 84 | case "$mb,$2" in |
| 85 | "$2,$mb") info "Update is fast-forward" ;; |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 86 | *) noff=y; info "This is not a fast-forward update.";; |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 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 | |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 101 | if test -f "$allowed_users_file" |
| 102 | then |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 103 | rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' | |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 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 |
Junio C Hamano | f2b7494 | 2012-11-20 21:06:26 | [diff] [blame] | 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 |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 123 | done |
| 124 | deny "The user is not in the access list for this branch" |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 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 | |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 139 | if test -f "$allowed_groups_file" |
| 140 | then |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 141 | rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' | |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 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 |
Junio C Hamano | f2b7494 | 2012-11-20 21:06:26 | [diff] [blame] | 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 |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 162 | done |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 163 | done |
| 164 | deny "None of the user's groups are in the access list for this branch" |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 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" |
Junio C Hamano | f2b7494 | 2012-11-20 21:06:26 | [diff] [blame] | 175 | ---------------------------------------------------- |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 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: |
| 180 | |
Junio C Hamano | f2b7494 | 2012-11-20 21:06:26 | [diff] [blame] | 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 |
Junio C Hamano | 1a4e841 | 2005-12-27 08:17:23 | [diff] [blame] | 187 | |
| 188 | With this, Linus can push or create "bw/penguin" or "bw/zebra" |
| 189 | or "bw/panda" branches, Pasky can do only "cogito", and JC can |
Junio C Hamano | 9eb5753 | 2008-06-26 06:33:46 | [diff] [blame] | 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. |