chiark / gitweb /
docs: wip
[cgi-auth-flexible.git] / cgi-auth-flexible.pm
index a6fb33cf4660e08f4031332aaa66e374840d0486..500c9e498e63995bac51867ddca34dba2e9e63be 100644 (file)
@@ -48,6 +48,8 @@ use Digest::HMAC;
 use Digest::SHA;
 use Data::Dumper;
 use File::Copy;
+use Cwd qw/realpath/;
+
 
 #---------- public utilities ----------
 
@@ -226,55 +228,71 @@ sub dump_plain ($$$$) {
     }
 }
 
-sub srcdump_process_dir ($$$$$$) {
-    my ($c, $v, $dumpdir, $incdir, $tarballcounter,
-       $needlicence, $dirsdone) = @_;
-    return () if $v->_ch('srcdump_system_dir', $incdir);
-    my $upwards = $incdir;
+sub srcdump_process_item ($$$$$$) {
+    my ($c, $v, $dumpdir, $item, $outfn, $needlicence, $dirsdone) = @_;
+    if ($v->_ch('srcdump_system_dir', $item)) {
+       $outfn->("srcdump_process_item: srcdump_system_dir, skipping $item");
+       return;
+    }
+    my $upwards = $item;
     for (;;) {
        $upwards =~ s#/+$##;
+       $upwards =~ s#/+\.$##;
        last unless $upwards =~ m#[^/]#;
        foreach my $try (@{ $v->{S}{srcdump_vcs_dirs} }) {
-#print STDERR "TRY $incdir $upwards $try\n";
+#print STDERR "TRY $item $upwards $try\n";
            if (!stat "$upwards/$try") {
-               $!==&ENOENT or die "check $upwards/$try $!";
+               $!==&ENOENT or $!==&ENOTDIR or die "check $upwards/$try $!";
                next;
            }
-#print STDERR "VCS $incdir $upwards $try\n";
-           return if $dirsdone->{$upwards}++;
-#print STDERR "VCS $incdir $upwards $try GO\n";
+#print STDERR "VCS $item $upwards $try\n";
+            if ($dirsdone->{$upwards}++) {
+               $outfn->("srcdump_process_item: did $upwards,".
+                        " skipping $item");
+               return;
+           }
+#print STDERR "VCS $item $upwards $try GO\n";
            $try =~ m/\w+/ or die;
-           return $v->_ch(('srcdump_byvcs_'.lc $&),
-                          $dumpdir, $upwards, $tarballcounter);
+           return $v->_ch('srcdump_byvcs', $dumpdir, $upwards, $outfn, lc $&);
        }
-       $upwards =~ s#/*[^/]+##;
+       $upwards =~ s#/*[^/]+$##;
     }
-    return $v->_ch('srcdump_novcs', $dumpdir, $incdir, $tarballcounter);
+    return $v->_ch('srcdump_novcs', $dumpdir, $item, $outfn);
 }
 
 sub srcdump_novcs ($$$$$) {
-    my ($c, $v, $dumpdir, $dir, $tarballcounter) = @_;
-    my $script = 'find -type f -perm +004';
-    foreach my $excl (@{ $v->{S}{srcdump_excludes} }) {
-       $script .= " \\! -name '$excl'";
+    my ($c, $v, $dumpdir, $item, $outfn) = @_;
+    stat $item or die "$item $!";
+    if (-d _) {
+       my $script = 'find -type f -perm +004';
+       foreach my $excl (@{ $v->{S}{srcdump_excludes} }) {
+           $script .= " \\! -name '$excl'";
+       }
+       $script .= " -print0";
+       return srcdump_dir_cpio($c,$v,$dumpdir,$item,$outfn,'novcs',$script);
+    } elsif (-f _) {
+       return srcdump_file($c,$v,$dumpdir,$item,$outfn);
+    } else {
+       die "$item not file or directory";
     }
-    $script .= " -print0";
-    return srcdump_dir_cpio($c,$v,$dumpdir,$dir,$tarballcounter,$script);
 }
 
-sub srcdump_byvcs_git ($$$$$) {
-    my ($c, $v, $dumpdir, $dir, $tarballcounter) = @_;
+sub srcdump_byvcs ($$$$$$) {
+    my ($c, $v, $dumpdir, $dir, $outfn, $vcs) = @_;
 #print STDERR "BYVCS GIT $dir\n";
-    return srcdump_dir_cpio($c,$v,$dumpdir,$dir,$tarballcounter,"
-                 git ls-files -z
-                 git ls-files -z --others --exclude-from=.gitignore
-                 find .git -print0
-                            ");
+    return srcdump_dir_cpio($c,$v,$dumpdir,$dir,$outfn,$vcs,
+                           $v->{S}{"srcdump_vcsscript_$vcs"});
 }
 
-sub srcdump_dir_cpio ($$$$$) {
-    my ($c,$v,$dumpdir,$dir,$tarballcounter,$script) = @_;
-    my $outfile = "$dumpdir/$$tarballcounter.tar";
+sub srcdump_file ($$$$) {
+    my ($c,$v,$dumpdir,$file,$outfn) = @_;
+    my $outfile = $outfn->("srcdump_file saved $file", "src");
+    copy($file,$outfile) or die "$file $outfile $!";
+}
+
+sub srcdump_dir_cpio ($$$$$$$) {
+    my ($c,$v,$dumpdir,$dir,$outfn,$how,$script) = @_;
+    my $outfile = $outfn->("srcdump_dir_cpio $how saved $dir", "tar");
 #print STDERR "CPIO $dir >$script<\n";
     my $pid = fork();
     defined $pid or die $!;
@@ -297,12 +315,7 @@ sub srcdump_dir_cpio ($$$$$) {
        die $!;
     }
     $!=0; (waitpid $pid, 0) == $pid or die "$!";
-    die "$dir ($script) $outfile $?" if $?;
-    print STDERR
-       "CGI::Auth::Flexible srcdump_dir_cpio saved $dir into $outfile\n"
-       or die $!;
-    $$tarballcounter++;
-    return $outfile;
+    die "$dir ($how $script) $outfile $?" if $?;
 }
 
 sub srcdump_dirscan_prepare ($$) {
@@ -319,37 +332,67 @@ sub srcdump_dirscan_prepare ($$) {
            or die "$v->{S}{srcdump_licence_path} $!";
        $needlicence = undef;
     }
-    unlink <"$dumpdir/[a-z][a-z][a-z].tar">;
-    my $srctarballcounter = 'aaa';
+    unlink <"$dumpdir/s.[a-z][a-z][a-z].*">;
+    my @srcfiles = qw(licence.data manifest.txt);
+    my $srcoutcounter = 'aaa';
+
+    my $reportfh = new IO::File "$dumpdir/manifest.txt", 'w' or die $!;
+    my $outfn = sub {
+       my ($message, $extension) = @_;
+       if (defined $extension) {
+           my $leaf = "s.$srcoutcounter.$extension";
+           $srcoutcounter++;
+           push @srcfiles, $leaf;
+           print $reportfh "$leaf: $message\n" or die $!;
+           return "$dumpdir/$leaf";
+       } else {
+           print $reportfh "none: $message\n" or die $!;
+           return undef;
+       }
+    };
     my %dirsdone;
-    my @srcfiles = ("$dumpdir/licence.data");
-    foreach my $incdir ($v->_ch('srcdump_includedirs')) {
-       if ($incdir eq '.' && $v->{S}{srcdump_filter_cwd}) {
+    foreach my $item ($v->_ch('srcdump_listitems')) {
+       if ($item eq '.' && $v->{S}{srcdump_filter_cwd}) {
            my @bad = grep { !m#^/# } values %INC;
-           die "filtering . from srcdump dirs and \@INC but already".
+           die "filtering . from srcdump items and \@INC but already".
                " included @bad " if @bad;
            @INC = grep { $_ ne '.' } @INC;
            next;
        }
-       if (!stat "$incdir/.") {
-           next if $!==&ENOENT;
-           die "stat $incdir $!";
+       if (!lstat "$item") {
+           die "stat $item $!" unless $!==&ENOENT;
+           $outfn->("srcdump_dirscan_prepare stat ENOENT, skipping $item");
+           next;
        };
+       if (-l _) {
+           $item = realpath($item);
+           if (!defined $item) {
+               die "realpath $item $!" unless $!==&ENOENT;
+               $outfn->("srcdump_dirscan_prepare realpath ENOENT,".
+                        " skipping $item");
+           }
+       }
        if (defined $needlicence) {
            foreach my $try (@{ $v->{S}{srcdump_licence_files} }) {
-               last if copy("$incdir/$try", $needlicence);
-               $!==&ENOENT or die "copy $incdir/$try $!";
+               last if copy("$item/$try", $needlicence);
+               $!==&ENOENT or $!==&ENOTDIR or die "copy $item/$try $!";
            }
        }
-       push @srcfiles, $v->_ch('srcdump_process_dir', $dumpdir, $incdir,
-                               \$srctarballcounter, \$needlicence, \%dirsdone);
-       $dirsdone{$incdir}++;
+       $v->_ch('srcdump_process_item', $dumpdir, $item,
+               $outfn, \$needlicence, \%dirsdone);
+       $dirsdone{$item}++;
     }
+    close $reportfh or die $!;
+    srcdump_install($c,$v, $dumpdir, 'licence', 'text/plain');
     $!=0;
-    my $r = system qw(tar -zvvc -f), "$dumpdir/source.tmp", '--', @srcfiles;
-    die "tar $r $!" if $r;
+    my @cmd = (qw(tar -zvvcf), "$dumpdir/source.tmp",
+              "-C", $dumpdir, qw(  --), @srcfiles);
+    my $r = system(@cmd);
+    if ($r) {
+       print STDERR "CGI::Auth::Flexible tar failed ($r $!) @cmd\n";
+       die "tar failed";
+    }
     die "licence file not found" unless defined $needlicence;
-    srcdump_install($c,$v, $dumpdir, 'licence', 'text/plain');
     srcdump_install($c,$v, $dumpdir, 'source', 'application/octet-stream');
     close $lockf or die $!;
 }
@@ -427,15 +470,23 @@ sub new_verifier {
            srcdump_prepare => \&srcdump_dirscan_prepare,
            srcdump_licence_path => undef,
            srcdump_licence_files => [qw(AGPLv3 CGI/Auth/Flexible/AGPLv3)],
-           srcdump_includedirs => sub { return @INC; },
+           srcdump_listitems => sub { (@INC, $ENV{'SCRIPT_FILENAME'}, $0); },
            srcdump_filter_cwd => 1,
-           srcdump_system_dir => sub { $_[2] =~ m#^/etc/|^/usr/(?!local/)#; },
-           srcdump_process_dir => \&srcdump_process_dir,
-           srcdump_vcs_dirs => [qw(.git .hg .svn CVS)],
-           srcdump_byvcs_git => \&srcdump_byvcs_git,
-           srcdump_byvcs_hg => \&srcdump_byvcs_hg,
-           srcdump_byvcs_svn => \&srcdump_byvcs_svn,
-           srcdump_byvcs_cvs => \&srcdump_byvcs_cvs,
+           srcdump_system_dir => sub {
+               $_[2] =~ m#^/etc/|^/usr/(?!local/)(?!lib/cgi)#;
+           },
+           srcdump_process_item => \&srcdump_process_item,
+           srcdump_vcs_dirs => [qw(.git .hg .bzr .svn CVS)],
+           srcdump_vcsscript_git => "
+                 git ls-files -z
+                 git ls-files -z --others --exclude-from=.gitignore
+                 find .git -print0
+                            ",
+           srcdump_vcsscript_hg => "false hg",
+           srcdump_vcsscript_bzr => "false bzr",
+           srcdump_vcsscript_svn => "false svn",
+           srcdump_vcsscript_cvs => "false cvs",
+           srcdump_byvcs => \&srcdump_byvcs,
            srcdump_novcs => \&srcdump_novcs,
            srcdump_excludes => [qw(*~ *.bak *.tmp), '#*#'],
            dump => \&dump_plain,
@@ -1274,31 +1325,222 @@ CGI::Auth::Flexible - web authentication optionally using cookies
 =head1 SYNOPSYS
 
  my $verifier = CGI::Auth::Flexible->new_verifier(setting => value,...);
- my $authreq = $verifier->new_request($cgi_request_object);
-
- my $authreq = CGI::Auth::Flexible->new_request($cgi_request_object,
-                                              setting => value,...);
-
-=head1 USAGE PATTERN FOR SIMPLE APPLICATIONS
+ my $authreq = $verifier->new_request($cgi_query_object);
 
+ # simple applications
  $authreq->check_ok() or return;
 
- blah blah blah
+ # sophisticated applications
+ my $divert_kind = $authreq->check_divert();
+ if ($divert_kind) { ... print diversion page and quit ... }
+
+ # while handling the request
+ $user = $authreq->get_username();
  $authreq->check_mutate();
- blah blah blah
 
-=head1 USAGE PATTERN FOR FANCY APPLICATIONS
+=head1 DESCRIPTION
 
- my $divert_kind = $authreq->check_divert();
- if ($divert_kind) {
-     if ($divert_kind eq 'LOGGEDOUT') {
-        print "goodbye you are now logged out" and quit
-     } elsif ($divert_kind eq 'NOCOOKIES') {
-        print "you need cookies" and quit
-     ... etc.
-     }
- }
-
- blah blah blah
- $authreq->check_mutate();
- blah blah blah
+CGI::Auth::Flexible is a library which you can use to add a
+forms/cookie-based login facility to a Perl web application.
+
+CGI::Auth::Flexible doesn't interfere with your application's URL path
+namespace and just needs a few (configurable) form parameter and
+cookie name(s) for its own use.  It tries to avoid making assumptions
+about the implementation structure of your application.
+
+Because CGI::Auth::Flexible is licenced under the AGPLv3, you will
+probably need to provide a facility to allow users (even ones not
+logged in) to download the source code for your web app.  Conveniently
+by default CGI::Auth::Flexible provides (for pure Perl webapps) a
+mechanism for users to get the source.
+
+CGI::Auth::Flexible is designed to try to stop you accidentally
+granting access by misunderstanding the API.  (Also it, of course,
+guards against cross-site scripting.)  You do need to make sure to
+call CGI::Auth::Flexible before answering AJAX requests as well as
+before generating HTML pages, of course, and to call it in every
+entrypoint to your system.
+
+=head2 INITIALISATION
+
+Your application should, on startup (eg, when it is loaded by
+mod_perl) do
+C<< $verifier = CGI::Auth::Flexible->new_verifier(settings...) >>.
+This call can be expensive and is best amortised.
+
+The resulting verifier object can be used to process individual
+requests, in each case with
+C<< $authreq = CGI::Auth::Flexible->new_request($cgi_query) >>.
+
+=head2 SIMPLE APPLICATIONS
+
+The simplist usage is to call C<< $request->check_ok() >> which will
+check the user's authentication.  If the user is not logged in it will
+generate a login form (or redirection or other appropriate page) and
+return false; your application should not then processing that request
+any further.  If the user is logged in it will return true.
+
+After calling C<check_ok> you can use C<< $request->get_username >>
+to find out which user the request came from.
+
+=head2 SOPHISTICATED APPLICATIONS
+
+If you want to handle the flow control and to generate login forms,
+redirections, etc., yourself, you can say
+C<< $divert = $request->check_divert >>.  This returns undef if
+the user is logged in, or I<divert spec> if some kind of login
+page or diversion should be generated.
+
+=head2 GENERATING (MUTATING) FORMS AND AJAX QUERIES
+
+When you generate a C<POST> form or AJAX request you need to include a
+special secret hidden form parameter for the benefit of
+CGI::Auth::Generic.  This form parameter will be checked by
+C<check_ok>/C<check_divert> and should be ignored by your application.
+
+By default the hidden parameter is called C<caf_assochash>.  After
+calling C<check_ok> or C<check_divert> the value to put in your form
+can be obtained from C<secret_hidden_val>; C<secret_hidden_html> will
+generate the whole HTML C<< <input...> >> element.
+
+Do not put the secret value in URLs for C<GET> requests.
+
+=head2 MUTATING OPERATIONS AND EXTERNAL LINKS INTO YOUR SITE
+
+By default CGI::Auth::Flexible does not permit external links into
+your site.  All GET requests give a "click to continue" page which
+submits a form which loads your app's main page.  In this
+configuration all your application's forms and AJAX requests should
+use C<POST>.  This restriction arises from complicated deficiencies
+in the web's security architecture.  
+
+The alternative is for your application to always make a special check
+when the incoming request is going to do some kind of action (such as
+modifying the user's setup, purchasing goods, or whatever) rather than
+just display HTML pages.  Then non-mutating pages can be linked to
+from other, untrustworthy, websites.
+
+To support external links, and C<GET> requests, pass
+C<< promise_check_mutate => 1 >> in I<settings>, and then call
+C<< $authreq->check_mutate() >> before taking any actions.  If the
+incoming request is not suitable then C<< $authreq->check_mutate() >>
+will call C<die>.
+
+You must make sure that you have no mutating C<GET> requests in your
+application - but you shouldn't have any of those anyway.
+
+=head2 DATA STORAGE
+
+CGI::Auth::Flexible needs to store various information in plain files;
+it does this in the directory specified by the C<dir> parameter.
+
+=head1 SOURCE CODE DOWNLOAD
+
+By default, CGI::Auth::Flexible provides a facility for users to
+download the source code for the running version of your web
+application.
+
+This facility makes a number of important assumptions which you need
+to check.  Note that if the provided facility is not sufficient
+because your application is more sophisticated than it copes with (or
+if you disable the builtin facility), you may need to implement a
+functioning alternative to avoid violating the AGPLv3 licence.
+
+Here are the most important (default) assumptions:
+
+=over
+
+=item *
+
+Your app's source code is available by looking at @INC, $0 and
+S<$ENV{'SCRIPT_FILENAME'}> (the B<source items>).  See
+C<srcdump_listitems>.  Where these point to files or directories under
+revision control, the source item is the whole containing vcs tree.
+
+=item *
+
+Specifically, there are no compiled or autogenerated Perl
+files, Javascript resources, etc., which are not contained in one of
+the source item directories.  (Files which came with your operating
+system install don't need to be shipped as they fall under the system
+library exceptio.)
+
+=item *
+
+You have not installed any modified versions of system
+libraries (including system-supplied) Perl modules in C</usr> outside
+C</usr/local>.  See C<srcdump_system_dir>.
+
+=item *
+
+For each source item in a dvcs, the entire dvcs history does
+not contain anything confidential (or libellous).  Also, all files which
+contain secrets are in the dvcs's C<.ignore> file.  See
+C<srcdump_vcsscript_git> et al.
+
+=item *
+
+For each source item NOT in a dvcs, there are no confidential
+files with the world-readable bit set (being in a world-inaccessible
+directory is not sufficient).  See C<srcdump_excludes>.
+
+=item *
+
+You have none of your app's source code in C</etc>.
+
+=item *
+
+You don't regard pathnames on your server as secret.
+
+=item *
+
+You don't intentionally load Perl code by virtule of C<.>
+being in C<@INC> by default.  (See C<srcdump_filter_cwd>.)
+
+=back
+
+=head1 MAIN FUNCTIONS AND METHODS
+
+=over
+
+=item C<< CGI::Auth::Flexible->new_verifier(setting => value, ...) >>
+
+Initialises an instance and returns a verifier object.
+The arguments are setting pairs like a hash initialiser.
+See L</SETTINGS> below.
+
+=item C<< $verifier->new_request($cgi_query) >>
+
+Prepares to process a request.  C<$cgi_query> should normally
+be the query object from L<CGI(3perl)>.  Most of the default
+hook methods assume that it is; however if you replace enough of
+the hook methods then you can pass any value you like and it
+will be passed to your hooks.
+
+The return value is the authentication request object (C<$authreq>)
+which is used to check the incoming request and will contain
+information about its credentials.
+
+=item C<< $authreq->check_divert() >>
+
+Checks whether the user is logged in.  Returns undef if the user is
+logged in and we should service the request.  Otherwise returns a
+divert spec (see L</DIVERT SPEC>) saying what should happen instead.
+
+This method may die if it doesn't like the request, in which case
+the request needs to be rejected.
+
+=item C<< $authreq->check_ok() >>
+
+Checks whether the user is logged in.  Returns true if the user is
+logged in and we should service the request.
+
+Otherwise it handles the request itself, generating any appropriate
+redirect, login form, or continuation page.  It then returns false and
+the application should not process the request further.
+
+=item C<< $verifier->disconnect() >>
+
+Discards the resources (open files, etc.) in the verifier object.
+
+=back