|  | From: Junio C Hamano <gitster@pobox.com> and Carl Baldwin <cnb@fc.hp.com> | 
|  | Subject: control access to branches. | 
|  | Date: Thu, 17 Nov 2005 23:55:32 -0800 | 
|  | Message-ID: <7vfypumlu3.fsf@assigned-by-dhcp.cox.net> | 
|  | Abstract: An example hooks/update script is presented to | 
|  | implement repository maintenance policies, such as who can push | 
|  | into which branch and who can make a tag. | 
|  | Content-type: text/asciidoc | 
|  |  | 
|  | How to use the update hook | 
|  | ========================== | 
|  |  | 
|  | When your developer runs git-push into the repository, | 
|  | git-receive-pack is run (either locally or over ssh) as that | 
|  | developer, so is hooks/update script. Quoting from the relevant | 
|  | section of the documentation: | 
|  |  | 
|  | Before each ref is updated, if $GIT_DIR/hooks/update file exists | 
|  | and executable, it is called with three parameters: | 
|  |  | 
|  | $GIT_DIR/hooks/update refname sha1-old sha1-new | 
|  |  | 
|  | The refname parameter is relative to $GIT_DIR; e.g. for the | 
|  | master head this is "refs/heads/master". Two sha1 are the | 
|  | object names for the refname before and after the update. Note | 
|  | that the hook is called before the refname is updated, so either | 
|  | sha1-old is 0{40} (meaning there is no such ref yet), or it | 
|  | should match what is recorded in refname. | 
|  |  | 
|  | So if your policy is (1) always require fast-forward push | 
|  | (i.e. never allow "git-push repo +branch:branch"), (2) you | 
|  | have a list of users allowed to update each branch, and (3) you | 
|  | do not let tags to be overwritten, then you can use something | 
|  | like this as your hooks/update script. | 
|  |  | 
|  | [jc: editorial note. This is a much improved version by Carl | 
|  | since I posted the original outline] | 
|  |  | 
|  | ---------------------------------------------------- | 
|  | #!/bin/bash | 
|  |  | 
|  | umask 002 | 
|  |  | 
|  | # If you are having trouble with this access control hook script | 
|  | # you can try setting this to true. It will tell you exactly | 
|  | # why a user is being allowed/denied access. | 
|  |  | 
|  | verbose=false | 
|  |  | 
|  | # Default shell globbing messes things up downstream | 
|  | GLOBIGNORE=* | 
|  |  | 
|  | function grant { | 
|  | $verbose && echo >&2 "-Grant-	$1" | 
|  | echo grant | 
|  | exit 0 | 
|  | } | 
|  |  | 
|  | function deny { | 
|  | $verbose && echo >&2 "-Deny-	$1" | 
|  | echo deny | 
|  | exit 1 | 
|  | } | 
|  |  | 
|  | function info { | 
|  | $verbose && echo >&2 "-Info-	$1" | 
|  | } | 
|  |  | 
|  | # Implement generic branch and tag policies. | 
|  | # - Tags should not be updated once created. | 
|  | # - Branches should only be fast-forwarded unless their pattern starts with '+' | 
|  | case "$1" in | 
|  | refs/tags/*) | 
|  | git rev-parse --verify -q "$1" && | 
|  | deny >/dev/null "You can't overwrite an existing tag" | 
|  | ;; | 
|  | refs/heads/*) | 
|  | # No rebasing or rewinding | 
|  | if expr "$2" : '0*$' >/dev/null; then | 
|  | info "The branch '$1' is new..." | 
|  | else | 
|  | # updating -- make sure it is a fast-forward | 
|  | mb=$(git-merge-base "$2" "$3") | 
|  | case "$mb,$2" in | 
|  | "$2,$mb") info "Update is fast-forward" ;; | 
|  | *) noff=y; info "This is not a fast-forward update.";; | 
|  | esac | 
|  | fi | 
|  | ;; | 
|  | *) | 
|  | deny >/dev/null \ | 
|  | "Branch is not under refs/heads or refs/tags. What are you trying to do?" | 
|  | ;; | 
|  | esac | 
|  |  | 
|  | # Implement per-branch controls based on username | 
|  | allowed_users_file=$GIT_DIR/info/allowed-users | 
|  | username=$(id -u -n) | 
|  | info "The user is: '$username'" | 
|  |  | 
|  | if test -f "$allowed_users_file" | 
|  | then | 
|  | rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' | | 
|  | while read heads user_patterns | 
|  | do | 
|  | # does this rule apply to us? | 
|  | head_pattern=${heads#+} | 
|  | matchlen=$(expr "$1" : "${head_pattern#+}") | 
|  | test "$matchlen" = ${#1} || continue | 
|  |  | 
|  | # if non-ff, $heads must be with the '+' prefix | 
|  | test -n "$noff" && | 
|  | test "$head_pattern" = "$heads" && continue | 
|  |  | 
|  | info "Found matching head pattern: '$head_pattern'" | 
|  | for user_pattern in $user_patterns; do | 
|  | info "Checking user: '$username' against pattern: '$user_pattern'" | 
|  | matchlen=$(expr "$username" : "$user_pattern") | 
|  | if test "$matchlen" = "${#username}" | 
|  | then | 
|  | grant "Allowing user: '$username' with pattern: '$user_pattern'" | 
|  | fi | 
|  | done | 
|  | deny "The user is not in the access list for this branch" | 
|  | done | 
|  | ) | 
|  | case "$rc" in | 
|  | grant) grant >/dev/null "Granting access based on $allowed_users_file" ;; | 
|  | deny) deny >/dev/null "Denying access based on $allowed_users_file" ;; | 
|  | *) ;; | 
|  | esac | 
|  | fi | 
|  |  | 
|  | allowed_groups_file=$GIT_DIR/info/allowed-groups | 
|  | groups=$(id -G -n) | 
|  | info "The user belongs to the following groups:" | 
|  | info "'$groups'" | 
|  |  | 
|  | if test -f "$allowed_groups_file" | 
|  | then | 
|  | rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' | | 
|  | while read heads group_patterns | 
|  | do | 
|  | # does this rule apply to us? | 
|  | head_pattern=${heads#+} | 
|  | matchlen=$(expr "$1" : "${head_pattern#+}") | 
|  | test "$matchlen" = ${#1} || continue | 
|  |  | 
|  | # if non-ff, $heads must be with the '+' prefix | 
|  | test -n "$noff" && | 
|  | test "$head_pattern" = "$heads" && continue | 
|  |  | 
|  | info "Found matching head pattern: '$head_pattern'" | 
|  | for group_pattern in $group_patterns; do | 
|  | for groupname in $groups; do | 
|  | info "Checking group: '$groupname' against pattern: '$group_pattern'" | 
|  | matchlen=$(expr "$groupname" : "$group_pattern") | 
|  | if test "$matchlen" = "${#groupname}" | 
|  | then | 
|  | grant "Allowing group: '$groupname' with pattern: '$group_pattern'" | 
|  | fi | 
|  | done | 
|  | done | 
|  | deny "None of the user's groups are in the access list for this branch" | 
|  | done | 
|  | ) | 
|  | case "$rc" in | 
|  | grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;; | 
|  | deny) deny >/dev/null "Denying access based on $allowed_groups_file" ;; | 
|  | *) ;; | 
|  | esac | 
|  | fi | 
|  |  | 
|  | deny >/dev/null "There are no more rules to check. Denying access" | 
|  | ---------------------------------------------------- | 
|  |  | 
|  | This uses two files, $GIT_DIR/info/allowed-users and | 
|  | allowed-groups, to describe which heads can be pushed into by | 
|  | whom. The format of each file would look like this: | 
|  |  | 
|  | refs/heads/master junio | 
|  | +refs/heads/pu junio | 
|  | refs/heads/cogito$ pasky | 
|  | refs/heads/bw/.* linus | 
|  | refs/heads/tmp/.* .* | 
|  | refs/tags/v[0-9].* junio | 
|  |  | 
|  | With this, Linus can push or create "bw/penguin" or "bw/zebra" | 
|  | or "bw/panda" branches, Pasky can do only "cogito", and JC can | 
|  | do master and pu branches and make versioned tags. And anybody | 
|  | can do tmp/blah branches. The '+' sign at the pu record means | 
|  | that JC can make non-fast-forward pushes on it. |