{"thread":{"id":"7750","subject":"[PATCH] Contribute a fairly paranoid update hook","startedAt":"2007-04-20T06:08:47Z","lastAt":"2007-04-21T03:39:20Z","messageCount":6,"participants":["Shawn O. Pearce","Junio C Hamano","Jakub Narebski"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"39941","messageId":"20070420060847.GA8255@spearce.org","threadId":"7750","inReplyTo":null,"subject":"[PATCH] Contribute a fairly paranoid update hook","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2007-04-20T06:08:47Z","receivedAt":"2007-04-20T06:08:47Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"I'm using a variant of this update hook in a corporate environment\nwhere we perform some validations of the commits and tags that\nare being pushed.  The model is a \"central repository\" type setup,\nwhere users are given access to push to specific branches within\nthe shared central repository.  In this particular installation we\nrun a specially patched git-receive-pack in setuid mode via SSH,\nallowing all writes into the repository as the repository owner,\nbut only if this hook blesses it.\n\nOne of the major checks we perform with this hook is that the\n'committer' line of a commit, or the 'tagger' line of a new annotated\ntag actually correlates to the UNIX user who is performing the push.\nUsers can falsify these lines on their local repositories, but\nthe central repository that management trusts will reject all such\nforgery attempts.  Of course 'author' lines are still allowed to\nbe any value, as sometimes changes do come from other individuals.\n\nAnother nice feature of this hook is the access control lists for\nall repositories on the system can also be stored and tracked in\na supporting Git repository, which can also be access controlled\nby itself.  This allows full auditing of who-had-what-when-and-why,\nthanks to git-blame's data mining capabilities.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n\n I have often mentioned this hook on the list, and some people\n have been interested in at least looking at it, or using it as a\n foundation for their own hooks.  I think it is a reasonable thing\n for contrib/hooks, so here it is...  ;-)\n\n contrib/hooks/update-paranoid |  284 +++++++++++++++++++++++++++++++++++++++++\n 1 files changed, 284 insertions(+), 0 deletions(-)\n create mode 100644 contrib/hooks/update-paranoid\n\ndiff --git a/contrib/hooks/update-paranoid b/contrib/hooks/update-paranoid\nnew file mode 100644\nindex 0000000..5ee1835\n--- /dev/null\n+++ b/contrib/hooks/update-paranoid\n@@ -0,0 +1,284 @@\n+#!/usr/bin/perl\n+\n+use strict;\n+use File::Spec;\n+\n+$ENV{PATH}     = '/opt/git/bin';\n+my $acl_git    = '/vcs/acls.git';\n+my $acl_branch = 'refs/heads/master';\n+my $debug      = 0;\n+\n+=doc\n+Invoked as: update refname old-sha1 new-sha1\n+\n+This script is run by git-receive-pack once for each ref that the\n+client is trying to modify.  If we exit with a non-zero exit value\n+then the update for that particular ref is denied, but updates for\n+other refs in the same run of receive-pack may still be allowed.\n+\n+We are run after the objects have been uploaded, but before the\n+ref is actually modified.  We take advantage of that fact when we\n+look for \"new\" commits and tags (the new objects won't show up in\n+`rev-list --all`).\n+\n+This script loads and parses the content of the config file\n+\"users/$this_user.acl\" from the $acl_branch commit of $acl_git ODB.\n+The acl file is a git-config style file, but uses a slightly more\n+restricted syntax as the Perl parser contained within this script\n+is not nearly as permissive as git-config.\n+\n+Example:\n+\n+  [user]\n+    committer = John Doe <john.doe@example.com>\n+    committer = John R. Doe <john.doe@example.com>\n+\n+  [repository \"acls\"]\n+    allow = heads/master\n+    allow = CDUR for heads/jd/\n+    allow = C    for ^tags/v\\\\d+$\n+\n+For all new commit or tag objects the committer (or tagger) line\n+within the object must exactly match one of the user.committer\n+values listed in the acl file (\"HEAD:users/$this_user.acl\").\n+\n+For a branch to be modified an allow line within the matching\n+repository section must be matched for both the refname and the\n+opcode.\n+\n+Repository sections are matched on the basename of the repository\n+(after removing the .git suffix).\n+\n+The opcode abbrevations are:\n+\n+  C: create new ref\n+  D: delete existing ref\n+  U: fast-forward existing ref (no commit loss)\n+  R: rewind/rebase existing ref (commit loss)\n+\n+if no opcodes are listed before the \"for\" keyword then \"U\" (for\n+fast-forward update only) is assumed as this is the most common\n+usage.\n+\n+Refnames are matched by always assuming a prefix of \"refs/\".\n+This hook forbids pushing or deleting anything not under \"refs/\".\n+\n+Refnames that start with ^ are Perl regular expressions, and the ^\n+is kept as part of the regexp.  \\\\ is needed to get just one \\, so\n+\\\\d expands to \\d in Perl.  The 3rd allow line above is an example.\n+\n+Refnames that don't start with ^ but that end with / are prefix\n+matches (2nd allow line above); all other refnames are strict\n+equality matches (1st allow line).\n+\n+Anything pushed to \"heads/\" (ok, really \"refs/heads/\") must be\n+a commit.  Tags are not permitted here.\n+\n+Anything pushed to \"tags/\" (err, really \"refs/tags/\") must be an\n+annotated tag.  Commits, blobs, trees, etc. are not permitted here.\n+Annotated tag signatures aren't checked, nor are they required.\n+\n+The special subrepository of 'info/new-commit-check' can\n+be created and used to allow users to push new commits and\n+tags from another local repository to this one, even if they\n+aren't the committer/tagger of those objects.  In a nut shell\n+the info/new-commit-check directory is a Git repository whose\n+objects/info/alternates file lists this repository and all other\n+possible sources, and whose refs subdirectory contains symlinks\n+to this repository's refs subdirectory, and to all other possible\n+sources refs subdirectories.  Yes, this means that you cannot\n+use packed-refs in those repositories as they won't be resolved\n+correctly.\n+\n+=cut\n+\n+my $git_dir = $ENV{GIT_DIR};\n+my $new_commit_check = \"$git_dir/info/new-commit-check\";\n+my $ref = $ARGV[0];\n+my $old = $ARGV[1];\n+my $new = $ARGV[2];\n+my $new_type;\n+my ($this_user) = getpwuid $<; # REAL_USER_ID\n+my $repository_name;\n+my %user_committer;\n+my @allow_rules;\n+\n+sub deny ($) {\n+\tprint STDERR \"-Deny-    $_[0]\\n\" if $debug;\n+\tprint STDERR \"\\ndenied: $_[0]\\n\\n\";\n+\texit 1;\n+}\n+\n+sub grant ($) {\n+\tprint STDERR \"-Grant-   $_[0]\\n\" if $debug;\n+\texit 0;\n+}\n+\n+sub info ($) {\n+\tprint STDERR \"-Info-    $_[0]\\n\" if $debug;\n+}\n+\n+sub parse_config ($$) {\n+\tmy ($data, $fn) = @_;\n+\tinfo \"Loading $fn\";\n+\topen(I,'-|','git',\"--git-dir=$acl_git\",'cat-file','blob',$fn);\n+\tmy $section = '';\n+\twhile (<I>) {\n+\t\tchomp;\n+\t\tif (/^\\s*$/ || /^\\s*#/) {\n+\t\t} elsif (/^\\[([a-z]+)\\]$/i) {\n+\t\t\t$section = $1;\n+\t\t} elsif (/^\\[([a-z]+)\\s+\"(.*)\"\\]$/i) {\n+\t\t\t$section = \"$1.$2\";\n+\t\t} elsif (/^\\s*([a-z][a-z0-9]+)\\s*=\\s*(.*?)\\s*$/i) {\n+\t\t\tpush @{$data->{\"$section.$1\"}}, $2;\n+\t\t} else {\n+\t\t\tdeny \"bad config file line $. in $fn\";\n+\t\t}\n+\t}\n+\tclose I;\n+}\n+\n+sub all_new_committers () {\n+\tlocal $ENV{GIT_DIR} = $git_dir;\n+\t$ENV{GIT_DIR} = $new_commit_check if -d $new_commit_check;\n+\n+\tinfo \"Getting committers of new commits.\";\n+\tmy %used;\n+\topen(T,'-|','git','rev-list','--pretty=raw',$new,'--not','--all');\n+\twhile (<T>) {\n+\t\tnext unless s/^committer //;\n+\t\tchop;\n+\t\ts/>.*$/>/;\n+\t\tinfo \"Found $_.\" unless $used{$_}++;\n+\t}\n+\tclose T;\n+\tinfo \"No new commits.\" unless %used;\n+\tkeys %used;\n+}\n+\n+sub all_new_taggers () {\n+\tmy %exists;\n+\topen(T,'-|','git','for-each-ref','--format=%(objectname)','refs/tags');\n+\twhile (<T>) {\n+\t\tchop;\n+\t\t$exists{$_} = 1;\n+\t}\n+\tclose T;\n+\n+\tinfo \"Getting taggers of new tags.\";\n+\tmy %used;\n+\tmy $obj = $new;\n+\tmy $obj_type = $new_type;\n+\twhile ($obj_type eq 'tag') {\n+\t\tlast if $exists{$obj};\n+\t\t$obj_type = '';\n+\t\topen(T,'-|','git','cat-file','tag',$obj);\n+\t\twhile (<T>) {\n+\t\t\tchop;\n+\t\t\tif (/^object ([a-z0-9]{40})$/) {\n+\t\t\t\t$obj = $1;\n+\t\t\t} elsif (/^type (.+)$/) {\n+\t\t\t\t$obj_type = $1;\n+\t\t\t} elsif (s/^tagger //) {\n+\t\t\t\ts/>.*$/>/;\n+\t\t\t\tinfo \"Found $_.\" unless $used{$_}++;\n+\t\t\t\tlast;\n+\t\t\t}\n+\t\t}\n+\t\tclose T;\n+\t}\n+\tinfo \"No new tags.\" unless %used;\n+\tkeys %used;\n+}\n+\n+sub check_committers (@) {\n+\tmy @bad;\n+\tforeach (@_) { push @bad, $_ unless $user_committer{$_}; }\n+\tif (@bad) {\n+\t\tprint STDERR \"\\n\";\n+\t\tprint STDERR \"You are not $_.\\n\" foreach (sort @bad);\n+\t\tdeny \"You cannot push changes not committed by you.\";\n+\t}\n+}\n+\n+sub git_value (@) {\n+\topen(T,'-|','git',@_); local $_ = <T>; chop; close T;\n+\t$_;\n+}\n+\n+deny \"No GIT_DIR inherited from caller\" unless $git_dir;\n+deny \"Need a ref name\" unless $ref;\n+deny \"Refusing funny ref $ref\" unless $ref =~ s,^refs/,,;\n+deny \"Bad old value $old\" unless $old =~ /^[a-z0-9]{40}$/;\n+deny \"Bad new value $new\" unless $new =~ /^[a-z0-9]{40}$/;\n+deny \"Cannot determine who you are.\" unless $this_user;\n+\n+$repository_name = File::Spec->rel2abs($git_dir);\n+$repository_name =~ m,/([^/]+)(?:\\.git|/\\.git)$,;\n+$repository_name = $1;\n+info \"Updating in '$repository_name'.\";\n+\n+my $op;\n+if    ($old =~ /^0{40}$/) { $op = 'C'; }\n+elsif ($new =~ /^0{40}$/) { $op = 'D'; }\n+else                      { $op = 'R'; }\n+\n+# This is really an update (fast-forward) if the\n+# merge base of $old and $new is $old.\n+#\n+$op = 'U' if ($op eq 'R'\n+\t&& $ref =~ m,^heads/,\n+\t&& $old eq git_value('merge-base',$old,$new));\n+\n+# Load the user's ACL file.\n+{\n+\tmy %data = ('user.committer' => []);\n+\tparse_config(\\%data, \"$acl_branch:users/$this_user.acl\");\n+\t%user_committer = map {$_ => $_} @{$data{'user.committer'}};\n+\tmy $rules = $data{\"repository.$repository_name.allow\"} || [];\n+\tforeach (@$rules) {\n+\t\tif (/^([CDRU ]+)\\s+for\\s+([^\\s]+)$/) {\n+\t\t\tmy $ops = $1;\n+\t\t\tmy $ref = $2;\n+\t\t\t$ops =~ s/ //g;\n+\t\t\t$ref =~ s/\\\\\\\\/\\\\/g;\n+\t\t\tpush @allow_rules, [$ops, $ref];\n+\t\t} elsif (/^for\\s+([^\\s]+)$/) {\n+\t\t\t# Mentioned, but nothing granted?\n+\t\t} elsif (/^[^\\s]+$/) {\n+\t\t\ts/\\\\\\\\/\\\\/g;\n+\t\t\tpush @allow_rules, ['U', $_];\n+\t\t}\n+\t}\n+}\n+\n+if ($op ne 'D') {\n+\t$new_type = git_value('cat-file','-t',$new);\n+\n+\tif ($ref =~ m,^heads/,) {\n+\t\tdeny \"$ref must be a commit.\" unless $new_type eq 'commit';\n+\t} elsif ($ref =~ m,^tags/,) {\n+\t\tdeny \"$ref must be an annotated tag.\" unless $new_type eq 'tag';\n+\t}\n+\n+\tcheck_committers (all_new_committers);\n+\tcheck_committers (all_new_taggers) if $new_type eq 'tag';\n+}\n+\n+info \"$this_user wants $op for $ref\";\n+foreach my $acl_entry (@allow_rules) {\n+\tmy ($acl_ops, $acl_n) = @$acl_entry;\n+\tnext unless $acl_ops =~ /^[CDRU]+$/; # Uhh.... shouldn't happen.\n+\tnext unless $acl_n;\n+\tnext unless $op =~ /^[$acl_ops]$/;\n+\n+\tgrant \"Allowed by: $acl_ops for $acl_n\"\n+\tif (\n+\t   ($acl_n eq $ref)\n+\t|| ($acl_n =~ m,/$, && substr($ref,0,length $acl_n) eq $acl_n)\n+\t|| ($acl_n =~ m,^\\^, && $ref =~ m:$acl_n:)\n+\t);\n+}\n+close A;\n+deny \"You are not permitted to $op $ref\";\n-- \n1.5.1.1.135.gf948\n"},{"id":"39959","messageId":"7vmz131fzz.fsf@assigned-by-dhcp.cox.net","threadId":"7750","inReplyTo":"20070420060847.GA8255@spearce.org","subject":"Re: [PATCH] Contribute a fairly paranoid update hook","fromName":"Junio C Hamano","fromEmail":"junkio@cox.net","sentAt":"2007-04-20T10:45:20Z","receivedAt":"2007-04-20T10:45:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Shawn O. Pearce\" <spearce@spearce.org> writes:\n\n> +$ENV{PATH}     = '/opt/git/bin';\n\nThis caught my attention ;-).\n"},{"id":"39987","messageId":"20070420145923.GB17480@spearce.org","threadId":"7750","inReplyTo":"7vmz131fzz.fsf@assigned-by-dhcp.cox.net","subject":"Re: [PATCH] Contribute a fairly paranoid update hook","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2007-04-20T14:59:23Z","receivedAt":"2007-04-20T14:59:23Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"Junio C Hamano <junkio@cox.net> wrote:\n> \"Shawn O. Pearce\" <spearce@spearce.org> writes:\n> \n> > +$ENV{PATH}     = '/opt/git/bin';\n> \n> This caught my attention ;-).\n\nWhat, is replacing PATH in the hook really all that paranoid?\nIts a perfectly reasonable thing to do.  ;-)\n\nSomeday I'll post the patch to receive-pack that I use to run\nit setuid.  That thing is also fairly paranoid.  I won't submit\nit for application to git.git though, as I don't think it really\nbelongs in the main receive-pack code.  So it might be a patch file\nfor contrib, or just an article in the mailing list archives that\npeople can link to.\n\nI also recently hacked some semi-useful features into gitweb.cgi.\nI need to finish those for our local use, and then see if I can\nextract them enough to make them publicly available.  gitweb.cgi is\nprobably going to become a cornerstone of our main development and\ntesting workflow, especially for managers and testers who don't want\n(or really need) full repositories on their deskstops.\n\n-- \nShawn.\n"},{"id":"39995","messageId":"7v647ryn0p.fsf@assigned-by-dhcp.cox.net","threadId":"7750","inReplyTo":"20070420145923.GB17480@spearce.org","subject":"Re: [PATCH] Contribute a fairly paranoid update hook","fromName":"Junio C Hamano","fromEmail":"junkio@cox.net","sentAt":"2007-04-20T17:27:18Z","receivedAt":"2007-04-20T17:27:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Shawn O. Pearce\" <spearce@spearce.org> writes:\n\n> Junio C Hamano <junkio@cox.net> wrote:\n>> \"Shawn O. Pearce\" <spearce@spearce.org> writes:\n>> \n>> > +$ENV{PATH}     = '/opt/git/bin';\n>> \n>> This caught my attention ;-).\n>\n> What, is replacing PATH in the hook really all that paranoid?\n> Its a perfectly reasonable thing to do.  ;-)\n\nI 100% agree it is the right thing to to make sure PATH is a\nknown good value.  What caught my attention is /opt/git/bin\nis the *ONLY* place you need to look at, not even /usr/bin!\n"},{"id":"40016","messageId":"f0bk15$c8m$1@sea.gmane.org","threadId":"7750","inReplyTo":"20070420145923.GB17480@spearce.org","subject":"Re: [PATCH] Contribute a fairly paranoid update hook","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2007-04-21T00:01:23Z","receivedAt":"2007-04-21T00:01:23Z","isPatch":true,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Shawn O. Pearce wrote:\n\n> I also recently hacked some semi-useful features into gitweb.cgi.\n> I need to finish those for our local use, and then see if I can\n> extract them enough to make them publicly available.  gitweb.cgi is\n> probably going to become a cornerstone of our main development and\n> testing workflow, especially for managers and testers who don't want\n> (or really need) full repositories on their deskstops.\n\nIf I can ask: what features? \n\n-- \nJakub Narebski\nWarsaw, Poland\nShadeHawk on #git\n"},{"id":"40026","messageId":"20070421033920.GC17480@spearce.org","threadId":"7750","inReplyTo":"f0bk15$c8m$1@sea.gmane.org","subject":"Re: [PATCH] Contribute a fairly paranoid update hook","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2007-04-21T03:39:20Z","receivedAt":"2007-04-21T03:39:20Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"Jakub Narebski <jnareb@gmail.com> wrote:\n> Shawn O. Pearce wrote:\n> \n> > I also recently hacked some semi-useful features into gitweb.cgi.\n...\n> If I can ask: what features? \n\nI added a \"pending\" action and link in the top navigation bar.\nThis page shows all of the topic branches that:\n\n - match a particular prefix string\n   (e.g. \"heads/test/pending/\");\n - has commits not reachable through refs matching another prefix\n   (e.g. \"heads/test/approved/\")\n - has commits not reachable through another specific ref\n   (e.g. \"heads/test/current\").\n\nThese branches are sorted by name and show under their title a\nshortlog of the \"new commits\" that aren't reachable by the latter\ntwo sets of refs.\n\nSometimes branches fully contain another; these are identified under\nboth branches.  When this happens commits are only shown under one\nof the two listings.\n\nHere's a rough ASCII approximation of the pending page:\n\n  fix1:\n\n    Also bundled within:\n    \n      * set-of-fixes\n\n    Shawn Pearce      don't be stupid, delete temp files\n\n  fix2:\n\n    Also bundled within:\n\n      * set-of-fixes\n\n    Shawn Pearce      don't leak printer handles\n\n  set-of-fixes:\n\n    Includes topics:\n\n      * fix1:\n      * fix2:\n\n    Stewie Griffin    Merge 'tiny-fix1'\n    Stewie Griffin    Merge 'tiny-fix2'\n    Glenn Quagmire    Improve performance by sucking less\n\nEach topic branch also has an \"approve\" link, which uses an \"approve\"\naction to copy the branch (err, create a new branch with update-ref)\ninto the \"heads/test/approved/\" namespace.  This causes them to\nfall out of the \"pending\" page when it reloads.\n\nThe approved page shows topics matching the \"heads/test/approved/\"\npage, but that have commits not in \"heads/test/current\".  It looks\nexactly like the pending page above, but has an \"unapprove\" link\nto delete the ref from the \"heads/test/approved/\" directory.\n\nWhy?\n\nManagers and testers want to choose the topic branches they will be\nincluding into the next release, and they don't make that decision\nuntil just before they ask the build manager to start creating\nthe build.  This gitweb interface lets them see the topics that\nare ready for building, and make their selections.\n\nThe build manager can then sweep the entire \"heads/test/approved/\"\ndirectory and merge everything not already merged into the current\nbranch.  Of course if a merge fails, or a compile fails, the build\nmanager stops and throws the mess back at the development team.\n\nBad merges or build failures happen less often than you might expect,\nas we do have fairly good isolation between our topic branches.\nDuring some periods of the year we don't think twice about using the\noctopus driver to merge topics, and will merge 30+ topics at a clip\nin just a minute or two, because they are all unrelated and isolated\nchanges that management has asked to go into a single release.\n\nMost of the above code is actually fairly well isolated in\ngitweb.cgi.  It turns out it was quite easy to add what I needed,\nand to reuse a lot of the existing code.  I'll probably be able to\nclean it up well enough to submit a patch.  Might take a few weeks.\n\n-- \nShawn.\n"}]}