Index: lib/Foswiki/Plugins/SqlPlugin.pm =================================================================== --- lib/Foswiki/Plugins/SqlPlugin.pm (revision 13534) +++ lib/Foswiki/Plugins/SqlPlugin.pm (working copy) @@ -18,7 +18,7 @@ use strict; our $VERSION = '$Rev: 1340 $'; -our $RELEASE = '1.01'; +our $RELEASE = '1.02'; our $SHORTDESCRIPTION = 'SQL interface for Foswiki'; our $NO_PREFS_IN_TOPIC = 1; our $doneInit; @@ -69,5 +69,23 @@ Foswiki::Plugins::SqlPlugin::Core::handleSQLINFO(@_); } +=begin TML +---++ StaticMethod execute($dbconn, $query, @bindvals) -> $sth + +Executes the provided $query and returns a DBI Statement handle. It is the +caller's responsibility to call $sth->finish after processing is complete. + + $ $dbconn: The database connection defined in the configure screen. + $ $query: The SQL query to be run, possibly with placeholders (?). + $ @bindvals: An OPTIONAL list of values to be applied. Only needed if $query has placeholders. + +Throws Error::Simple on errors. + +=cut +sub execute { + init(); + Foswiki::Plugins::SqlPlugin::Core::handleExecute(@_); +} + 1; Index: lib/Foswiki/Plugins/SqlPlugin/Core.pm =================================================================== --- lib/Foswiki/Plugins/SqlPlugin/Core.pm (revision 13534) +++ lib/Foswiki/Plugins/SqlPlugin/Core.pm (working copy) @@ -22,6 +22,7 @@ our $baseWeb; our $baseTopic; our %connections; +our %accessControls; our %cache; our $defaultDatabase; @@ -41,6 +42,20 @@ $connections{$desc->{id}} = $connection; $defaultDatabase = $desc->{id} unless $defaultDatabase; } + + if (exists $Foswiki::cfg{SqlPlugin}{AccessControl} && $Foswiki::cfg{SqlPlugin}{AccessControl}) { + foreach my $ac (@{$Foswiki::cfg{SqlPlugin}{AccessControl}}) { + my $id = $ac->{id}; + my %ac2; + $ac2{who} = $ac->{who} + if $ac->{who}; +# $ac2{queries} = [ map { uc $_ } @{$ac->{queries}} ] + # This fails horribly for regexes :-( + $ac2{queries} = $ac->{queries} + if $ac->{queries}; + push @{$accessControls{$id}}, \%ac2; + } + } } ############################################################################## @@ -57,6 +72,28 @@ } ############################################################################## +sub handleExecute { + my ($theDatabase, $theQuery, @thePlaceHolders) = @_; + $theDatabase + or $theDatabase = $defaultDatabase; + $theQuery + or throw Error::Simple("No Query provided"); + + checkAccess($theDatabase, $theQuery); + + my $connection = $connections{$theDatabase} + or throw Error::Simple("unknown database '$theDatabase'"); + + $connection->connect(); + + my $sth = $connection->{db}->prepare_cached($theQuery) + or throw Error::Simple("Can't prepare cmd '$theQuery': ".$connection->{db}->errstr); + $sth->execute(@thePlaceHolders) + or throw Error::Simple("Can't execute cmd '$theQuery': ".$connection->{db}->errstr); + return $sth; +} + +############################################################################## sub handleSQL { my ($session, $params, $theTopic, $theWeb) = @_; @@ -65,6 +102,7 @@ my $theDatabase = $params->{database} || $defaultDatabase; my $theId = $params->{id}; my $theQuery = $params->{_DEFAULT} || $params->{query}; + my $theParams = $params->{params} || ''; my $theDecode = $params->{decode} || ''; if ($theDecode eq 'url') { @@ -73,6 +111,8 @@ $theQuery = entityDecode($theQuery); } + my @bindVals = split '\s*,\s*', $theParams; + my $connection = $connections{$theDatabase}; return inlineError("unknown database '$theDatabase'") unless $connection; return inlineError("no query") unless defined $theQuery; @@ -83,12 +123,13 @@ Foswiki::Func::writeWarning("User $wikiName has sent query '$theQuery'"); try { + checkAccess($theDatabase, $theQuery); $connection->connect(); my $sth = $connection->{db}->prepare_cached($theQuery) or throw Error::Simple("Can't prepare cmd '$theQuery': ".$connection->{db}->errstr); - $sth->execute or + $sth->execute(@bindVals) or throw Error::Simple("Can't execute cmd '$theQuery': ".$connection->{db}->errstr); # cache this statement under the given id @@ -252,6 +293,65 @@ return $result; } +############################################################################## +# Check if the currently logged in user has permission to run +# $theQuery on $theDatabase. Thows Error::Simple on access failure. +############################################################################## +sub checkAccess { + my ($theDatabase, $theQuery) = @_; + + if (! %accessControls || ! exists $accessControls{$theDatabase}) { + return; + } + + my $user = Foswiki::Func::getWikiName(); + foreach my $access (@{$accessControls{$theDatabase}}) { + + my $whoPasses = 0; + if (! exists $access->{who} ) { + $whoPasses = 1; + } else { + my $who = $access->{who}; + if ($who eq $user) { + $whoPasses = 1; + } elsif ( Foswiki::Func::isGroupMember( $who)) { + $whoPasses = 1; + } + } + + my $queryPasses = 0; + if (! exists $access->{queries} ) { + $queryPasses = 1; + } else { + my $searchQuery = uc $theQuery; + # convert multiple lines into one + $searchQuery = join ' ', ($searchQuery =~ /^(.*)$/gm); + $searchQuery =~ s/\s+/ /g; + for my $query (@{$access->{queries}}) { + if ($searchQuery eq $query) { + $queryPasses = 1; + last; + } + # Trap regexp compilation errors - we don't care. + eval { + if ($searchQuery =~ /^\s*$query\s*$/) { + $queryPasses = 1; + last; + } + } + } + } + + if ($whoPasses && $queryPasses) { + return; + } + } + + Foswiki::Func::writeWarning("SqlPlugin", "Access control check failed on database '$theDatabase' for query '$theQuery'"); + throw Error::Simple("Access control check failed on database $theDatabase for query $theQuery"); +} + + sub urlDecode { my $text = shift; $text =~ s/%([\da-f]{2})/chr(hex($1))/gei; Index: lib/Foswiki/Plugins/SqlPlugin/Config.spec =================================================================== --- lib/Foswiki/Plugins/SqlPlugin/Config.spec (revision 13534) +++ lib/Foswiki/Plugins/SqlPlugin/Config.spec (working copy) @@ -20,3 +20,29 @@ password => 'foswiki_password', }, ]; + +# **PERL** +#

