Documentation / howto / update-hook-example.txton commit Merge branch 'ep/cvstag' (0354539)
   1From: 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
 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
 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------------