diff options
-rw-r--r-- | IkiWiki.pm | 11 | ||||
-rw-r--r-- | IkiWiki/Plugin/git.pm | 115 | ||||
-rw-r--r-- | doc/plugins/write.mdwn | 9 | ||||
-rw-r--r-- | doc/rcs/details.mdwn | 3 | ||||
-rw-r--r-- | doc/rcs/git.mdwn | 27 | ||||
-rwxr-xr-x | ikiwiki.in | 3 |
6 files changed, 158 insertions, 10 deletions
diff --git a/IkiWiki.pm b/IkiWiki.pm index e0454963d..245eaafba 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -382,6 +382,13 @@ sub getsetup () { #{{{ safe => 0, rebuild => 0, }, + test_receive => { + type => "internal", + default => 0, + description => "running in receive test mode", + safe => 0, + rebuild => 0, + }, getctime => { type => "internal", default => 0, @@ -1575,6 +1582,10 @@ sub rcs_getctime ($) { #{{{ $hooks{rcs}{rcs_getctime}{call}->(@_); } #}}} +sub rcs_test_receive ($) { #{{{ + $hooks{rcs}{rcs_test_receive}{call}->(@_); +} #}}} + sub globlist_to_pagespec ($) { #{{{ my @globlist=split(' ', shift); diff --git a/IkiWiki/Plugin/git.pm b/IkiWiki/Plugin/git.pm index 14b0ab285..1facb14c0 100644 --- a/IkiWiki/Plugin/git.pm +++ b/IkiWiki/Plugin/git.pm @@ -23,6 +23,7 @@ sub import { #{{{ hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges); hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff); hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime); + hook(type => "rcs", id => "rcs_test_receive", call => \&rcs_test_receive); } #}}} sub checkconfig () { #{{{ @@ -32,12 +33,21 @@ sub checkconfig () { #{{{ if (! defined $config{gitmaster_branch}) { $config{gitmaster_branch}="master"; } - if (defined $config{git_wrapper} && length $config{git_wrapper}) { + if (defined $config{git_wrapper} && + length $config{git_wrapper}) { push @{$config{wrappers}}, { wrapper => $config{git_wrapper}, wrappermode => (defined $config{git_wrappermode} ? $config{git_wrappermode} : "06755"), }; } + if (defined $config{git_test_receive_wrapper} && + length $config{git_test_receive_wrapper}) { + push @{$config{wrappers}}, { + test_receive => 1, + wrapper => $config{git_test_receive_wrapper}, + wrappermode => "0755", + }; + } } #}}} sub getsetup () { #{{{ @@ -60,6 +70,20 @@ sub getsetup () { #{{{ safe => 0, rebuild => 0, }, + git_test_receive_wrapper => { + type => "string", + example => "/git/wiki.git/hooks/pre-receive", + description => "git pre-receive hook to generate", + safe => 0, # file + rebuild => 0, + }, + git_untrusted_committers => { + type => "string", + example => [], + description => "unix users whose commits should be checked by the pre-receive hook", + safe => 0, + rebuild => 0, + }, historyurl => { type => "string", example => "http://git.example.com/gitweb.cgi?p=wiki.git;a=history;f=[[file]]", @@ -320,6 +344,9 @@ sub parse_diff_tree ($@) { #{{{ 'file' => decode("utf8", $file), 'sha1_from' => $sha1_from[0], 'sha1_to' => $sha1_to, + 'mode_from' => $mode_from[0], + 'mode_to' => $mode_to, + 'status' => $status, }; } next; @@ -331,14 +358,12 @@ sub parse_diff_tree ($@) { #{{{ } #}}} sub git_commit_info ($;$) { #{{{ - # Return an array of commit info hashes of num commits (default: 1) + # Return an array of commit info hashes of num commits # starting from the given sha1sum. - my ($sha1, $num) = @_; - $num ||= 1; - - my @raw_lines = run_or_die('git', 'log', "--max-count=$num", + my @raw_lines = run_or_die('git', 'log', + (defined $num ? "--max-count=$num" : ""), '--pretty=raw', '--raw', '--abbrev=40', '--always', '-c', '-r', $sha1, '--', '.'); my ($prefix) = run_or_die('git', 'rev-parse', '--show-prefix'); @@ -355,7 +380,6 @@ sub git_commit_info ($;$) { #{{{ sub git_sha1 (;$) { #{{{ # Return head sha1sum (of given file). - my $file = shift || q{--}; # Ignore error since a non-existing file might be given. @@ -378,7 +402,6 @@ sub rcs_update () { #{{{ sub rcs_prepedit ($) { #{{{ # Return the commit sha1sum of the file when editing begins. # This will be later used in rcs_commit if a merge is required. - my ($file) = @_; return git_sha1($file); @@ -475,7 +498,7 @@ sub rcs_recentchanges ($) { #{{{ error($@) if $@; my @rets; - foreach my $ci (git_commit_info('HEAD', $num)) { + foreach my $ci (git_commit_info('HEAD', $num || 1)) { # Skip redundant commits. next if ($ci->{'comment'} && @{$ci->{'comment'}}[0] eq $dummy_commit_msg); @@ -558,11 +581,83 @@ sub rcs_getctime ($) { #{{{ $file =~ s/^\Q$config{srcdir}\E\/?//; my $sha1 = git_sha1($file); - my $ci = git_commit_info($sha1); + my $ci = git_commit_info($sha1, 1); my $ctime = $ci->{'author_epoch'}; debug("ctime for '$file': ". localtime($ctime)); return $ctime; } #}}} +sub rcs_test_receive () { #{{{ + # quick success if the user is trusted + my $committer=(getpwuid($<))[0]; + if (! defined $committer) { + error("cannot determine username for $<"); + } + exit 0 if ! ref $config{git_untrusted_committers} || + ! grep { $_ eq $committer } @{$config{git_untrusted_committers}}; + + # The wiki may not be the only thing in the git repo. + # Determine if it is in a subdirectory by examining the srcdir, + # and its parents, looking for the .git directory. + my $subdir=""; + my $dir=$config{srcdir}; + while (! -d "$dir/.git") { + $subdir=IkiWiki::basename($dir)."/".$subdir; + $dir=IkiWiki::dirname($dir); + if (! length $dir) { + error("cannot determine root of git repo"); + } + } + + my @errors; + while (<>) { + chomp; + my ($oldrev, $newrev, $refname) = split(' ', $_, 3); + + # only allow changes to gitmaster_branch + if ($refname !~ /^refs\/heads\/\Q$config{gitmaster_branch}\E$/) { + push @errors, sprintf(gettext("you are not allowed to change %s"), $refname); + } + + foreach my $ci (git_commit_info($oldrev."..".$newrev)) { + foreach my $detail (@{ $ci->{'details'} }) { + my $file = $detail->{'file'}; + + # check that all changed files are in the subdir + if (length $subdir && + ! ($file =~ s/^\Q$subdir\E//)) { + push @errors, sprintf(gettext("you are not allowed to change %s"), $file); + next; + } + + if ($detail->{'mode_from'} ne $detail->{'mode_to'}) { + push @errors, gettext("you are not allowed to change file modes"); + } + + if ($detail->{'status'} =~ /^D+\d*/) { + # TODO check_canremove + } + elsif ($detail->{'status'} !~ /^[MA]+\d*$/) { + push @errors, "unknown status ".$detail->{'status'}; + } + else { + # TODO check_canedit + # TODO check_canattach + } + } + } + } + + if (@errors) { + # TODO clean up objects from failed push + + print STDERR "$_\n" foreach @errors; + exit 1; + } + else { + exit 0; + } +} #}}} + 1 diff --git a/doc/plugins/write.mdwn b/doc/plugins/write.mdwn index 0d244e1f5..5a5db6be0 100644 --- a/doc/plugins/write.mdwn +++ b/doc/plugins/write.mdwn @@ -820,6 +820,15 @@ it up in the history. It's ok if this is not implemented, and throws an error. +#### `rcs_test_receive()` + +This is used to test if changes pushed into the RCS should be accepted. +Ikiwiki will be running as a pre-receive hook (or equivilant) and should +examine the incoming changes, decide if they are allowed, and communicate +that to the RCS. + +This is optional, and doesn't make sense for all RCSs. + ### PageSpec plugins It's also possible to write plugins that add new functions to diff --git a/doc/rcs/details.mdwn b/doc/rcs/details.mdwn index e62f3ef49..089221cab 100644 --- a/doc/rcs/details.mdwn +++ b/doc/rcs/details.mdwn @@ -280,6 +280,9 @@ Here is a how a commit from a remote repository works: * git-commit in the remote repository * git-push, pushes the commit to the master repo on the server +* (Optionally, the master repo's pre-receive hook runs, and checks that the + update only modifies files that the pushing user is allowed to update. + If not, it aborts the receive.) * the master repo's post-update hook notices this update, and runs ikiwiki * ikiwiki notices the modifies page source, and compiles it diff --git a/doc/rcs/git.mdwn b/doc/rcs/git.mdwn index b210af825..2a6feecf5 100644 --- a/doc/rcs/git.mdwn +++ b/doc/rcs/git.mdwn @@ -100,6 +100,33 @@ repository, should only be writable by the wiki's admin, and *not* by the group. Take care that ikiwiki uses a umask that does not cause files in the srcdir to become group writable. (umask 022 will work.) +## git repository with untrusted committers + +By default, anyone who can commit to the git repository can modify any file +on the wiki however they like. A `pre-receive` hook can be set up to limit +incoming commits from untrusted users. Then the same limits that are placed +on edits via the web will be in effect for commits to git for the users. +They will not be allowed to edit locked pages, they will only be able to +delete pages that the [[plugins/remove]] configuration allows them to +remove, and they will only be allowed to add non-page attachments that the +[[plugins/attachment]] configuration allows. + +To enable this, you need to set up the git repository to have multiple +committers. Trusted committers, including the user that ikiwiki runs as, +will not have their commits checked by the `pre-receive` hook. Untrusted +committers will have their commits checked. The configuration settings to +enable are `git_test_receive_wrapper`, which enables generation of a +`pre-receive` hook, and `git_untrusted_committers`, which is a list of +usernames of the untrusted committers. + +Note that when the `pre-receive` hook is checking incoming changes, it +ignores the git authorship information, and uses the username of the unix +user who made the commit. Then tests including the `locked_pages` [[PageSpec]] +are checked to see if that user can edit the pages in the commit. + +You can even set up an anonymous user, to allow anyone to push +changes in via git rather than using the web interface. + ## Optionally using a local wiki to preview changes When working on the "working clones" to add content to your wiki, diff --git a/ikiwiki.in b/ikiwiki.in index fd21b4911..22addb463 100755 --- a/ikiwiki.in +++ b/ikiwiki.in @@ -189,6 +189,9 @@ sub main () { #{{{ elsif ($config{post_commit} && ! commit_hook_enabled()) { # do nothing } + elsif ($config{test_receive}) { + rcs_test_receive(); + } else { if ($config{rebuild}) { debug(gettext("rebuilding wiki..")); |