%META:TOPICINFO{author="ChrisHoefler" date="1457496285" format="1.1" version="1"}% %META:TOPICPARENT{name="Plugins"}% ----+!! %TOPIC% Provides JSON handlers for interacting with SQL databases %STARTSECTION{"summary"}% [[https://metacpan.org/pod/JsonSQL][JsonSQL]] is a CPAN module that generates SQL from well-formed JSON that conforms to a prescribed JSON schema. It supports most of the common SQL features for SELECT and INSERT statements, as well as a whitelisting mechanism that blocks SQL generation for tables and table columns that are not explicitly allowed. See [[https://metacpan.org/pod/JsonSQL]] for up-to-date documentation. This plugin provides a set of JSON-RPC handlers to receive submitted JSON, use !JsonSQL to generate SQL, perform the query, and return the results. This positions JsonSQLPlugin as a shim between Foswiki and an SQL database, proxying requests and data between the two. %ENDSECTION{"summary"}% %TOC% ---++ Overview Consider you have some data in an SQL database that you would like to retrieve and display for Foswiki users. You use the following SELECT query, SELECT * from product_table This is a simple, static query that is probably best hardcoded into a Perl plugin. No need for anything fancy. The plugin does the query and returns a TML-formatted result. However, it is not so straightforward if you desire a more complex query. For example, SELECT product_id,product_name,quantity,unit_price FROM product_table WHERE ( product_id BETWEEN 3 AND 35 ) AND quantity > 5 If you need to generate a query like the above using user input, you can attempt to capture the parameter space in an HTML form in a user-friendly way, but input validation, query processing, and result formatting will have to be handled specifically for each query. If you have more than a few queries, or if your queries need to change in response to user input (ex: modifying the WHERE clause), this quickly becomes untenable. Enter JsonSQLPlugin. By leveraging the !JsonSQL CPAN module, the above query becomes a snippet of JSON, { "fields": [ {"column": "product_id"}, {"column": "product_name"}, {"column": "quantity"}, {"column": "unit_price"} ], "from": [ {"table": "product_table"} ], "where": { "and": [ { "bt": {"field": {"column": "product_id"}, "minvalue": 3, "maxvalue": 35} }, { "gt": {"field": {"column": "quantity"}, "value": 5} } ] } } Since JSON is native to !JavaScript and the structure of the object is standardized by !JsonSQL, it can be easily generated on the client in response to user input. The JSON is transferred to the server via RPC, allowing for asynchronous processing, and server-side validation is also handled in a standardized way. Once the query is performed the result is returned as JSON, allowing for easy formatting and display using [[System.JQueryRender][JsRender]] templates or similar. ---++ !JsonSQL CPAN Module The current !JsonSQL modules support SELECT and INSERT queries. Additional queries will be supported in the future, but with the current functionality you can generate SQL queries supporting a wide-range of common needs, including JOINS, DB schemas, and a very flexible WHERE clause. The SQL is generated with parameterized inputs to guard against SQL injection. This makes it useful for major databases such as !PostgreSQL, !MySQL, SQL Server, and Oracle. For other databases (ex: SQLite), verify support for parameterized input as well as support by the Perl DBI module before attempting to use with this plugin. SQL generation follows a fixed format and is not currently configurable. A simple SELECT statement, { "fields": [ {"column": "*"} ], "from": [ {"table": "my_table"} ] } Generates: SELECT * FROM "my_table" A SELECT with JOIN, { "fields": [ {"column": "field1"}, {"column": "field2", "alias": "test"} ], "joins": [ {"jointype": "inner", "from": {"table": "table1", "schema": "MySchema"}, "to": {"table": "table2", "schema": "MySchema"}, "on": {"eq": {"field": {"column": "field2"}, "value": {"column": "field1"}} }} ] } Generates: SELECT "field1","field2" AS "test" FROM "MySchema"."table1" INNER JOIN "MySchema"."table2" ON "field2" = "field1" A simple INSERT, { "inserts": [ { "table": {"table": "table1", "schema": "MySchema"}, "values": [ {"column": "column1", "value": "value1"}, {"column": "column2", "value": "value2"} ] }, { "table": {"table": "table2"}, "values": [ {"column": "columnA", "value": "valueA"}, {"column": "columnB", "value": "valueB"} ] } ] } Generates: INSERT INTO "MySchema"."table1" ("column1","column2") VALUES (?,?) ["value1",value2"] INSERT INTO "table2" ("columnA","columnB") VALUES (?,?) ["valueA","valueB"] Additional examples and documentation are available in the !JsonSQL distribution. See for example, * [[https://metacpan.org/pod/JsonSQL][JsonSQL]] -- The main documentation topic * [[https://metacpan.org/pod/JsonSQL::Query::Select][JsonSQL::Query::Select]] -- Documentation for SELECT queries * [[https://metacpan.org/pod/JsonSQL::Query::Insert][JsonSQL::Query::Insert]] -- Documentation for INSERT queries ---+++ Limitations The SQL generation by !JsonSQL is database agnostic. This will change in the future, but currently it does not attempt to limit SQL generation to the subset of features supported by a given database, so it is up to the user to verify support for their database. That said, the generated SQL is ANSI-compliant, so any database that supports that standard should be able to use the generated SQL (Oracle might be the biggest exception). !PostgreSQL support has been verified. Other databases have not been tested. ---++ Security With the purpose of this plugin being to perform arbitrary SQL operations generated from user input on potentially critical databases, security is of paramount concern. The !JsonSQL CPAN modules and JsonSQLPlugin address security using several reinforcing mechanisms. 1 The first is the JSON schema enforcement by !JsonSQL. Both SELECT and INSERT queries represented as JSON objects must conform to a structure that tries limit dangerous input while also maintaining flexibility. As such, table identifiers, for example, must meet the regex requirement /^[a-zA-Z_][a-zA-Z0-9_]*$/. In other words, no special characters are allowed, and the first character must be a letter or underscrore. To learn more about the schema requirements, see the documentation for the [[https://metacpan.org/pod/JsonSQL::Schemas::select][JsonSQL::Schema::Select]] and [[https://metacpan.org/pod/JsonSQL::Schemas::insert][JsonSQL::Schema::Insert]] modules. 1 The second is whitelisting of query generation to particular tables and columns that must be configured. !JsonSQL takes a set of whitelisting rules as input when it is converting JSON to SQL. See [[https://metacpan.org/pod/JsonSQL::Validator#Whitelisting-Module][JsonSQL::Validator]] for more details on how to construct this. JsonSQLPlugin takes a =configure= setting (see below) where these whitelisting rules are defined per user, per database, and per database operation. Because the rules are set and applied server-side, it is an effective way to block most attempts at malicious query generation by untrusted clients. 1 Value-based input such as WHERE conditions are parameterized by placeholders, which has become a standard and recommended practice in most database products. See [[http://bobby-tables.com/][Bobby Tables]] for a good description of the SQL injection attack. 1 Non-parameterizable input (such as table and column identifiers) are quoted. 1 Finally, the query itself is limited by both a =configure= setting that restricts operations to particular users, and database-level controls. JsonSQLPlugin currently only supports basic username and password based authentication, but support for Kerberos and other authentication mechanisms is planned for the future. While it is not possible to guarantee security, the combination of the above mitigation strategies should provide for reasonably safe dynamic SQL generation and execution. ---++ JsonSQLPlugin Configuration #PluginConfiguration The JsonSQLPlugin will not operate until it is configured with the appropriate settings to connect to your database. There are two configuration items that are defined as Perl data structures. This makes it difficult to configure for novice users, so building a better configuration interface will be a future development effort. However, in the meantime, Perl data structures are similar to JSON, so if you are familiar with constructing JSON objects it should not be too hard to understand how to configure this module. ---+++ dbconnections The $Foswiki::cfg{Extensions}{JsonSQLPlugin}{dbconnections} configuration item is a hash reference that takes the following form: { 'dbname' => { dsn => 'dsn', allowSelect => [ { allowedGroup => 'WikiGroup' || allowedUser => 'WikiUser', whitelist_rules => [ { schema => 'schema', 'allowedTable' => ['allowedColumns'], 'allowedTable' => ['allowedColumns'] } ] } ] allowInsert => <...> } } Each 'dbname' key of the hash reference is any valid string used to define the DB "namespace". The value of the key is a hash reference with the following properties: * dsn -- A Data Source Name (DSN) string for connecting to your database. It needs to be in the format expected by the Perl DBI module. * allowSelect -- An array of Foswiki users and groups and that are allowed SELECT access to this database, and the whitelist rules that should be applied. * allowInsert -- An array of Foswiki users and groups that are allowed INSERT access to this database, and the whitelist rules that should be applied. The whitelist rules must be in the format expected by [[https://metacpan.org/pod/JsonSQL::Validator#Whitelisting-Module][JsonSQL::Validator]]. A complete example might look something like this: { company_inventory => { dsn => 'dbi:Pg:dbname=inventory;host=inventory.company.com', allowSelect => [ { allowedGroup => 'Customers', whitelist_rules => [ { schema => 'product_schema', 'saleItems' => ['productId', 'productName', 'productPrice'] } ] }, { allowedGroup => 'Salesforce', whitelist_rules => [ { schema => 'product_schema', '#anyTable' => '' } ] } ], allowInsert => [ { allowedGroup => 'ProductDevelopment', whitelist_rules => [ { schema => 'product_schema', 'newItems' => ['#anyColumn'] } ] }, { allowedUser => 'DBAdmin', whitelist_rules => [ { schema => 'product_schema', '#anyTable' => '' } ] } ] } } The configuration is searched using the credentials of the currently logged in user. It is not possible to specify alternate user processing. If more than one set of connection settings for a given database apply to the current user (ex: user is a member of more than one allowedGroup), the following rules are used to determine which settings to use: * For users who are members of more than one group, group settings that come later in the allow* list override any settings that come earlier. * If a user is subject to both group settings (ie: is a member of a group) and has specific settings defined for that user, the user settings will override any group settings. ---+++ dbusermap The $Foswiki::cfg{Extensions}{JsonSQLPlugin}{dbusermap} configuration item is an array reference that takes the following form: [ { allowedGroup => 'WikiGroup' || allowedUser => 'WikiUser', 'dbname' => { default => { user => 'user', pass => 'pass' }, select => { user => 'user', pass => 'pass' }, insert => { user => 'user', pass => 'pass' } } } ] Each element of the array reference is a Fowiki user or group that is mapped to a set of DB user/pass credentials for each DB namespace they are allowed access to. The 'dbname' key is the same DB namespace used to define the dbconnection above. The value associated with each 'dbname' key is a hash reference of one or more of the following keys: * select -- the DB user credentials to use for SELECT operations * insert -- the DB user credentials to use for INSERT operations * default -- the DB user credentials to use if select or insert or both or not defined. A complete example to go with the above dbconnections example might look something like this: [ { allowedGroup => 'Customers', 'company_inventory' => { select => { user => 'wikicustomer', pass => 'wIKiCu$tomer%' } } }, { allowedGroup => 'Salesforce', 'company_inventory' => { select => { user => 'salesrep', pass => '$Ales-ReP)' } } }, { allowedGroup => 'ProductDevelopment', 'company_inventory' => { insert => { user => 'pd_agent', pass => 'secretpass' } } }, { allowedUser => 'DBAdmin', 'company_inventory' => { default => { user => 'dbadmin', pass => 'verysecretpass' } } } ] As with the dbconnections configuration, the dbusermap configuration is searched using the credentials of the currently logged in user. It is not possible to specify alternate user processing. If more than one set of user mappings for a given database+operation apply to the current user (ex: user is a member of more than one allowedGroup), the following rules are used to determine which mappings to use: * For users who are members of more than one group, group mappings that come later in the dbusermap list override any mappings that come earlier. * If a user is subject to both group mappings (ie: is a member of a group) and has specific mappings defined for that user, the user mappings will override any group mappings. ---+++ Why not DatabaseContrib? I looked at DatabaseContrib and had originally planned to use it for managing the database connection, but in the end I needed to manage the access rules in a different way for it to work with the !JsonSQL modules. I also wanted to simplify some of the settings to make it a bit more manageable. However, if there is a way to merge the two efforts I am open to reducing redundant functionality. ---++ JSON-RPC Handlers The core of JsonSQLPlugin is the management of three (currently) JSON-RPC handlers for passing JSON-formatted SQL queries to the server. | *Namespace* | *Method* | *Description* | | jsondb | select | A simple SQL SELECT. Returns the result as a JSON string | | jsondb | selectwithsearch | Combines an SQL SELECT with a search for topics that match certain criteria. The results of the topic search are merged with the SELECT result and returned as a JSON string | | jsondb | insert | Does an SQL INSERT. Returns, as a JSON string, either # of rows inserted, or the results of a SELECT after the INSERT if the RETURNING clause is used | ---+++ Params | *Method* | *Param* | *Description* | *Default* | | select | jsonQuery | The stringified JSON-formatted SQL query | None | |^| dbName | The name of the DB namespace to use as defined in the configuration settings. | None | | selectwithsearch | jsonQuery | The stringified JSON-formatted SQL query | None | |^| dbName | The name of the DB namespace to use as defined in the configuration settings. | None | |^| searchTopics | A stringified JSON object of topic search parameters with the following properties (see below) | None | | insert | jsonQuery | The stringified JSON-formatted SQL query | None | |^| dbName | The name of the DB namespace to use as defined in the configuration settings. | None | For "selectwithsearch", the searchTopics parameter takes the form: [ { ** General search params ** "web": , "query": , "form": , "topic": , ** Filtering results by DataForm values ** "formField": , "queryField": , ** Formatting return results ** "retKey": "retMany": }, { } ] The primary use of "selectwithsearch" is to identify Foswiki topics that contain data from the SQL query result. This way, you can effectively manage a hybrid between unstructured data storage in Foswiki topics and structured data in an SQL database. To link them, you only need a a DataForm field in the topic to link SQL query results to that topic (or vice versa). Think of it as a rudimentary JOIN for Foswiki topics. ---+++ Usage See JsonRpcContrib for details, but I've found the easiest method to be calling the $.jsonRpc() method. For example, var jsonQuery = { "fields": [ {"column": "*"} ], "from": [ {"schema": "product_schema", "table": "saleItems"} ] } var jsonParams = { jsonQuery: JSON.stringify(jsonQuery), dbName: 'company_inventory' }; $.jsonRpc( foswiki.getScriptUrl("jsonrpc"), { namespace: "jsondb", method: "select", params: jsonParams, error: function(jsonResponse, status, xhr) { alert("Error: "+jsonResponse.error); }, success: function(jsonResponse, status, xhr) { alert("Success: "+jsonResponse.result); } } ); ---++ Putting it all together. A complete example. It's not possible to bundle a working example because an appropriate SQL DB needs to be available. But using the snippets provided in this doc, we can piece together a complete prototype that should be easily modifiable to fit your needs. First, configure JsonSQLPlugin with the appropriate connection settings and user mappings for your database and Foswiki users/groups. Using the snippets provided above in [[#PluginConfiguration][JsonSQLPlugin Configuration]], we need a !PostgreSQL database with the following structure: inventory.company.com (host) inventory (database) product_schema (schema) saleItems (table) productId (column) productName (column) productPrice (column) newItems (table) You are free to try this with a different database (ex: !MySQL). It should work, but not all of the features provided by !JsonSQL (ex: DB schemas) are supported by !MySQL. So adjust accordingly. For this example, we will use a Foswiki user in the Foswiki group "Salesforce" to access the database, so the appropriate DB credentials have to be created. Populate the DB with some data. Now the code. In your SCRIPT HEAD, And in your BODY,
Product IdProduct NameProduct Price
And that's it. ---++ Installation You do not need to install anything in the browser to use this extension. The following instructions are for the administrator who installs the extension on the server. Open configure, and open the "Extensions" section. Use "Find More Extensions" to get a list of available extensions. Select "Install". If you have any problems, or if the extension isn't available in =configure=, then you can still install manually from the command-line. See http://foswiki.org/Support/ManuallyInstallingExtensions for more help. ---++ Dependencies
NameVersionDescription
Foswiki::Func>=0May be required for lib/Foswiki/Plugins/JsonSQLPlugin.pm
Foswiki::Plugins>=0May be required for lib/Foswiki/Plugins/JsonSQLPlugin.pm
JsonSQL>=0May be required for lib/Foswiki/Plugins/JsonSQLPlugin.pm
DBI>=0May be required for lib/Foswiki/Plugins/JsonSQLPlugin.pm
JSON>=0May be required for lib/Foswiki/Plugins/JsonSQLPlugin.pm
---++ Change History %TABLE{columnwidths="7em" tablewidth="100%"}% | 0.4 | First public release. | %META:FORM{name="PackageForm"}% %META:FIELD{name="Author" title="Author" value="ChrisHoefler"}% %META:FIELD{name="Version" title="Version" value="0.4"}% %META:FIELD{name="Release" title="Release" value="01 Aug 2017"}% %META:FIELD{name="Description" title="Description" value="Provides JSON handlers for interacting with SQL databases"}% %META:FIELD{name="Copyright" value="2017, Chris Hoefler, All Rights Reserved"}% %META:FIELD{name="License" value="GPL ([[http://www.gnu.org/copyleft/gpl.html][GNU General Public License]])"}% %META:FIELD{name="Repository" value="https://github.com/foswiki/JsonSQLPlugin"}% %META:FIELD{name="Home" value="https://foswiki.org/Extensions/JsonSQLPlugin"}% %META:FIELD{name="Support" value="https://foswiki.org/Support/JsonSQLPlugin"}% %META:FIELD{name="Repository" title="Repository" value="https://github.com/foswiki/distro"}%