192 lines
		
	
	
	
		
			6.1 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			192 lines
		
	
	
	
		
			6.1 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
| 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.
 |