Spaces:
Running
Running
From: Junio C Hamano <[email protected]> and Carl Baldwin <[email protected]> | |
Subject: control access to branches. | |
Date: Thu, 17 Nov 2005 23:55:32 -0800 | |
Message-ID: <[email protected]> | |
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/seen 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 "seen" branches and make versioned tags. And anybody | |
can do tmp/blah branches. The '+' sign at the "seen" record means | |
that JC can make non-fast-forward pushes on it. | |