#!/usr/bin/perl -w # # Tests for the wallet-backend dispatch code. # # Written by Russ Allbery # Copyright 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014 # The Board of Trustees of the Leland Stanford Junior University # # See LICENSE for licensing terms. use strict; use Test::More tests => 1311; # Create a dummy class for Wallet::Server that prints what method was called # with its arguments and returns data for testing. package Wallet::Server; use vars qw($error $okay); $error = 0; $okay = 0; sub error { if ($okay) { $okay = 0; return; } else { $error++; return "error count $error"; } } sub new { shift; print "new @_\n"; return bless ({}, 'Wallet::Server') } sub create { shift; print "create @_\n"; ($_[0] eq 'error') ? undef : 1 } sub destroy { shift; print "destroy @_\n"; ($_[0] eq 'error') ? undef : 1 } sub store { shift; print "store @_\n"; ($_[0] eq 'error') ? undef : 1 } sub acl_add { shift; print "acl_add @_\n"; ($_[0] eq 'error') ? undef : 1 } sub acl_create { shift; print "acl_create @_\n"; ($_[0] eq 'error') ? undef : 1 } sub acl_destroy { shift; print "acl_destroy @_\n"; ($_[0] eq 'error') ? undef : 1 } sub acl_remove { shift; print "acl_remove @_\n"; ($_[0] eq 'error') ? undef : 1 } sub acl_rename { shift; print "acl_rename @_\n"; ($_[0] eq 'error') ? undef : 1 } sub acl_check { shift; print "acl_check @_\n"; if ($_[0] eq 'error') { return; } elsif ($_[0] eq 'unknown') { return 0; } else { return 1; } } sub acl_history { shift; print "acl_history @_\n"; return if $_[0] eq 'error'; return 'acl_history'; } sub acl_show { shift; print "acl_show @_\n"; return if $_[0] eq 'error'; return 'acl_show'; } sub flag_clear { shift; print "flag_clear @_\n"; ($_[0] eq 'error') ? undef : 1 } sub flag_set { shift; print "flag_set @_\n"; ($_[0] eq 'error') ? undef : 1 } sub acl { shift; print "acl @_\n"; if ($_[0] eq 'error') { return; } elsif ($_[1] eq 'empty') { $okay = 1; return; } else { return 'acl'; } } sub attr { shift; print "attr @_\n"; if ($_[0] eq 'error') { return; } elsif ($_[1] eq 'empty') { $okay = 1; return; } elsif (@_ == 3) { return ('attr1', 'attr2'); } else { return 'attr'; } } sub autocreate { shift; print "autocreate @_\n"; return ($_[0] eq 'error') ? undef : 1 } sub check { shift; print "check @_\n"; if ($_[0] eq 'error') { return; } elsif ($_[1] eq 'empty') { return 0; } else { return 1; } } sub comment { shift; print "comment @_\n"; if ($_[0] eq 'error') { return; } elsif ($_[1] eq 'empty') { $okay = 1; return; } else { return 'comment'; } } sub expires { shift; print "expires @_\n"; if ($_[0] eq 'error') { return; } elsif ($_[1] eq 'empty') { $okay = 1; return; } else { return 'expires'; } } sub get { shift; print "get @_\n"; return if $_[0] eq 'error'; return 'get'; } sub history { shift; print "history @_\n"; return if $_[0] eq 'error'; return 'history'; } sub owner { shift; print "owner @_\n"; if ($_[0] eq 'error') { return; } elsif ($_[1] eq 'empty') { $okay = 1; return; } else { return 'owner'; } } sub show { shift; print "show @_\n"; return if $_[0] eq 'error'; return 'show'; } # Back to the main package and the actual test suite. Lie about whether the # Wallet::Server package has already been loaded. package main; $INC{'Wallet/Server.pm'} = 'FAKE'; my $OUTPUT; our $SYSLOG = \$OUTPUT; my $INPUT = ''; eval { do "$ENV{SOURCE}/../server/wallet-backend" }; # Run the wallet backend. This fun hack takes advantage of the fact that the # wallet backend is written in Perl so that we can substitute our own # Wallet::Server class. sub run_backend { my (@args) = @_; my $result = ''; open (OUTPUT, '>', \$result) or die "cannot create output string: $!\n"; select OUTPUT; close STDIN; open (STDIN, '<', \$INPUT) or die "cannot change stdin: $!\n"; local $| = 1; eval { command (@args) }; my $error = $@; select STDOUT; return ($result, $error); } # Now for the actual tests. First, check for lack of trace information. my ($out, $err) = run_backend; is ($err, "REMOTE_USER not set\n", 'REMOTE_USER required'); is ($OUTPUT, "error: REMOTE_USER not set\n", ' and syslog correct'); $ENV{REMOTE_USER} = 'admin'; ($out, $err) = run_backend; is ($err, "neither REMOTE_HOST nor REMOTE_ADDR set\n", 'REMOTE_HOST or _ADDR required'); is ($OUTPUT, "error for admin: neither REMOTE_HOST nor REMOTE_ADDR set\n", ' and syslog correct'); $ENV{REMOTE_ADDR} = '1.2.3.4'; my $new = 'new admin 1.2.3.4'; # Check unknown commands. ($out, $err) = run_backend ('foo'); is ($err, "unknown command foo\n", 'Unknown command'); is ($OUTPUT, "error for admin (1.2.3.4): unknown command foo\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); ($out, $err) = run_backend ('acl', 'foo'); is ($err, "unknown command acl foo\n", 'Unknown ACL command'); is ($OUTPUT, "error for admin (1.2.3.4): unknown command acl foo\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); ($out, $err) = run_backend ('flag', 'foo', 'service', 'foo', 'foo'); is ($err, "unknown command flag foo\n", 'Unknown flag command'); is ($OUTPUT, "error for admin (1.2.3.4): unknown command flag foo\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); # Check too few, too many, and bad arguments for every command. my %commands = (autocreate => [2, 2], check => [2, 2], comment => [2, 3], create => [2, 2], destroy => [2, 2], expires => [2, 3], get => [2, 2], getacl => [3, 3], getattr => [3, 3], history => [2, 2], owner => [2, 3], setacl => [4, 4], setattr => [4, 9], show => [2, 2], store => [2, 3]); my %acl_commands = (add => [3, 3], check => [1, 1], create => [1, 1], destroy => [1, 1], history => [1, 1], remove => [3, 3], rename => [2, 2], show => [1, 1]); my %flag_commands = (clear => [3, 3], set => [3, 3]); for my $command (sort keys %commands) { my ($min, $max) = @{ $commands{$command} }; ($out, $err) = run_backend ($command, ('foo') x ($min - 1)); is ($err, "insufficient arguments\n", "Too few arguments for $command"); is ($OUTPUT, "error for admin (1.2.3.4): insufficient arguments\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); unless ($max >= 9) { ($out, $err) = run_backend ($command, ('foo') x ($max + 1)); is ($err, "too many arguments\n", "Too many arguments for $command"); is ($OUTPUT, "error for admin (1.2.3.4): too many arguments\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); } my @base = ('foobar') x $max; for my $arg (0 .. ($max - 1)) { my @args = @base; $args[$arg] = 'foo;bar'; ($out, $err) = run_backend ($command, @args); if (($command eq 'store' or $command eq 'comment') and $arg == 2) { is ($err, '', 'Store and comment allow any characters'); if ($command eq 'store') { is ($OUTPUT, "command $command @args[0,1] from admin" . " (1.2.3.4) succeeded\n", ' and success logged'); } else { is ($OUTPUT, "command $command @args from admin" . " (1.2.3.4) succeeded\n", ' and success logged'); } is ($out, "$new\n$command foobar foobar foo;bar\n", ' and calls the right method'); } else { is ($err, "invalid characters in argument: foo;bar\n", "Invalid arguments for $command $arg"); is ($OUTPUT, "error for admin (1.2.3.4): invalid characters in" . " argument: foo;bar\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); } } } for my $command (sort keys %acl_commands) { my ($min, $max) = @{ $acl_commands{$command} }; ($out, $err) = run_backend ('acl', $command, ('foo') x ($min - 1)); is ($err, "insufficient arguments\n", "Too few arguments for acl $command"); is ($OUTPUT, "error for admin (1.2.3.4): insufficient arguments\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); ($out, $err) = run_backend ('acl', $command, ('foo') x ($max + 1)); is ($err, "too many arguments\n", "Too many arguments for acl $command"); is ($OUTPUT, "error for admin (1.2.3.4): too many arguments\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); my @base = ('foobar') x $max; for my $arg (0 .. ($max - 1)) { my @args = @base; $args[$arg] = 'foo;bar'; ($out, $err) = run_backend ('acl', $command, @args); if (($command eq 'add' or $command eq 'remove') and $arg == 2) { is ($err, '', 'Add/remove allows any characters'); is ($OUTPUT, "command acl $command @args[0..2] from admin" . " (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\nacl_$command @args[0..2]\n", ' and calls the right method'); } else { is ($err, "invalid characters in argument: foo;bar\n", "Invalid arguments for acl $command $arg"); is ($OUTPUT, "error for admin (1.2.3.4): invalid characters in" . " argument: foo;bar\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); } } } for my $command (sort keys %flag_commands) { my ($min, $max) = @{ $flag_commands{$command} }; ($out, $err) = run_backend ('flag', $command, ('foo') x ($min - 1)); is ($err, "insufficient arguments\n", "Too few arguments for flag $command"); is ($OUTPUT, "error for admin (1.2.3.4): insufficient arguments\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); ($out, $err) = run_backend ('flag', $command, ('foo') x ($max + 1)); is ($err, "too many arguments\n", "Too many arguments for flag $command"); is ($OUTPUT, "error for admin (1.2.3.4): too many arguments\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); my @base = ('foobar') x $max; for my $arg (0 .. ($max - 1)) { my @args = @base; $args[$arg] = 'foo;bar'; ($out, $err) = run_backend ('flag', $command, @args); is ($err, "invalid characters in argument: foo;bar\n", "Invalid arguments for flag $command $arg"); is ($OUTPUT, "error for admin (1.2.3.4): invalid characters in" . " argument: foo;bar\n", ' and syslog correct'); is ($out, "$new\n", ' and nothing ran'); } } # Now, test that we ran the right functions and passed the correct arguments. my $error = 1; for my $command (qw/autocreate create destroy setacl setattr store/) { my $method = { setacl => 'acl', setattr => 'attr' }->{$command}; $method ||= $command; my @extra = ('foo') x ($commands{$command}[0] - 2); my $extra = @extra ? join (' ', '', @extra) : ''; $extra = ' ' if $command eq 'store'; ($out, $err) = run_backend ($command, 'type', 'name', @extra); my $ran; if ($command eq 'store') { $ran = "$command type name"; } else { $ran = "$command type name" . (@extra ? " @extra" : ''); } is ($err, '', "Command $command ran with no errors"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\n$method type name$extra\n", ' and ran the right method'); ($out, $err) = run_backend ($command, 'error', 'name', @extra); if ($command eq 'store') { $ran = "$command error name"; } else { $ran = "$command error name" . (@extra ? " @extra" : ''); } is ($err, "error count $error\n", "Command $command ran with errors"); is ($OUTPUT, "command $ran from admin (1.2.3.4) failed: error count" . " $error\n", ' and syslog correct'); is ($out, "$new\n$method error name$extra\n", ' and ran the right method'); $error++; } for my $command (qw/check comment expires get getacl getattr history owner show/) { my $method = { getacl => 'acl', getattr => 'attr' }->{$command}; $method ||= $command; my @extra = ('foo') x ($commands{$command}[0] - 2); my $extra = @extra ? join (' ', '', @extra) : ''; ($out, $err) = run_backend ($command, 'type', 'name', @extra); my $ran = "$command type name" . (@extra ? " @extra" : ''); is ($err, '', "Command $command ran with no errors"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); if ($command eq 'getattr') { is ($out, "$new\n$method type name$extra\nattr1\nattr2\n", ' and ran the right method with output'); } elsif ($command eq 'check') { is ($out, "$new\n$method type name$extra\nyes\n", ' and ran the right method with output'); } else { my $newline = ($command =~ /^(get|history|show)\z/) ? '' : "\n"; is ($out, "$new\n$method type name$extra\n$method$newline", ' and ran the right method with output'); } if ($command eq 'expires' or $command eq 'owner' or $command eq 'comment') { ($out, $err) = run_backend ($command, 'type', 'name', @extra, 'foo'); my $ran = "$command type name" . (@extra ? " @extra" : '') . ' foo'; is ($err, '', "Command $command ran with no errors (setting)"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\n$method type name$extra foo\n", ' and ran the right method'); } if ($command eq 'expires' or $command eq 'getacl' or $command eq 'owner' or $command eq 'comment') { ($out, $err) = run_backend ($command, 'type', 'empty', @extra); my $ran = "$command type empty" . (@extra ? " @extra" : ''); is ($err, '', "Command $command ran with no errors (empty)"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); my $desc; if ($command eq 'comment') { $desc = 'comment' } elsif ($command eq 'expires') { $desc = 'expiration' } elsif ($command eq 'getacl') { $desc = 'ACL' } elsif ($command eq 'owner') { $desc = 'owner' } is ($out, "$new\n$method type empty$extra\nNo $desc set\n", ' and ran the right method with output'); $error++; } elsif ($command eq 'getattr') { ($out, $err) = run_backend ($command, 'type', 'empty', @extra); my $ran = "$command type empty" . (@extra ? " @extra" : ''); is ($err, '', "Command $command ran with no errors (empty)"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\n$method type empty$extra\n", ' and ran the right method with output'); $error++; } elsif ($command eq 'check') { ($out, $err) = run_backend ($command, 'type', 'empty', @extra); my $ran = "$command type empty" . (@extra ? " @extra" : ''); is ($err, '', "Command $command ran with no errors (empty)"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\n$method type empty$extra\nno\n", ' and ran the right method with output'); } ($out, $err) = run_backend ($command, 'error', 'name', @extra); $ran = "$command error name" . (@extra ? " @extra" : ''); is ($err, "error count $error\n", "Command $command ran with errors"); is ($OUTPUT, "command $ran from admin (1.2.3.4) failed: error count" . " $error\n", ' and syslog correct'); is ($out, "$new\n$method error name$extra\n", ' and ran the right method'); $error++; } for my $command (sort keys %acl_commands) { my @extra = ('foo') x ($acl_commands{$command}[0] - 1); my $extra = @extra ? join (' ', '', @extra) : ''; ($out, $err) = run_backend ('acl', $command, 'name', @extra); my $ran = "acl $command name" . (@extra ? " @extra" : ''); is ($err, '', "Command acl $command ran with no errors"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); my $expected; if ($command eq 'check') { $expected = "$new\nacl_$command name$extra\nyes\n"; } elsif ($command eq 'show') { $expected = "$new\nacl_$command name$extra\nacl_show"; } elsif ($command eq 'history') { $expected = "$new\nacl_$command name$extra\nacl_history"; } else { $expected = "$new\nacl_$command name$extra\n"; } is ($out, $expected, ' and ran the right method'); ($out, $err) = run_backend ('acl', $command, 'error', @extra); $ran = "acl $command error" . (@extra ? " @extra" : ''); is ($err, "error count $error\n", "Command acl $command ran with errors"); is ($OUTPUT, "command $ran from admin (1.2.3.4) failed: error count" . " $error\n", ' and syslog correct'); is ($out, "$new\nacl_$command error$extra\n", ' and ran the right method'); $error++; if ($command eq 'check') { ($out, $err) = run_backend ('acl', $command, 'unknown'); my $ran = "acl $command unknown"; is ($err, '', "Command $command ran with no errors (unknown)"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\nacl_$command unknown\nno\n", ' and ran the right method with output'); } } for my $command (sort keys %flag_commands) { my @extra = ('foo') x ($flag_commands{$command}[0] - 2); my $extra = @extra ? join (' ', '', @extra) : ''; ($out, $err) = run_backend ('flag', $command, 'type', 'name', @extra); my $ran = "flag $command type name" . (@extra ? " @extra" : ''); is ($err, '', "Command flag $command ran with no errors"); is ($OUTPUT, "command $ran from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\nflag_$command type name$extra\n", ' and ran the right method'); ($out, $err) = run_backend ('flag', $command, 'error', 'name', @extra); $ran = "flag $command error name" . (@extra ? " @extra" : ''); is ($err, "error count $error\n", "Command flag $command ran with errors"); is ($OUTPUT, "command $ran from admin (1.2.3.4) failed: error count" . " $error\n", ' and syslog correct'); is ($out, "$new\nflag_$command error name$extra\n", ' and ran the right method'); $error++; } # Special check for store allowing nul characters on standard input. $INPUT = "Some data\000with a nul character"; ($out, $err) = run_backend ('store', 'type', 'name'); is ($err, '', 'store with nul data ran with no errors'); is ($OUTPUT, "command store type name from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\nstore type name $INPUT\n", ' and ran the right method'); $INPUT = ''; ($out, $err) = run_backend ('store', 'type', 'name'); is ($err, '', 'store with empty stdin data ran with no errors'); is ($OUTPUT, "command store type name from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\nstore type name \n", ' and ran the right method'); # Almost done. All that remains is to test the robustness of the bad # character checks against every possible character and test permitting the # empty argument. ($out, $err) = run_backend ('show', 'type', ''); is ($err, '', 'Allowed the empty argument'); is ($OUTPUT, "command show type from admin (1.2.3.4) succeeded\n", ' and success logged'); my $ok = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/.@-'; ($out, $err) = run_backend ('show', 'type', $ok); is ($err, '', 'Allowed all valid characters'); is ($OUTPUT, "command show type $ok from admin (1.2.3.4) succeeded\n", ' and success logged'); is ($out, "$new\nshow type $ok\nshow", ' and returned the right output'); for my $n (0 .. 255) { my $c = chr ($n); my $name = $ok . $c; ($out, $err) = run_backend ('show', 'type', $name); if (index ($ok, $c) == -1) { is ($err, "invalid characters in argument: $name\n", "Rejected invalid character $n"); my $stripped = $name; $stripped =~ s/[^\x20-\x7e]/_/g; is ($OUTPUT, "error for admin (1.2.3.4): invalid characters in" . " argument: $stripped\n", ' and syslog correct'); is ($out, "$new\n", ' and did nothing'); } else { is ($err, '', "Accepted valid character $n"); is ($OUTPUT, "command show type $name from admin (1.2.3.4)" . " succeeded\n", ' and success logged'); is ($out, "$new\nshow type $name\nshow", ' and ran the method'); } }