{"thread":{"id":"9451","subject":"[PATCH 2/3] Teach the update-paranoid to look at file differences","startedAt":"2007-08-09T06:38:12Z","lastAt":"2007-08-09T06:38:12Z","messageCount":1,"participants":["Shawn O. Pearce"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"50287","messageId":"20070809063812.GB26843@spearce.org","threadId":"9451","inReplyTo":null,"subject":"[PATCH 2/3] Teach the update-paranoid to look at file differences","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2007-08-09T06:38:12Z","receivedAt":"2007-08-09T06:38:12Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"In some applications of the update hook a user may be allowed to\nmodify a branch, but only if the file level difference is also an\nallowed change.  This is the commonly requested feature of allowing\nusers to modify only certain files.\n\nA new repository.*.allow syntax permits granting the three basic\nfile level operations:\n\n  A: file is added relative to the other tree\n  M: file exists in both trees, but its SHA-1 or mode differs\n  D: file is removed relative to the other tree\n\non a per-branch and path-name basis.  The user must also have a\nbranch level allow line already granting them access to create,\nrewind or update (CRU) that branch before the hook will consult\nany file level rules.\n\nIn order for a branch change to succeed _all_ files that differ\nrelative to some base (by default the old value of this branch,\nbut it can also be any valid tree-ish) must be allowed by file\nlevel allow rules.  A push is rejected if any diff exists that\nis not covered by at least one allow rule.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n contrib/hooks/update-paranoid |  112 ++++++++++++++++++++++++++++++++++++++---\n 1 files changed, 105 insertions(+), 7 deletions(-)\n\ndiff --git a/contrib/hooks/update-paranoid b/contrib/hooks/update-paranoid\nindex fb2aca3..84ed452 100644\n--- a/contrib/hooks/update-paranoid\n+++ b/contrib/hooks/update-paranoid\n@@ -102,6 +102,8 @@ my ($this_user) = getpwuid $<; # REAL_USER_ID\n my $repository_name;\n my %user_committer;\n my @allow_rules;\n+my @path_rules;\n+my %diff_cache;\n \n sub deny ($) {\n \tprint STDERR \"-Deny-    $_[0]\\n\" if $debug;\n@@ -122,6 +124,13 @@ sub git_value (@) {\n \topen(T,'-|','git',@_); local $_ = <T>; chop; close T; $_;\n }\n \n+sub match_string ($$) {\n+\tmy ($acl_n, $ref) = @_;\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+}\n+\n sub parse_config ($$$$) {\n \tmy $data = shift;\n \tlocal $ENV{GIT_DIR} = shift;\n@@ -209,6 +218,31 @@ sub check_committers (@) {\n \t}\n }\n \n+sub load_diff ($) {\n+\tmy $base = shift;\n+\tmy $d = $diff_cache{$base};\n+\tunless ($d) {\n+\t\tlocal $/ = \"\\0\";\n+\t\topen(T,'-|','git','diff-tree',\n+\t\t\t'-r','--name-status','-z',\n+\t\t\t$base,$new) or return undef;\n+\t\tmy %this_diff;\n+\t\twhile (<T>) {\n+\t\t\tmy $op = $_;\n+\t\t\tchop $op;\n+\n+\t\t\tmy $path = <T>;\n+\t\t\tchop $path;\n+\n+\t\t\t$this_diff{$path} = $op;\n+\t\t}\n+\t\tclose T or return undef;\n+\t\t$d = \\%this_diff;\n+\t\t$diff_cache{$base} = $d;\n+\t}\n+\treturn $d;\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@@ -266,7 +300,19 @@ RULE:\n \t\t\ts/\\${user\\.$k}/$v->[0]/g;\n \t\t}\n \n-\t\tif (/^([CDRU ]+)\\s+for\\s+([^\\s]+)$/) {\n+\t\tif (/^([AMD ]+)\\s+of\\s+([^\\s]+)\\s+for\\s+([^\\s]+)\\s+diff\\s+([^\\s]+)$/) {\n+\t\t\tmy ($ops, $pth, $ref, $bst) = ($1, $2, $3, $4);\n+\t\t\t$ops =~ s/ //g;\n+\t\t\t$pth =~ s/\\\\\\\\/\\\\/g;\n+\t\t\t$ref =~ s/\\\\\\\\/\\\\/g;\n+\t\t\tpush @path_rules, [$ops, $pth, $ref, $bst];\n+\t\t} elsif (/^([AMD ]+)\\s+of\\s+([^\\s]+)\\s+for\\s+([^\\s]+)$/) {\n+\t\t\tmy ($ops, $pth, $ref) = ($1, $2, $3);\n+\t\t\t$ops =~ s/ //g;\n+\t\t\t$pth =~ s/\\\\\\\\/\\\\/g;\n+\t\t\t$ref =~ s/\\\\\\\\/\\\\/g;\n+\t\t\tpush @path_rules, [$ops, $pth, $ref, $old];\n+\t\t} elsif (/^([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@@ -300,13 +346,65 @@ foreach my $acl_entry (@allow_rules) {\n \tnext unless $acl_ops =~ /^[CDRU]+$/; # Uhh.... shouldn't happen.\n \tnext unless $acl_n;\n \tnext unless $op =~ /^[$acl_ops]$/;\n+\tnext unless match_string $acl_n, $ref;\n+\n+\t# Don't test path rules on branch deletes.\n+\t#\n+\tgrant \"Allowed by: $acl_ops for $acl_n\" if $op eq 'D';\n+\n+\t# Aggregate matching path rules; allow if there aren't\n+\t# any matching this ref.\n+\t#\n+\tmy %pr;\n+\tforeach my $p_entry (@path_rules) {\n+\t\tmy ($p_ops, $p_n, $p_ref, $p_bst) = @$p_entry;\n+\t\tnext unless $p_ref;\n+\t\tpush @{$pr{$p_bst}}, $p_entry if match_string $p_ref, $ref;\n+\t}\n+\tgrant \"Allowed by: $acl_ops for $acl_n\" unless %pr;\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+\t# Allow only if all changes against a single base are\n+\t# allowed by file path rules.\n+\t#\n+\tmy @bad;\n+\tforeach my $p_bst (keys %pr) {\n+\t\tmy $diff_ref = load_diff $p_bst;\n+\t\tdeny \"Cannot difference trees.\" unless ref $diff_ref;\n+\n+\t\tmy %fd = %$diff_ref;\n+\t\tforeach my $p_entry (@{$pr{$p_bst}}) {\n+\t\t\tmy ($p_ops, $p_n, $p_ref, $p_bst) = @$p_entry;\n+\t\t\tnext unless $p_ops =~ /^[AMD]+$/;\n+\t\t\tnext unless $p_n;\n+\n+\t\t\tforeach my $f_n (keys %fd) {\n+\t\t\t\tmy $f_op = $fd{$f_n};\n+\t\t\t\tnext unless $f_op;\n+\t\t\t\tnext unless $f_op =~ /^[$p_ops]$/;\n+\t\t\t\tdelete $fd{$f_n} if match_string $p_n, $f_n;\n+\t\t\t}\n+\t\t\tlast unless %fd;\n+\t\t}\n+\n+\t\tif (%fd) {\n+\t\t\tpush @bad, [$p_bst, \\%fd];\n+\t\t} else {\n+\t\t\t# All changes relative to $p_bst were allowed.\n+\t\t\t#\n+\t\t\tgrant \"Allowed by: $acl_ops for $acl_n diff $p_bst\";\n+\t\t}\n+\t}\n+\n+\tforeach my $bad_ref (@bad) {\n+\t\tmy ($p_bst, $fd) = @$bad_ref;\n+\t\tprint STDERR \"\\n\";\n+\t\tprint STDERR \"Not allowed to make the following changes:\\n\";\n+\t\tprint STDERR \"(base: $p_bst)\\n\";\n+\t\tforeach my $f_n (sort keys %$fd) {\n+\t\t\tprint STDERR \"  $fd->{$f_n} $f_n\\n\";\n+\t\t}\n+\t}\n+\tdeny \"You are not permitted to $op $ref\";\n }\n close A;\n deny \"You are not permitted to $op $ref\";\n-- \n1.5.3.rc4.29.g74276\n"}]}