Access Control

+# Security Configuration. +# This structure is an array of hashes, each of which contains a list of +# queries that are allowed to be run. Each item in the query list is evaluated as a regular expression +# to see if the query matches, and also evaluated for exact string equality to see if the query matches. +# For both of these checks, the input string is converted to ALL UPPERCASE. +# If a database connection has no items defined here, then all queries are permitted. +# Either 'who' or 'queries' can be omitted, but not both. +# +$Foswiki::cfg{SqlPlugin}{AccessControl} = +[ + { + id => 'foswiki', + who => 'WikiUserOrGroup', + queries => [ + 'select * from table1', + 'update table1' + ] + }, +]; Index: data/System/VarSQLFORMAT.txt =================================================================== --- data/System/VarSQLFORMAT.txt (revision 0) +++ data/System/VarSQLFORMAT.txt (revision 0) @@ -0,0 +1,4 @@ +%META:TOPICINFO{author="ProjectContributor" date="1327283252" format="1.1" version="1"}% +%META:TOPICPARENT{name="SqlPlugin"}% + +See documentation at SqlPlugin. Index: data/System/VarSQL.txt =================================================================== --- data/System/VarSQL.txt (revision 0) +++ data/System/VarSQL.txt (revision 0) @@ -0,0 +1,4 @@ +%META:TOPICINFO{author="ProjectContributor" date="1327283252" format="1.1" version="1"}% +%META:TOPICPARENT{name="SqlPlugin"}% + +See documentation at SqlPlugin. Index: data/System/SqlPlugin.txt =================================================================== --- data/System/SqlPlugin.txt (revision 13534) +++ data/System/SqlPlugin.txt (working copy) @@ -16,6 +16,7 @@ | *Parameter* | *Description* | *Default* | | query | sql statement to be executed | | +| params | comma separated list of bind parameters for placeholders (?) in query | | | database | connection to be used | first database in connection pool | | id | identifies the query result to be reusable via SQLFORMAT | | | decode | values are "url" or "entity"; specifies the encoding of the query before being executed | none | @@ -74,6 +75,34 @@ This setting configures three connections - known under the ids =mysql=, =sqlite= and =csv= using different drivers. The first sets up a connection to a mysql database called "foswiki" on the localhost server, protected by the given user and password settings. The second connects to an sqlite database stored at the given path; the third one connects to a database of CSV files stored at the giveh directory. Note, you will need to make sure that the specified driver in the =dsn= parameter is installed on your system. Please look up the individual driver documentation how to specify a correct value for =dsn=. + +---++ Securing database access + +Underneath the database connections section in =[[%SCRIPTURLPATH{configure}%][configure]]=, there is a section to configure access control. +Access can be restricted by wiki user / group, by a 'whitelist' of permitted queries, or both. If no access control is specified for +a database connection, then all access is allowed for that connection. + +Here is an example. Note that the first element of the =queries= list is a regular expression, and +the rest of the entries are literal string matches. + + +[ + { + 'who' => 'KipLubliner', + 'id' => 'mysql', + 'queries' => [ + 'SELECT [^;]+', + 'UPDATE TEAM SET TM_NAME = ? WHERE TMID = ?', + 'UPDATE PLAYER SET PL_NICKNAME = ?, PL_FIRSTNAME = ?, PL_LASTNAME = ?, PL_TEAM = ? WHERE PLID = ?', + 'INSERT INTO TEAM( TM_NAME ) VALUES (?)', + 'INSERT INTO PLAYER( PL_NICKNAME, PL_FIRSTNAME, PL_LASTNAME, PL_TEAM ) VALUES (?, ?, ?, ?)' + ] + } +]; + + +See the integrated documentation on the configure screen for more details. + ---++ Installation Instructions %$INSTALL_INSTRUCTIONS%