提交 b940440b authored 作者: andrei's avatar andrei

Merge remote-tracking branch 'h2database/master' into txcommit-atomic

...@@ -45,11 +45,7 @@ SELECT * FROM (SELECT ID, COUNT(*) FROM TEST ...@@ -45,11 +45,7 @@ SELECT * FROM (SELECT ID, COUNT(*) FROM TEST
" "
"Commands (DML)","INSERT"," "Commands (DML)","INSERT","
INSERT INTO tableName INSERT INTO tableName insertColumnsAndSource
{ [ ( columnName [,...] ) ]
{ VALUES { ( { DEFAULT | expression } [,...] ) } [,...]
| [ DIRECT ] [ SORTED ] select } } |
{ SET { columnName = { DEFAULT | expression } } [,...] }
"," ","
Inserts a new row / new rows into a table. Inserts a new row / new rows into a table.
...@@ -61,9 +57,7 @@ INSERT INTO TEST VALUES(1, 'Hello') ...@@ -61,9 +57,7 @@ INSERT INTO TEST VALUES(1, 'Hello')
" "
"Commands (DML)","UPDATE"," "Commands (DML)","UPDATE","
UPDATE tableName [ [ AS ] newTableAlias ] SET UPDATE tableName [ [ AS ] newTableAlias ] SET setClauseList
{ { columnName = { DEFAULT | expression } } [,...] } |
{ ( columnName [,...] ) = ( select ) }
[ WHERE expression ] [ ORDER BY order [,...] ] [ LIMIT expression ] [ WHERE expression ] [ ORDER BY order [,...] ] [ LIMIT expression ]
"," ","
Updates data in a table. Updates data in a table.
...@@ -74,7 +68,7 @@ UPDATE PERSON P SET NAME=(SELECT A.NAME FROM ADDRESS A WHERE A.ID=P.ID); ...@@ -74,7 +68,7 @@ UPDATE PERSON P SET NAME=(SELECT A.NAME FROM ADDRESS A WHERE A.ID=P.ID);
" "
"Commands (DML)","DELETE"," "Commands (DML)","DELETE","
DELETE [ TOP term ] FROM tableName [ WHERE expression ] [ LIMIT term ] DELETE [ TOP term ] FROM tableName deleteSearchCondition
"," ","
Deletes rows form a table. Deletes rows form a table.
If TOP or LIMIT is specified, at most the specified number of rows are deleted (no limit if null or smaller than zero). If TOP or LIMIT is specified, at most the specified number of rows are deleted (no limit if null or smaller than zero).
...@@ -129,16 +123,19 @@ MERGE INTO TEST KEY(ID) VALUES(2, 'World') ...@@ -129,16 +123,19 @@ MERGE INTO TEST KEY(ID) VALUES(2, 'World')
MERGE INTO targetTableName [ [AS] targetAlias] MERGE INTO targetTableName [ [AS] targetAlias]
USING { ( select ) | sourceTableName }[ [AS] sourceAlias ] USING { ( select ) | sourceTableName }[ [AS] sourceAlias ]
ON ( expression ) ON ( expression )
[ WHEN MATCHED THEN [ update ] [ delete] ] [ WHEN MATCHED THEN
[ WHEN NOT MATCHED THEN insert ] [ UPDATE SET setClauseList ] [ DELETE deleteSearchCondition ] ]
[ WHEN NOT MATCHED THEN INSERT insertColumnsAndSource ]
"," ","
Updates or deletes existing rows, and insert rows that don't exist. The ON clause Updates or deletes existing rows, and insert rows that don't exist. The ON clause
specifies the matching column expression and must be specified. If more than one row specifies the matching column expression and must be specified. If more than one row
is updated per input row, an exception is thrown. is updated per input row, an exception is thrown.
If the source data contains duplicate rows (specifically those columns used in the If the source data contains duplicate rows (specifically those columns used in the
row matching ON clause), then an exception is thrown to prevent two updates applying row matching ON clause), then an exception is thrown to prevent two updates applying
to the same target row. The embedded update, delete or insert statements can not re-specify to the same target row.
the target table name. WHEN MATCHED THEN or WHEN NOT MATCHED THEN clauses or both of them in any order should be specified.
If WHEN MATCHED THEN is specified it should contain UPDATE or DELETE clauses of both of them.
If statement doesn't need a source table a DUAL table can be substituted.
"," ","
MERGE INTO TARGET_TABLE AS T USING SOURCE_TABLE AS S MERGE INTO TARGET_TABLE AS T USING SOURCE_TABLE AS S
ON (T.ID = S.ID) ON (T.ID = S.ID)
...@@ -154,6 +151,9 @@ MERGE INTO TARGET_TABLE AS T USING (SELECT * FROM SOURCE_TABLE) AS S ...@@ -154,6 +151,9 @@ MERGE INTO TARGET_TABLE AS T USING (SELECT * FROM SOURCE_TABLE) AS S
DELETE WHERE T.COL2='FINAL' DELETE WHERE T.COL2='FINAL'
WHEN NOT MATCHED THEN WHEN NOT MATCHED THEN
INSERT (ID,COL1,COL2) VALUES(S.ID,S.COL1,S.COL2) INSERT (ID,COL1,COL2) VALUES(S.ID,S.COL1,S.COL2)
MERGE INTO TARGET_TABLE USING DUAL ON (ID = 1)
WHEN NOT MATCHED THEN INSERT (ID, NAME) VALUES (1, 'Test')
WHEN MATCHED THEN UPDATE SET NAME = 'Test'
" "
"Commands (DML)","RUNSCRIPT"," "Commands (DML)","RUNSCRIPT","
...@@ -2252,6 +2252,14 @@ SELECT CAST(0 AS DOUBLE) ...@@ -2252,6 +2252,14 @@ SELECT CAST(0 AS DOUBLE)
SELECT -1.4e-10 SELECT -1.4e-10
" "
"Other Grammar","Delete search condition","
[ WHERE expression ] [ LIMIT term ]
","
Search condition for DELETE statement.
","
WHERE ID = 2
"
"Other Grammar","Digit"," "Other Grammar","Digit","
0-9 0-9
"," ","
...@@ -2313,6 +2321,17 @@ the column in the same way. ...@@ -2313,6 +2321,17 @@ the column in the same way.
NAME NAME
" "
"Other Grammar","Insert columns and source","
{ [ ( columnName [,...] ) ]
{ VALUES { ( { DEFAULT | expression } [,...] ) } [,...]
| [ DIRECT ] [ SORTED ] select } } |
{ SET { columnName = { DEFAULT | expression } } [,...] }
","
Names of columns and their values for INSERT statement.
","
(ID, NAME) VALUES (1, 'Test')
"
"Other Grammar","Int"," "Other Grammar","Int","
[ + | - ] number [ + | - ] number
"," ","
...@@ -2438,6 +2457,15 @@ An expression in a SELECT statement. ...@@ -2438,6 +2457,15 @@ An expression in a SELECT statement.
ID AS VALUE ID AS VALUE
" "
"Other Grammar","Set clause list","
{ { columnName = { DEFAULT | expression } } [,...] } |
{ ( columnName [,...] ) = ( select ) }
","
List of SET clauses.
","
NAME = 'Test', VALUE = 2
"
"Other Grammar","String"," "Other Grammar","String","
'anythingExceptSingleQuote' 'anythingExceptSingleQuote'
"," ","
......
...@@ -83,6 +83,8 @@ Features ...@@ -83,6 +83,8 @@ Features
Compacting a Database</a><br /> Compacting a Database</a><br />
<a href="#cache_settings"> <a href="#cache_settings">
Cache Settings</a><br /> Cache Settings</a><br />
<a href="#external_authentication">
External Authentication (Experimental)</a><br />
<h2 id="feature_list">Feature List</h2> <h2 id="feature_list">Feature List</h2>
<h3>Main Features</h3> <h3>Main Features</h3>
...@@ -1826,6 +1828,79 @@ To get information about page reads and writes, and the current caching algorith ...@@ -1826,6 +1828,79 @@ To get information about page reads and writes, and the current caching algorith
call <code>SELECT * FROM INFORMATION_SCHEMA.SETTINGS</code>. The number of pages read / written call <code>SELECT * FROM INFORMATION_SCHEMA.SETTINGS</code>. The number of pages read / written
is listed. is listed.
</p> </p>
<h2 id="external_authentication">External authentication (Experimental)</h2>
<p>
External authentication allows to optionally validate user credentials externally (JAAS,LDAP,custom classes).
Is also possible to temporary assign roles to to externally authenticated users. <b>This feature is experimental and subject to change</b>
</p>
<p>Master user cannot be externally authenticated</p>
<p>
To enable external authentication on a database execute statement <code>SET AUTHENTICATOR TRUE</code>. This setting in persisted on the database.
</p>
<p>
To connect on a database by using external credentials client must append <code>AUTHREALM=H2</code> to the database URL. <code>H2</code>
is the identifier of the authentication realm (see later).
</p>
<p>External authentication requires to send password to the server. For this reason is works only on local connection or remote over ssl</p>
<p>
By default external authentication is performed through JAAS login interface (configuration name is <code>h2</code>).
To configure JAAS add argument <code>-Djava.security.auth.login.config=jaas.conf</code>
Here an example of <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html">
JAAS login configuration file</a> content:
</p>
<pre>
h2 {
com.sun.security.auth.module.LdapLoginModule REQUIRED \
userProvider="ldap://127.0.0.1:10389" authIdentity="uid={USERNAME},ou=people,dc=example,dc=com" \
debug=true useSSL=false ;
};
</pre>
<p>
Is it possible to specify custom authentication settings by using
JVM argument <code>-Dh2auth.configurationFile={urlOfH2Auth.xml}</code>. Here an example of h2auth.xml file content:
</p>
<pre>
&lt;h2Auth allowUserRegistration="false" createMissingRoles="true"&gt;
&lt;!-- realm: DUMMY authenticate users named DUMMY[0-9] with a static password --&gt;
&lt;realm name="DUMMY"
validatorClass="org.h2.security.auth.impl.FixedPasswordCredentialsValidator"&gt;
&lt;property name="userNamePattern" value="DUMMY[0-9]" /&gt;
&lt;property name="password" value="mock" /&gt;
&lt;/realm&gt;
&lt;!-- realm LDAPEXAMPLE:perform credentials validation on LDAP --&gt;
&lt;realm name="LDAPEXAMPLE"
validatorClass="org.h2.security.auth.impl.LdapCredentialsValidator"&gt;
&lt;property name="bindDnPattern" value="uid=%u,ou=people,dc=example,dc=com" /&gt;
&lt;property name="host" value="127.0.0.1" /&gt;
&lt;property name="port" value="10389" /&gt;
&lt;property name="secure" value="false" /&gt;
&lt;/realm&gt;
&lt;!-- realm JAAS: perform credentials validation by using JAAS api --&gt;
&lt;realm name="JAAS"
validatorClass="org.h2.security.auth.impl.JaasCredentialsValidator"&gt;
&lt;property name="appName" value="H2" /&gt;
&lt;/realm&gt;
&lt;!--Assign to each user role @{REALM} --&gt;
&lt;userToRolesMapper class="org.h2.security.auth.impl.AssignRealmNameRole"/&gt;
&lt;!--Assign to each user role REMOTEUSER --&gt;
&lt;userToRolesMapper class="org.h2.security.auth.impl.StaticRolesMapper"&gt;
&lt;property name="roles" value="REMOTEUSER"/&gt;
&lt;/userToRolesMapper&gt;
&lt;/h2Auth&gt;
</pre>
<p>
Custom credentials validators must implement the interface
<code>org.h2.api.CredentialsValidator</code>
</p>
<p>
Custom criteria for role assignments must implement the interface
<code>org.h2.api.UserToRoleMapper</code>
</p>
<!-- [close] { --></div></td></tr></table><!-- } --><!-- analytics --></body></html> <!-- [close] { --></div></td></tr></table><!-- } --><!-- analytics --></body></html>
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.api;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.security.auth.Configurable;
/**
* A class that implement this interface can be used to validate
* credentials provided by client.
* <p>
* <b>This feature is experimental and subject to change</b>
* </p>
*/
public interface CredentialsValidator extends Configurable {
/**
* Validate user credential
* @param authenticationInfo = authentication info
* @return true if credentials are valid, otherwise false
* @throws Exception any exception occurred (invalid credentials or internal issue) prevent user login
*/
boolean validateCredentials(AuthenticationInfo authenticationInfo) throws Exception;
}
...@@ -1990,7 +1990,20 @@ public class ErrorCode { ...@@ -1990,7 +1990,20 @@ public class ErrorCode {
*/ */
public static final int ROW_NOT_FOUND_IN_PRIMARY_INDEX = 90143; public static final int ROW_NOT_FOUND_IN_PRIMARY_INDEX = 90143;
// next are 90122, 90144 /**
* The error with code <code>90144</code> is thrown when
* user trying to login into a database with AUTHREALM set and
* the target database doesn't have an authenticator defined
* <p>Authenticator experimental feature can be enabled by
* </p>
* <pre>
* SET AUTHENTICATOR TRUE
* </pre>
*/
public static final int AUTHENTICATOR_NOT_AVAILABLE = 90144;
// next are 90122, 90145
private ErrorCode() { private ErrorCode() {
// utility class // utility class
......
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.api;
import java.util.Collection;
import java.util.Set;
import org.h2.security.auth.AuthenticationException;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.security.auth.Configurable;
/**
* A class that implement this interface can be used during
* authentication to map external users to database roles.
* <p>
* <b>This feature is experimental and subject to change</b>
* </p>
*/
public interface UserToRolesMapper extends Configurable {
/**
* Map user identified by authentication info to a set of granted roles
* @param authenticationInfo
* @return list of roles to be assigned to the user temporary
* @throws AuthenticationException
*/
Collection<String> mapUserToRoles(AuthenticationInfo authenticationInfo) throws AuthenticationException;
}
...@@ -48,8 +48,8 @@ public abstract class Command implements CommandInterface { ...@@ -48,8 +48,8 @@ public abstract class Command implements CommandInterface {
private boolean canReuse; private boolean canReuse;
Command(Parser parser, String sql) { Command(Session session, String sql) {
this.session = parser.getSession(); this.session = session;
this.sql = sql; this.sql = sql;
trace = session.getDatabase().getTrace(Trace.COMMAND); trace = session.getDatabase().getTrace(Trace.COMMAND);
} }
......
...@@ -9,6 +9,7 @@ import java.util.ArrayList; ...@@ -9,6 +9,7 @@ import java.util.ArrayList;
import org.h2.api.DatabaseEventListener; import org.h2.api.DatabaseEventListener;
import org.h2.command.dml.Explain; import org.h2.command.dml.Explain;
import org.h2.command.dml.Query; import org.h2.command.dml.Query;
import org.h2.engine.Session;
import org.h2.expression.Parameter; import org.h2.expression.Parameter;
import org.h2.expression.ParameterInterface; import org.h2.expression.ParameterInterface;
import org.h2.result.ResultInterface; import org.h2.result.ResultInterface;
...@@ -26,8 +27,8 @@ public class CommandContainer extends Command { ...@@ -26,8 +27,8 @@ public class CommandContainer extends Command {
private boolean readOnlyKnown; private boolean readOnlyKnown;
private boolean readOnly; private boolean readOnly;
CommandContainer(Parser parser, String sql, Prepared prepared) { CommandContainer(Session session, String sql, Prepared prepared) {
super(parser, sql); super(session, sql);
prepared.setCommand(this); prepared.setCommand(this);
this.prepared = prepared; this.prepared = prepared;
} }
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
package org.h2.command; package org.h2.command;
import java.util.ArrayList; import java.util.ArrayList;
import org.h2.engine.Session;
import org.h2.expression.ParameterInterface; import org.h2.expression.ParameterInterface;
import org.h2.result.ResultInterface; import org.h2.result.ResultInterface;
...@@ -17,8 +19,8 @@ class CommandList extends Command { ...@@ -17,8 +19,8 @@ class CommandList extends Command {
private final Command command; private final Command command;
private final String remaining; private final String remaining;
CommandList(Parser parser, String sql, Command c, String remaining) { CommandList(Session session, String sql, Command c, String remaining) {
super(parser, sql); super(session, sql);
this.command = c; this.command = c;
this.remaining = remaining; this.remaining = remaining;
} }
......
...@@ -8,6 +8,13 @@ ...@@ -8,6 +8,13 @@
*/ */
package org.h2.command; package org.h2.command;
import static org.h2.util.ParserUtil.FALSE;
import static org.h2.util.ParserUtil.IDENTIFIER;
import static org.h2.util.ParserUtil.KEYWORD;
import static org.h2.util.ParserUtil.NULL;
import static org.h2.util.ParserUtil.ROWNUM;
import static org.h2.util.ParserUtil.TRUE;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.charset.Charset; import java.nio.charset.Charset;
...@@ -189,13 +196,7 @@ public class Parser { ...@@ -189,13 +196,7 @@ public class Parser {
private static final int CHAR_STRING = 7, CHAR_DOT = 8, private static final int CHAR_STRING = 7, CHAR_DOT = 8,
CHAR_DOLLAR_QUOTED_STRING = 9; CHAR_DOLLAR_QUOTED_STRING = 9;
// this are token types // this are token types, see also types in ParserUtil
private static final int KEYWORD = ParserUtil.KEYWORD;
private static final int IDENTIFIER = ParserUtil.IDENTIFIER;
private static final int NULL = ParserUtil.NULL;
private static final int TRUE = ParserUtil.TRUE;
private static final int FALSE = ParserUtil.FALSE;
private static final int ROWNUM = ParserUtil.ROWNUM;
private static final int PARAMETER = 10, END = 11, VALUE = 12; private static final int PARAMETER = 10, END = 11, VALUE = 12;
private static final int EQUAL = 13, BIGGER_EQUAL = 14, BIGGER = 15; private static final int EQUAL = 13, BIGGER_EQUAL = 14, BIGGER = 15;
private static final int SMALLER = 16, SMALLER_EQUAL = 17, NOT_EQUAL = 18; private static final int SMALLER = 16, SMALLER_EQUAL = 17, NOT_EQUAL = 18;
...@@ -285,11 +286,11 @@ public class Parser { ...@@ -285,11 +286,11 @@ public class Parser {
throw getSyntaxError(); throw getSyntaxError();
} }
p.prepare(); p.prepare();
Command c = new CommandContainer(this, sql, p); Command c = new CommandContainer(session, sql, p);
if (hasMore) { if (hasMore) {
String remaining = originalSQL.substring(parseIndex); String remaining = originalSQL.substring(parseIndex);
if (!StringUtils.isWhitespaceOrEmpty(remaining)) { if (!StringUtils.isWhitespaceOrEmpty(remaining)) {
c = new CommandList(this, sql, c, remaining); c = new CommandList(session, sql, c, remaining);
} }
} }
return c; return c;
...@@ -574,7 +575,7 @@ public class Parser { ...@@ -574,7 +575,7 @@ public class Parser {
command.setTable(table); command.setTable(table);
} }
if (readIf("SAMPLE_SIZE")) { if (readIf("SAMPLE_SIZE")) {
command.setTop(readPositiveInt()); command.setTop(readNonNegativeInt());
} }
return command; return command;
} }
...@@ -734,10 +735,9 @@ public class Parser { ...@@ -734,10 +735,9 @@ public class Parser {
} }
private Column readTableColumn(TableFilter filter) { private Column readTableColumn(TableFilter filter) {
String tableAlias = null;
String columnName = readColumnIdentifier(); String columnName = readColumnIdentifier();
if (readIf(".")) { if (readIf(".")) {
tableAlias = columnName; String tableAlias = columnName;
columnName = readColumnIdentifier(); columnName = readColumnIdentifier();
if (readIf(".")) { if (readIf(".")) {
String schema = tableAlias; String schema = tableAlias;
...@@ -1156,11 +1156,15 @@ public class Parser { ...@@ -1156,11 +1156,15 @@ public class Parser {
TableFilter sourceTableFilter = readSimpleTableFilter(0, excludeIdentifiers); TableFilter sourceTableFilter = readSimpleTableFilter(0, excludeIdentifiers);
command.setSourceTableFilter(sourceTableFilter); command.setSourceTableFilter(sourceTableFilter);
StringBuilder buff = new StringBuilder("SELECT * FROM "); Select preparedQuery = new Select(session);
appendTableWithSchemaAndAlias(buff, sourceTableFilter.getTable(), sourceTableFilter.getTableAlias()); ArrayList<Expression> expr = new ArrayList<>(1);
Prepared preparedQuery = prepare(session, buff.toString(), null/*paramValues*/); expr.add(new Wildcard(null, null));
command.setQuery((Select) preparedQuery); preparedQuery.setExpressions(expr);
TableFilter filter = new TableFilter(session, sourceTableFilter.getTable(),
sourceTableFilter.getTableAlias(), rightsChecked, preparedQuery, 0, null);
preparedQuery.addTableFilter(filter, true);
preparedQuery.init();
command.setQuery(preparedQuery);
} }
read("ON"); read("ON");
read("("); read("(");
...@@ -1168,9 +1172,20 @@ public class Parser { ...@@ -1168,9 +1172,20 @@ public class Parser {
command.setOnCondition(condition); command.setOnCondition(condition);
read(")"); read(")");
boolean matched = parseWhenMatched(command); read("WHEN");
if (parseWhenNotMatched(command) && !matched) { boolean matched = readIf("MATCHED");
if (matched) {
parseWhenMatched(command); parseWhenMatched(command);
} else {
parseWhenNotMatched(command);
}
if (readIf("WHEN")) {
if (matched) {
parseWhenNotMatched(command);
} else {
read("MATCHED");
parseWhenMatched(command);
}
} }
setSQL(command, "MERGE", start); setSQL(command, "MERGE", start);
...@@ -1188,17 +1203,17 @@ public class Parser { ...@@ -1188,17 +1203,17 @@ public class Parser {
return command; return command;
} }
private boolean parseWhenMatched(MergeUsing command) { private void parseWhenMatched(MergeUsing command) {
if (!readIfAll("WHEN", "MATCHED", "THEN")) { read("THEN");
return false;
}
int startMatched = lastParseIndex; int startMatched = lastParseIndex;
boolean ok = false;
if (readIf("UPDATE")) { if (readIf("UPDATE")) {
Update updateCommand = new Update(session); Update updateCommand = new Update(session);
TableFilter filter = command.getTargetTableFilter(); TableFilter filter = command.getTargetTableFilter();
updateCommand.setTableFilter(filter); updateCommand.setTableFilter(filter);
parseUpdateSetClause(updateCommand, filter, startMatched); parseUpdateSetClause(updateCommand, filter, startMatched);
command.setUpdateCommand(updateCommand); command.setUpdateCommand(updateCommand);
ok = true;
} }
startMatched = lastParseIndex; startMatched = lastParseIndex;
if (readIf("DELETE")) { if (readIf("DELETE")) {
...@@ -1207,21 +1222,25 @@ public class Parser { ...@@ -1207,21 +1222,25 @@ public class Parser {
deleteCommand.setTableFilter(filter); deleteCommand.setTableFilter(filter);
parseDeleteGivenTable(deleteCommand, null, startMatched); parseDeleteGivenTable(deleteCommand, null, startMatched);
command.setDeleteCommand(deleteCommand); command.setDeleteCommand(deleteCommand);
ok = true;
} }
return true; if (!ok) {
throw getSyntaxError();
} }
private boolean parseWhenNotMatched(MergeUsing command) {
if (!readIfAll("WHEN", "NOT", "MATCHED", "THEN")) {
return false;
} }
private void parseWhenNotMatched(MergeUsing command) {
read("NOT");
read("MATCHED");
read("THEN");
if (readIf("INSERT")) { if (readIf("INSERT")) {
Insert insertCommand = new Insert(session); Insert insertCommand = new Insert(session);
insertCommand.setTable(command.getTargetTable()); insertCommand.setTable(command.getTargetTable());
parseInsertGivenTable(insertCommand, command.getTargetTable()); parseInsertGivenTable(insertCommand, command.getTargetTable());
command.setInsertCommand(insertCommand); command.setInsertCommand(insertCommand);
} else {
throw getSyntaxError();
} }
return true;
} }
private static void appendTableWithSchemaAndAlias(StringBuilder buff, Table table, String alias) { private static void appendTableWithSchemaAndAlias(StringBuilder buff, Table table, String alias) {
...@@ -1964,9 +1983,8 @@ public class Parser { ...@@ -1964,9 +1983,8 @@ public class Parser {
} }
private Query parseSelect() { private Query parseSelect() {
Query command = null;
int paramIndex = parameters.size(); int paramIndex = parameters.size();
command = parseSelectUnion(); Query command = parseSelectUnion();
int size = parameters.size(); int size = parameters.size();
ArrayList<Parameter> params = new ArrayList<>(size); ArrayList<Parameter> params = new ArrayList<>(size);
for (int i = paramIndex; i < size; i++) { for (int i = paramIndex; i < size; i++) {
...@@ -2042,10 +2060,7 @@ public class Parser { ...@@ -2042,10 +2060,7 @@ public class Parser {
} }
ArrayList<SelectOrderBy> orderList = Utils.newSmallArrayList(); ArrayList<SelectOrderBy> orderList = Utils.newSmallArrayList();
do { do {
boolean canBeNumber = true; boolean canBeNumber = !readIf("=");
if (readIf("=")) {
canBeNumber = false;
}
SelectOrderBy order = new SelectOrderBy(); SelectOrderBy order = new SelectOrderBy();
Expression expr = readExpression(); Expression expr = readExpression();
if (canBeNumber && expr instanceof ValueExpression && if (canBeNumber && expr instanceof ValueExpression &&
...@@ -2159,7 +2174,7 @@ public class Parser { ...@@ -2159,7 +2174,7 @@ public class Parser {
return command; return command;
} }
if (readIf("WITH")) { if (readIf("WITH")) {
Query query = null; Query query;
try { try {
query = (Query) parseWith(); query = (Query) parseWith();
} catch (ClassCastException e) { } catch (ClassCastException e) {
...@@ -2691,7 +2706,7 @@ public class Parser { ...@@ -2691,7 +2706,7 @@ public class Parser {
} }
private JavaFunction readJavaFunction(Schema schema, String functionName, boolean throwIfNotFound) { private JavaFunction readJavaFunction(Schema schema, String functionName, boolean throwIfNotFound) {
FunctionAlias functionAlias = null; FunctionAlias functionAlias;
if (schema != null) { if (schema != null) {
functionAlias = schema.findFunction(functionName); functionAlias = schema.findFunction(functionName);
} else { } else {
...@@ -3403,10 +3418,10 @@ public class Parser { ...@@ -3403,10 +3418,10 @@ public class Parser {
return function; return function;
} }
private int readPositiveInt() { private int readNonNegativeInt() {
int v = readInt(); int v = readInt();
if (v < 0) { if (v < 0) {
throw DbException.getInvalidValueException("positive integer", v); throw DbException.getInvalidValueException("non-negative integer", v);
} }
return v; return v;
} }
...@@ -3452,14 +3467,21 @@ public class Parser { ...@@ -3452,14 +3467,21 @@ public class Parser {
} }
private boolean readBooleanSetting() { private boolean readBooleanSetting() {
if (currentTokenType == VALUE) { switch (currentTokenType) {
case TRUE:
read();
return true;
case FALSE:
read();
return false;
case VALUE:
boolean result = currentValue.getBoolean(); boolean result = currentValue.getBoolean();
read(); read();
return result; return result;
} }
if (readIf("TRUE") || readIf("ON")) { if (readIf("ON")) {
return true; return true;
} else if (readIf("FALSE") || readIf("OFF")) { } else if (readIf("OFF")) {
return false; return false;
} else { } else {
throw getSyntaxError(); throw getSyntaxError();
...@@ -3547,26 +3569,6 @@ public class Parser { ...@@ -3547,26 +3569,6 @@ public class Parser {
return false; return false;
} }
/*
* Reads every token in list, in order - returns true if all are found.
* If any are not found, returns false - AND resets parsing back to state when called.
*/
private boolean readIfAll(String... tokens) {
// save parse location in case we have to fail this test
int start = lastParseIndex;
for (String token: tokens) {
if (!currentTokenQuoted && equalsToken(token, currentToken)) {
read();
} else {
// read failed - revert parse location to before when called
parseIndex = start;
read();
return false;
}
}
return true;
}
private boolean isToken(String token) { private boolean isToken(String token) {
boolean result = equalsToken(token, currentToken) && boolean result = equalsToken(token, currentToken) &&
!currentTokenQuoted; !currentTokenQuoted;
...@@ -3753,12 +3755,11 @@ public class Parser { ...@@ -3753,12 +3755,11 @@ public class Parser {
return; return;
} }
case CHAR_DOLLAR_QUOTED_STRING: { case CHAR_DOLLAR_QUOTED_STRING: {
String result = null;
int begin = i - 1; int begin = i - 1;
while (types[i] == CHAR_DOLLAR_QUOTED_STRING) { while (types[i] == CHAR_DOLLAR_QUOTED_STRING) {
i++; i++;
} }
result = sqlCommand.substring(begin, i); String result = sqlCommand.substring(begin, i);
currentToken = "'"; currentToken = "'";
checkLiterals(true); checkLiterals(true);
currentValue = ValueString.get(StringUtils.cache(result), currentValue = ValueString.get(StringUtils.cache(result),
...@@ -3877,10 +3878,6 @@ public class Parser { ...@@ -3877,10 +3878,6 @@ public class Parser {
currentTokenType = VALUE; currentTokenType = VALUE;
} }
public Session getSession() {
return session;
}
private void initialize(String sql) { private void initialize(String sql) {
if (sql == null) { if (sql == null) {
sql = ""; sql = "";
...@@ -4306,7 +4303,7 @@ public class Parser { ...@@ -4306,7 +4303,7 @@ public class Parser {
column.setSequence(sequence); column.setSequence(sequence);
} }
if (readIf("SELECTIVITY")) { if (readIf("SELECTIVITY")) {
int value = readPositiveInt(); int value = readNonNegativeInt();
column.setSelectivity(value); column.setSelectivity(value);
} }
String comment = readCommentIf(); String comment = readCommentIf();
...@@ -4354,7 +4351,7 @@ public class Parser { ...@@ -4354,7 +4351,7 @@ public class Parser {
} }
} else if (readIf("TIME")) { } else if (readIf("TIME")) {
if (readIf("(")) { if (readIf("(")) {
originalScale = readPositiveInt(); originalScale = readNonNegativeInt();
if (originalScale > ValueTime.MAXIMUM_SCALE) { if (originalScale > ValueTime.MAXIMUM_SCALE) {
throw DbException.get(ErrorCode.INVALID_VALUE_SCALE_PRECISION, Integer.toString(originalScale)); throw DbException.get(ErrorCode.INVALID_VALUE_SCALE_PRECISION, Integer.toString(originalScale));
} }
...@@ -4367,10 +4364,10 @@ public class Parser { ...@@ -4367,10 +4364,10 @@ public class Parser {
} }
} else if (readIf("TIMESTAMP")) { } else if (readIf("TIMESTAMP")) {
if (readIf("(")) { if (readIf("(")) {
originalScale = readPositiveInt(); originalScale = readNonNegativeInt();
// Allow non-standard TIMESTAMP(..., ...) syntax // Allow non-standard TIMESTAMP(..., ...) syntax
if (readIf(",")) { if (readIf(",")) {
originalScale = readPositiveInt(); originalScale = readNonNegativeInt();
} }
if (originalScale > ValueTimestamp.MAXIMUM_SCALE) { if (originalScale > ValueTimestamp.MAXIMUM_SCALE) {
throw DbException.get(ErrorCode.INVALID_VALUE_SCALE_PRECISION, Integer.toString(originalScale)); throw DbException.get(ErrorCode.INVALID_VALUE_SCALE_PRECISION, Integer.toString(originalScale));
...@@ -4458,7 +4455,7 @@ public class Parser { ...@@ -4458,7 +4455,7 @@ public class Parser {
} }
} else if (original.equals("DATETIME") || original.equals("DATETIME2")) { } else if (original.equals("DATETIME") || original.equals("DATETIME2")) {
if (readIf("(")) { if (readIf("(")) {
originalScale = readPositiveInt(); originalScale = readNonNegativeInt();
if (originalScale > ValueTime.MAXIMUM_SCALE) { if (originalScale > ValueTime.MAXIMUM_SCALE) {
throw DbException.get(ErrorCode.INVALID_VALUE_SCALE_PRECISION, throw DbException.get(ErrorCode.INVALID_VALUE_SCALE_PRECISION,
Integer.toString(originalScale)); Integer.toString(originalScale));
...@@ -4506,7 +4503,7 @@ public class Parser { ...@@ -4506,7 +4503,7 @@ public class Parser {
} }
} else if (dataType.type == Value.DOUBLE && original.equals("FLOAT")) { } else if (dataType.type == Value.DOUBLE && original.equals("FLOAT")) {
if (readIf("(")) { if (readIf("(")) {
int p = readPositiveInt(); int p = readNonNegativeInt();
read(")"); read(")");
if (p > 53) { if (p > 53) {
throw DbException.get(ErrorCode.INVALID_VALUE_SCALE_PRECISION, Integer.toString(p)); throw DbException.get(ErrorCode.INVALID_VALUE_SCALE_PRECISION, Integer.toString(p));
...@@ -4540,7 +4537,7 @@ public class Parser { ...@@ -4540,7 +4537,7 @@ public class Parser {
} else if (readIf("(")) { } else if (readIf("(")) {
// Support for MySQL: INT(11), MEDIUMINT(8) and so on. // Support for MySQL: INT(11), MEDIUMINT(8) and so on.
// Just ignore the precision. // Just ignore the precision.
readPositiveInt(); readNonNegativeInt();
read(")"); read(")");
} }
if (readIf("FOR")) { if (readIf("FOR")) {
...@@ -4793,7 +4790,7 @@ public class Parser { ...@@ -4793,7 +4790,7 @@ public class Parser {
Select command = new Select(session); Select command = new Select(session);
currentSelect = command; currentSelect = command;
TableFilter filter = parseValuesTable(0); TableFilter filter = parseValuesTable(0);
ArrayList<Expression> list = Utils.newSmallArrayList(); ArrayList<Expression> list = new ArrayList<>(1);
list.add(new Wildcard(null, null)); list.add(new Wildcard(null, null));
command.setExpressions(list); command.setExpressions(list);
command.addTableFilter(filter, true); command.addTableFilter(filter, true);
...@@ -5087,7 +5084,7 @@ public class Parser { ...@@ -5087,7 +5084,7 @@ public class Parser {
command.setRowBased(false); command.setRowBased(false);
} }
if (readIf("QUEUE")) { if (readIf("QUEUE")) {
command.setQueueSize(readPositiveInt()); command.setQueueSize(readNonNegativeInt());
} }
command.setNoWait(readIf("NOWAIT")); command.setNoWait(readIf("NOWAIT"));
if (readIf("AS")) { if (readIf("AS")) {
...@@ -5170,7 +5167,7 @@ public class Parser { ...@@ -5170,7 +5167,7 @@ public class Parser {
viewsCreated.add(parseSingleCommonTableExpression(isPersistent)); viewsCreated.add(parseSingleCommonTableExpression(isPersistent));
} while (readIf(",")); } while (readIf(","));
Prepared p = null; Prepared p;
// reverse the order of constructed CTE views - as the destruction order // reverse the order of constructed CTE views - as the destruction order
// (since later created view may depend on previously created views - // (since later created view may depend on previously created views -
// we preserve that dependency order in the destruction sequence ) // we preserve that dependency order in the destruction sequence )
...@@ -5218,7 +5215,6 @@ public class Parser { ...@@ -5218,7 +5215,6 @@ public class Parser {
private TableView parseSingleCommonTableExpression(boolean isPersistent) { private TableView parseSingleCommonTableExpression(boolean isPersistent) {
String cteViewName = readIdentifierWithSchema(); String cteViewName = readIdentifierWithSchema();
Schema schema = getSchema(); Schema schema = getSchema();
Table recursiveTable = null;
ArrayList<Column> columns = Utils.newSmallArrayList(); ArrayList<Column> columns = Utils.newSmallArrayList();
String[] cols = null; String[] cols = null;
...@@ -5233,7 +5229,7 @@ public class Parser { ...@@ -5233,7 +5229,7 @@ public class Parser {
} }
} }
Table oldViewFound = null; Table oldViewFound;
if (isPersistent) { if (isPersistent) {
oldViewFound = getSchema().findTableOrView(session, cteViewName); oldViewFound = getSchema().findTableOrView(session, cteViewName);
} else { } else {
...@@ -5265,7 +5261,7 @@ public class Parser { ...@@ -5265,7 +5261,7 @@ public class Parser {
* work (its removed after creation in this method). Only create table * work (its removed after creation in this method). Only create table
* data and table if we don't have a working CTE already. * data and table if we don't have a working CTE already.
*/ */
recursiveTable = TableView.createShadowTableForRecursiveTableExpression( Table recursiveTable = TableView.createShadowTableForRecursiveTableExpression(
isPersistent, session, cteViewName, schema, columns, database); isPersistent, session, cteViewName, schema, columns, database);
List<Column> columnTemplateList; List<Column> columnTemplateList;
String[] querySQLOutput = {null}; String[] querySQLOutput = {null};
...@@ -5653,7 +5649,7 @@ public class Parser { ...@@ -5653,7 +5649,7 @@ public class Parser {
} else if (readIf("NUMBERS")) { } else if (readIf("NUMBERS")) {
command.setInt(Constants.ALLOW_LITERALS_NUMBERS); command.setInt(Constants.ALLOW_LITERALS_NUMBERS);
} else { } else {
command.setInt(readPositiveInt()); command.setInt(readNonNegativeInt());
} }
return command; return command;
} else if (readIf("DEFAULT_TABLE_TYPE")) { } else if (readIf("DEFAULT_TABLE_TYPE")) {
...@@ -5664,7 +5660,7 @@ public class Parser { ...@@ -5664,7 +5660,7 @@ public class Parser {
} else if (readIf("CACHED")) { } else if (readIf("CACHED")) {
command.setInt(Table.TYPE_CACHED); command.setInt(Table.TYPE_CACHED);
} else { } else {
command.setInt(readPositiveInt()); command.setInt(readNonNegativeInt());
} }
return command; return command;
} else if (readIf("CREATE")) { } else if (readIf("CREATE")) {
...@@ -6163,7 +6159,7 @@ public class Parser { ...@@ -6163,7 +6159,7 @@ public class Parser {
// Oracle specifies (but will not require) an opening parenthesis // Oracle specifies (but will not require) an opening parenthesis
boolean hasOpeningBracket = readIf("("); boolean hasOpeningBracket = readIf("(");
String columnName = readColumnIdentifier(); String columnName = readColumnIdentifier();
AlterTableAlterColumn command = null; AlterTableAlterColumn command;
NullConstraintType nullConstraint = parseNotNullConstraint(); NullConstraintType nullConstraint = parseNotNullConstraint();
switch (nullConstraint) { switch (nullConstraint) {
case NULL_IS_ALLOWED: case NULL_IS_ALLOWED:
......
...@@ -390,31 +390,21 @@ public class MergeUsing extends Prepared { ...@@ -390,31 +390,21 @@ public class MergeUsing extends Prepared {
query.prepare(); query.prepare();
} }
int embeddedStatementsCount = 0;
// Prepare each of the sub-commands ready to aid in the MERGE // Prepare each of the sub-commands ready to aid in the MERGE
// collaboration // collaboration
if (updateCommand != null) { if (updateCommand != null) {
updateCommand.setSourceTableFilter(sourceTableFilter); updateCommand.setSourceTableFilter(sourceTableFilter);
updateCommand.setCondition(appendOnCondition(updateCommand)); updateCommand.setCondition(appendOnCondition(updateCommand));
updateCommand.prepare(); updateCommand.prepare();
embeddedStatementsCount++;
} }
if (deleteCommand != null) { if (deleteCommand != null) {
deleteCommand.setSourceTableFilter(sourceTableFilter); deleteCommand.setSourceTableFilter(sourceTableFilter);
deleteCommand.setCondition(appendOnCondition(deleteCommand)); deleteCommand.setCondition(appendOnCondition(deleteCommand));
deleteCommand.prepare(); deleteCommand.prepare();
embeddedStatementsCount++;
} }
if (insertCommand != null) { if (insertCommand != null) {
insertCommand.setSourceTableFilter(sourceTableFilter); insertCommand.setSourceTableFilter(sourceTableFilter);
insertCommand.prepare(); insertCommand.prepare();
embeddedStatementsCount++;
}
if (embeddedStatementsCount == 0) {
throw DbException.get(ErrorCode.SYNTAX_ERROR_1,
"At least UPDATE, DELETE or INSERT embedded statement must be supplied.");
} }
// setup the targetMatchQuery - for detecting if the target row exists // setup the targetMatchQuery - for detecting if the target row exists
......
...@@ -18,9 +18,11 @@ import org.h2.engine.Setting; ...@@ -18,9 +18,11 @@ import org.h2.engine.Setting;
import org.h2.expression.Expression; import org.h2.expression.Expression;
import org.h2.expression.ValueExpression; import org.h2.expression.ValueExpression;
import org.h2.message.DbException; import org.h2.message.DbException;
import org.h2.message.Trace;
import org.h2.result.ResultInterface; import org.h2.result.ResultInterface;
import org.h2.result.RowFactory; import org.h2.result.RowFactory;
import org.h2.schema.Schema; import org.h2.schema.Schema;
import org.h2.security.auth.AuthenticatorFactory;
import org.h2.table.Table; import org.h2.table.Table;
import org.h2.tools.CompressTool; import org.h2.tools.CompressTool;
import org.h2.util.JdbcUtils; import org.h2.util.JdbcUtils;
...@@ -540,6 +542,25 @@ public class Set extends Prepared { ...@@ -540,6 +542,25 @@ public class Set extends Prepared {
session.getColumnNamerConfiguration().configure(expression.getColumnName()); session.getColumnNamerConfiguration().configure(expression.getColumnName());
break; break;
} }
case SetTypes.AUTHENTICATOR: {
session.getUser().checkAdmin();
try {
if (expression.getBooleanValue(session)) {
database.setAuthenticator(AuthenticatorFactory.createAuthenticator());
} else {
database.setAuthenticator(null);
}
addOrUpdateSetting(name,expression.getValue(session).getString(),0);
} catch (Exception e) {
//Errors during start are ignored to allow to open the database
if (database.isStarting()) {
database.getTrace(Trace.DATABASE).error(e, "{0}: failed to set authenticator during database start ",expression.toString());
} else {
throw DbException.convert(e);
}
}
break;
}
default: default:
DbException.throwInternalError("type="+type); DbException.throwInternalError("type="+type);
} }
......
...@@ -252,7 +252,12 @@ public class SetTypes { ...@@ -252,7 +252,12 @@ public class SetTypes {
*/ */
public static final int COLUMN_NAME_RULES = 48; public static final int COLUMN_NAME_RULES = 48;
private static final int COUNT = COLUMN_NAME_RULES + 1; /**
* The type of a SET AUTHENTICATOR statement.
*/
public static final int AUTHENTICATOR= 49;
private static final int COUNT = AUTHENTICATOR + 1;
private static final ArrayList<String> TYPES; private static final ArrayList<String> TYPES;
...@@ -311,6 +316,7 @@ public class SetTypes { ...@@ -311,6 +316,7 @@ public class SetTypes {
list.add(LAZY_QUERY_EXECUTION, "LAZY_QUERY_EXECUTION"); list.add(LAZY_QUERY_EXECUTION, "LAZY_QUERY_EXECUTION");
list.add(BUILTIN_ALIAS_OVERRIDE, "BUILTIN_ALIAS_OVERRIDE"); list.add(BUILTIN_ALIAS_OVERRIDE, "BUILTIN_ALIAS_OVERRIDE");
list.add(COLUMN_NAME_RULES, "COLUMN_NAME_RULES"); list.add(COLUMN_NAME_RULES, "COLUMN_NAME_RULES");
list.add(AUTHENTICATOR, "AUTHENTICATOR");
TYPES = list; TYPES = list;
} }
......
...@@ -74,9 +74,9 @@ public class ConnectionInfo implements Cloneable { ...@@ -74,9 +74,9 @@ public class ConnectionInfo implements Cloneable {
readProperties(info); readProperties(info);
readSettingsFromURL(); readSettingsFromURL();
setUserName(removeProperty("USER", "")); setUserName(removeProperty("USER", ""));
convertPasswords();
name = url.substring(Constants.START_URL.length()); name = url.substring(Constants.START_URL.length());
parseName(); parseName();
convertPasswords();
String recoverTest = removeProperty("RECOVER_TEST", null); String recoverTest = removeProperty("RECOVER_TEST", null);
if (recoverTest != null) { if (recoverTest != null) {
FilePathRec.register(); FilePathRec.register();
...@@ -95,7 +95,7 @@ public class ConnectionInfo implements Cloneable { ...@@ -95,7 +95,7 @@ public class ConnectionInfo implements Cloneable {
"IFEXISTS", "INIT", "PASSWORD", "RECOVER", "RECOVER_TEST", "IFEXISTS", "INIT", "PASSWORD", "RECOVER", "RECOVER_TEST",
"USER", "AUTO_SERVER", "AUTO_SERVER_PORT", "NO_UPGRADE", "USER", "AUTO_SERVER", "AUTO_SERVER_PORT", "NO_UPGRADE",
"AUTO_RECONNECT", "OPEN_NEW", "PAGE_SIZE", "PASSWORD_HASH", "JMX", "AUTO_RECONNECT", "OPEN_NEW", "PAGE_SIZE", "PASSWORD_HASH", "JMX",
"SCOPE_GENERATED_KEYS" }; "SCOPE_GENERATED_KEYS", "AUTHREALM", "AUTHZPWD" };
HashSet<String> set = new HashSet<>(128); HashSet<String> set = new HashSet<>(128);
set.addAll(SetTypes.getTypes()); set.addAll(SetTypes.getTypes());
for (String key : connectionTime) { for (String key : connectionTime) {
...@@ -274,8 +274,15 @@ public class ConnectionInfo implements Cloneable { ...@@ -274,8 +274,15 @@ public class ConnectionInfo implements Cloneable {
} }
} }
private void preservePasswordForAuthentication(Object password) {
if ((!isRemote() || isSSL()) && prop.containsKey("AUTHREALM") && password!=null) {
prop.put("AUTHZPWD",password);
}
}
private char[] removePassword() { private char[] removePassword() {
Object p = prop.remove("PASSWORD"); Object p = prop.remove("PASSWORD");
preservePasswordForAuthentication(p);
if (p == null) { if (p == null) {
return new char[0]; return new char[0];
} else if (p instanceof char[]) { } else if (p instanceof char[]) {
...@@ -657,4 +664,8 @@ public class ConnectionInfo implements Cloneable { ...@@ -657,4 +664,8 @@ public class ConnectionInfo implements Cloneable {
return url; return url;
} }
public void cleanAuthenticationInfo() {
removeProperty("AUTHREALM", false);
removeProperty("AUTHZPWD", false);
}
} }
...@@ -20,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap; ...@@ -20,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.h2.api.DatabaseEventListener; import org.h2.api.DatabaseEventListener;
import org.h2.api.ErrorCode; import org.h2.api.ErrorCode;
import org.h2.api.JavaObjectSerializer; import org.h2.api.JavaObjectSerializer;
...@@ -43,6 +44,8 @@ import org.h2.schema.Schema; ...@@ -43,6 +44,8 @@ import org.h2.schema.Schema;
import org.h2.schema.SchemaObject; import org.h2.schema.SchemaObject;
import org.h2.schema.Sequence; import org.h2.schema.Sequence;
import org.h2.schema.TriggerObject; import org.h2.schema.TriggerObject;
import org.h2.security.auth.Authenticator;
import org.h2.security.auth.AuthenticatorFactory;
import org.h2.store.DataHandler; import org.h2.store.DataHandler;
import org.h2.store.FileLock; import org.h2.store.FileLock;
import org.h2.store.FileLockMethod; import org.h2.store.FileLockMethod;
...@@ -225,6 +228,8 @@ public class Database implements DataHandler { ...@@ -225,6 +228,8 @@ public class Database implements DataHandler {
private QueryStatisticsData queryStatisticsData; private QueryStatisticsData queryStatisticsData;
private RowFactory rowFactory = RowFactory.DEFAULT; private RowFactory rowFactory = RowFactory.DEFAULT;
private Authenticator authenticator;
public Database(ConnectionInfo ci, String cipher) { public Database(ConnectionInfo ci, String cipher) {
if (ASSERT) { if (ASSERT) {
META_LOCK_DEBUGGING.set(null); META_LOCK_DEBUGGING.set(null);
...@@ -2973,4 +2978,23 @@ public class Database implements DataHandler { ...@@ -2973,4 +2978,23 @@ public class Database implements DataHandler {
return engine; return engine;
} }
/**
* get authenticator for database users
* @return authenticator set for database
*/
public Authenticator getAuthenticator() {
return authenticator;
}
/**
* Set current database authenticator
*
* @param authenticator = authenticator to set, null to revert to the Internal authenticator
*/
public void setAuthenticator(Authenticator authenticator) {
if (authenticator!=null) {
authenticator.init(this);
};
this.authenticator=authenticator;
}
} }
...@@ -14,6 +14,9 @@ import org.h2.command.Parser; ...@@ -14,6 +14,9 @@ import org.h2.command.Parser;
import org.h2.command.dml.SetTypes; import org.h2.command.dml.SetTypes;
import org.h2.message.DbException; import org.h2.message.DbException;
import org.h2.message.Trace; import org.h2.message.Trace;
import org.h2.security.auth.AuthenticationException;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.security.auth.Authenticator;
import org.h2.store.FileLock; import org.h2.store.FileLock;
import org.h2.store.FileLockMethod; import org.h2.store.FileLockMethod;
import org.h2.util.MathUtils; import org.h2.util.MathUtils;
...@@ -90,12 +93,28 @@ public class Engine implements SessionFactory { ...@@ -90,12 +93,28 @@ public class Engine implements SessionFactory {
} }
if (user == null) { if (user == null) {
if (database.validateFilePasswordHash(cipher, ci.getFilePasswordHash())) { if (database.validateFilePasswordHash(cipher, ci.getFilePasswordHash())) {
if (ci.getProperty("AUTHREALM")== null) {
user = database.findUser(ci.getUserName()); user = database.findUser(ci.getUserName());
if (user != null) { if (user != null) {
if (!user.validateUserPasswordHash(ci.getUserPasswordHash())) { if (!user.validateUserPasswordHash(ci.getUserPasswordHash())) {
user = null; user = null;
} }
} }
} else {
Authenticator authenticator = database.getAuthenticator();
if (authenticator==null) {
throw DbException.get(ErrorCode.AUTHENTICATOR_NOT_AVAILABLE, name);
} else {
try {
AuthenticationInfo authenticationInfo=new AuthenticationInfo(ci);
user = database.getAuthenticator().authenticate(authenticationInfo, database);
} catch (AuthenticationException authenticationError) {
database.getTrace(Trace.DATABASE).error(authenticationError,
"an error occurred during authentication; user: \"" +
ci.getUserName() + "\"");
}
}
}
} }
if (opened && (user == null || !user.isAdmin())) { if (opened && (user == null || !user.isAdmin())) {
// reset - because the user is not an admin, and has no // reset - because the user is not an admin, and has no
...@@ -110,6 +129,8 @@ public class Engine implements SessionFactory { ...@@ -110,6 +129,8 @@ public class Engine implements SessionFactory {
database.removeSession(null); database.removeSession(null);
throw er; throw er;
} }
//Prevent to set _PASSWORD
ci.cleanAuthenticationInfo();
checkClustering(ci, database); checkClustering(ci, database);
Session session = database.createSession(user); Session session = database.createSession(user);
if (session == null) { if (session == null) {
......
...@@ -5,7 +5,10 @@ ...@@ -5,7 +5,10 @@
*/ */
package org.h2.engine; package org.h2.engine;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import org.h2.table.Table; import org.h2.table.Table;
...@@ -151,6 +154,26 @@ public abstract class RightOwner extends DbObjectBase { ...@@ -151,6 +154,26 @@ public abstract class RightOwner extends DbObjectBase {
} }
} }
/**
* Remove all the temporary rights granted on roles
*/
public void revokeTemporaryRightsOnRoles() {
if (grantedRoles == null) {
return;
}
List<Role> rolesToRemove= new ArrayList<>();
for (Entry<Role,Right> currentEntry : grantedRoles.entrySet()) {
if ( currentEntry.getValue().isTemporary() || !currentEntry.getValue().isValid()) {
rolesToRemove.add(currentEntry.getKey());
}
}
for (Role currentRoleToRemove : rolesToRemove) {
revokeRole(currentRoleToRemove);
}
}
/** /**
* Get the 'grant schema' right of this object. * Get the 'grant schema' right of this object.
* *
......
...@@ -555,6 +555,16 @@ public class SysProperties { ...@@ -555,6 +555,16 @@ public class SysProperties {
public static final String CUSTOM_DATA_TYPES_HANDLER = public static final String CUSTOM_DATA_TYPES_HANDLER =
Utils.getProperty("h2.customDataTypesHandler", null); Utils.getProperty("h2.customDataTypesHandler", null);
/**
* System property <code>h2.authConfigFile</code>
* (default: null).<br />
* authConfigFile define the URL of configuration file
* of {@link org.h2.security.auth.DefaultAuthenticator}
*
*/
public static final String AUTH_CONFIG_FILE =
Utils.getProperty("h2.authConfigFile", null);
private static final String H2_BASE_DIR = "h2.baseDir"; private static final String H2_BASE_DIR = "h2.baseDir";
private SysProperties() { private SysProperties() {
......
package org.h2.engine;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.util.MathUtils;
public class UserBuilder {
/**
* build the database user starting from authentication informations
* @param authenticationInfo = authentication info
* @param database = target database
* @param persistent = true if the user will be persisted in the database
* @return user bean
*/
public static User buildUser(AuthenticationInfo authenticationInfo, Database database, boolean persistent) {
User user = new User(database, persistent ? database.allocateObjectId() : -1, authenticationInfo.getFullyQualifiedName(), false);
//In case of external authentication fill the password hash with random data
user.setUserPasswordHash( authenticationInfo.getRealm()==null ? authenticationInfo.getConnectionInfo().getUserPasswordHash(): MathUtils.secureRandomBytes(64));
user.setTemporary(!persistent);
return user;
}
}
...@@ -174,6 +174,7 @@ ...@@ -174,6 +174,7 @@
90141=Serializer cannot be changed because there is a data table: {0} 90141=Serializer cannot be changed because there is a data table: {0}
90142=Step size must not be zero 90142=Step size must not be zero
90143=Row {1} not found in primary index {0} 90143=Row {1} not found in primary index {0}
90144=Authenticator not enabled on database {0}
HY000=General error: {0} HY000=General error: {0}
HY004=Unknown data type: {0} HY004=Unknown data type: {0}
HYC00=Feature not supported: {0} HYC00=Feature not supported: {0}
......
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
/**
* Exception thrown when an issue occurs during the authentication configuration
*
*/
public class AuthConfigException extends RuntimeException {
public AuthConfigException() {
super();
}
public AuthConfigException(String message) {
super(message);
}
public AuthConfigException(Throwable cause) {
super(cause);
}
public AuthConfigException(String message, Throwable cause) {
super(message, cause);
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
/**
* Exception thrown in case of errors during authentication
*/
public class AuthenticationException extends Exception {
public AuthenticationException() {
super();
}
public AuthenticationException(String message) {
super(message);
}
public AuthenticationException(Throwable cause) {
super(cause);
}
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
import org.h2.engine.ConnectionInfo;
import org.h2.util.StringUtils;
/**
* Input data for authenticators; it wraps ConnectionInfo
*/
public class AuthenticationInfo {
private ConnectionInfo connectionInfo;
private String password;
private String realm;
/*
* Can be used by authenticator to hold informations
*/
Object nestedIdentity;
public AuthenticationInfo(ConnectionInfo connectionInfo) {
this.connectionInfo = connectionInfo;
this.realm = connectionInfo.getProperty("AUTHREALM", null);
if (this.realm!=null) {
this.realm=StringUtils.toUpperEnglish(this.realm);
}
this.password = connectionInfo.getProperty("AUTHZPWD", null);
}
public String getUserName() {
return connectionInfo.getUserName();
}
public String getRealm() {
return realm;
}
public String getPassword() {
return password;
}
public ConnectionInfo getConnectionInfo() {
return connectionInfo;
}
public String getFullyQualifiedName() {
if (realm==null) {
return connectionInfo.getUserName();
} else {
return connectionInfo.getUserName()+"@"+realm;
}
}
/**
* get nested identity
* @return
*/
public Object getNestedIdentity() {
return nestedIdentity;
}
/**
* Method used by authenticators to hold informations about authenticated user
* @param nestedIdentity = nested identity object
*/
public void setNestedIdentity(Object nestedIdentity) {
this.nestedIdentity = nestedIdentity;
}
public void clean() {
this.password = null;
this.nestedIdentity = null;
connectionInfo.cleanAuthenticationInfo();
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
import org.h2.engine.Database;
import org.h2.engine.User;
/**
* Low level interface to implement full authentication process
*/
public interface Authenticator {
/**
* perform user authentication
*
* @param authenticationInfo
* @param database
* @return valid database user or null if user doesn't exists in the database
* @throws AuthenticationException
*/
User authenticate(AuthenticationInfo authenticationInfo, Database database) throws AuthenticationException;
/**
* Initialize the authenticator. This method is invoked by databases when the authenticator is set
* when the authenticator is set.
* @param database = target database
* @throws AuthConfigException
*/
void init(Database database) throws AuthConfigException;
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
/**
* Authenticator factory
*/
public class AuthenticatorFactory {
public static Authenticator createAuthenticator() {
return DefaultAuthenticator.getInstance();
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.h2.util.Utils;
/**
* wrapper for configuration properties
*/
public class ConfigProperties {
private Map<String, String> properties;
public ConfigProperties() {
properties = new HashMap<>();
}
public ConfigProperties(PropertyConfig...configProperties) {
this(configProperties==null?null:Arrays.asList(configProperties));
}
public ConfigProperties(Collection<PropertyConfig> configProperties) {
properties = new HashMap<>();
if (properties != null) {
for (PropertyConfig currentProperty : configProperties) {
if (properties.put(currentProperty.getName(), currentProperty.getValue())!=null) {
throw new AuthConfigException("duplicate property "+currentProperty.getName());
}
}
}
}
public String getStringValue(String name, String defaultValue) {
String result = properties.get(name);
if (result == null) {
return defaultValue;
}
return result;
}
public String getStringValue(String name) {
String result = properties.get(name);
if (result == null) {
throw new AuthConfigException("missing config property "+name);
}
return result;
}
public int getIntValue(String name, int defaultValue) {
String result = properties.get(name);
if (result == null) {
return defaultValue;
}
return Integer.parseInt(result);
}
public int getIntValue(String name) {
String result = properties.get(name);
if (result == null) {
throw new AuthConfigException("missing config property "+name);
}
return Integer.parseInt(result);
}
public boolean getBooleanValue(String name, boolean defaultValue) {
String result = properties.get(name);
if (result == null) {
return defaultValue;
}
return Utils.parseBoolean(result, defaultValue, true);
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
/**
* describe how to perform objects runtime configuration
*/
public interface Configurable {
/**
* configure the component
* @param configProperties = configuration properties
*/
void configure(ConfigProperties configProperties);
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXB;
import org.h2.api.CredentialsValidator;
import org.h2.api.UserToRolesMapper;
import org.h2.engine.Database;
import org.h2.engine.Right;
import org.h2.engine.Role;
import org.h2.engine.SysProperties;
import org.h2.engine.User;
import org.h2.engine.UserBuilder;
import org.h2.message.Trace;
import org.h2.security.auth.impl.AssignRealmNameRole;
import org.h2.security.auth.impl.JaasCredentialsValidator;
import org.h2.util.StringUtils;
/**
* Default authenticator implementation.
* <p>
* When client connectionInfo contains property AUTHREALM={realName} credentials
* (typically user id and password) are validated by
* by {@link org.h2.api.CredentialsValidator} configured for that realm.
* </p>
* <p>
* When client connectionInfo doesn't contains AUTHREALM property credentials
* are validated internally on the database
* </p>
* <p>
* Rights assignment can be managed through {@link org.h2.api.UserToRolesMapper}
* </p>
* <p>
* Default configuration has a realm H2 that validate credentials through JAAS api (appName=h2).
* To customize configuration set h2.authConfigFile system property to refer a valid h2auth.xml config file
* </p>
*/
public class DefaultAuthenticator implements Authenticator {
public static final String DEFAULT_REALMNAME = "H2";
private Map<String, CredentialsValidator> realms = new HashMap<>();
private List<UserToRolesMapper> userToRolesMappers = new ArrayList<>();
private boolean allowUserRegistration;
private boolean persistUsers;
private boolean createMissingRoles;
private boolean skipDefaultInitialization;
private boolean initialized;
private static DefaultAuthenticator instance;
protected static final DefaultAuthenticator getInstance() {
if (instance==null) {
instance= new DefaultAuthenticator();
}
return instance;
}
/**
* Create the Authenticator with default configurations
*/
public DefaultAuthenticator() {
}
/**
* Create authenticator and optionally skip the default configuration. This
* option is useful when the authenticator is configured at code level
*
* @param skipDefaultInitialization
* if true default initialization is skipped
*/
public DefaultAuthenticator(boolean skipDefaultInitialization) {
this.skipDefaultInitialization = skipDefaultInitialization;
}
/**
* If set save users externals defined during the authentication.
*
* @return
*/
public boolean isPersistUsers() {
return persistUsers;
}
public void setPersistUsers(boolean persistUsers) {
this.persistUsers = persistUsers;
}
/**
* If set create external users in the database if not present.
*
* @return
*/
public boolean isAllowUserRegistration() {
return allowUserRegistration;
}
public void setAllowUserRegistration(boolean allowUserRegistration) {
this.allowUserRegistration = allowUserRegistration;
}
/**
* When set create roles not found in the database. If not set roles not found
* in the database are silently skipped
*
* @return
*/
public boolean isCreateMissingRoles() {
return createMissingRoles;
}
public void setCreateMissingRoles(boolean createMissingRoles) {
this.createMissingRoles = createMissingRoles;
}
/**
* Add an authentication realm. Realms are case insensitive
*
* @param name realm name
* @param credentialsValidator credentials validator for realm
*/
public void addRealm(String name, CredentialsValidator credentialsValidator) {
realms.put(StringUtils.toUpperEnglish(name), credentialsValidator);
}
/**
* UserToRoleMappers assign roles to authenticated users
*
* @return current UserToRoleMappers active
*/
public List<UserToRolesMapper> getUserToRolesMappers() {
return userToRolesMappers;
}
public void setUserToRolesMappers(UserToRolesMapper... userToRolesMappers) {
List<UserToRolesMapper> userToRolesMappersList = new ArrayList<>();
for (UserToRolesMapper current : userToRolesMappers) {
userToRolesMappersList.add(current);
}
this.userToRolesMappers = userToRolesMappersList;
}
/**
* Initializes the authenticator (it is called by AuthententicationManager)
*
* this method is skipped if skipDefaultInitialization is set
* Order of initialization is
* <ol>
* <li>Check h2.authConfigFile system property.</li>
* <li>Use the default configuration hard coded</li>
* </ol>
* @param database where authenticator is initialized
* @throws AuthConfigException
*/
public void init(Database database) throws AuthConfigException {
if (skipDefaultInitialization) {
return;
}
if (initialized) {
return;
}
synchronized (this) {
if (initialized) {
return;
}
Trace trace=database.getTrace(Trace.DATABASE);
URL h2AuthenticatorConfigurationUrl = null;
try {
String configFile = SysProperties.AUTH_CONFIG_FILE;
if (configFile != null) {
if (trace.isDebugEnabled()) {
trace.debug("DefaultAuthenticator.config: configuration read from system property h2auth.configurationfile={0}", configFile);
}
h2AuthenticatorConfigurationUrl = new URL(configFile);
}
if (h2AuthenticatorConfigurationUrl == null) {
if (trace.isDebugEnabled()) {
trace.debug("DefaultAuthenticator.config: default configuration");
}
defaultConfiguration();
} else {
configureFromUrl(h2AuthenticatorConfigurationUrl);
}
} catch (Exception e) {
trace.error(e, "DefaultAuthenticator.config: an error occurred during configuration from {0} ", h2AuthenticatorConfigurationUrl);
throw new AuthConfigException("Failed to configure authentication from " + h2AuthenticatorConfigurationUrl,
e);
}
initialized=true;
}
}
void defaultConfiguration() {
createMissingRoles = false;
allowUserRegistration = true;
realms = new HashMap<>();
CredentialsValidator jaasCredentialsValidator = new JaasCredentialsValidator();
jaasCredentialsValidator.configure(new ConfigProperties());
realms.put(DEFAULT_REALMNAME, jaasCredentialsValidator);
UserToRolesMapper assignRealmNameRole = new AssignRealmNameRole();
assignRealmNameRole.configure(new ConfigProperties());
userToRolesMappers.add(assignRealmNameRole);
}
/**
* Configure the authenticator from a configuration file
*
* @param configUrl URL of configuration file
* @throws Exception
*/
public void configureFromUrl(URL configUrl) throws Exception {
H2AuthConfig config = JAXB.unmarshal(configUrl, H2AuthConfig.class);
configureFrom(config);
}
void configureFrom(H2AuthConfig config) throws Exception {
allowUserRegistration = config.isAllowUserRegistration();
createMissingRoles = config.isCreateMissingRoles();
Map<String, CredentialsValidator> newRealms = new HashMap<>();
for (RealmConfig currentRealmConfig : config.getRealms()) {
String currentRealmName = currentRealmConfig.getName();
if (currentRealmName == null) {
throw new Exception("Missing realm name");
}
currentRealmName = currentRealmName.toUpperCase();
CredentialsValidator currentValidator = null;
try {
currentValidator = (CredentialsValidator) Class.forName(currentRealmConfig.getValidatorClass())
.newInstance();
} catch (Exception e) {
throw new Exception("invalid validator class fo realm " + currentRealmName, e);
}
currentValidator.configure(new ConfigProperties(currentRealmConfig.getProperties()));
if (newRealms.put(currentRealmConfig.getName().toUpperCase(), currentValidator) != null) {
throw new Exception("Duplicate realm " + currentRealmConfig.getName());
}
}
this.realms = newRealms;
List<UserToRolesMapper> newUserToRolesMapper = new ArrayList<>();
for (UserToRolesMapperConfig currentUserToRolesMapperConfig : config.getUserToRolesMappers()) {
UserToRolesMapper currentUserToRolesMapper = null;
try {
currentUserToRolesMapper = (UserToRolesMapper) Class
.forName(currentUserToRolesMapperConfig.getClassName()).newInstance();
} catch (Exception e) {
throw new Exception("Invalid class in UserToRolesMapperConfig", e);
}
currentUserToRolesMapper.configure(new ConfigProperties(currentUserToRolesMapperConfig.getProperties()));
newUserToRolesMapper.add(currentUserToRolesMapper);
}
this.userToRolesMappers = newUserToRolesMapper;
}
boolean updateRoles(AuthenticationInfo authenticationInfo, User user, Database database)
throws AuthenticationException {
boolean updatedDb = false;
Set<String> roles = new HashSet<>();
for (UserToRolesMapper currentUserToRolesMapper : userToRolesMappers) {
Collection<String> currentRoles = currentUserToRolesMapper.mapUserToRoles(authenticationInfo);
if (currentRoles != null && !currentRoles.isEmpty()) {
roles.addAll(currentRoles);
}
}
for (String currentRoleName : roles) {
if (currentRoleName == null || currentRoleName.isEmpty()) {
continue;
}
Role currentRole = database.findRole(currentRoleName);
if (currentRole == null && isCreateMissingRoles()) {
synchronized (database.getSystemSession()) {
currentRole = new Role(database, database.allocateObjectId(), currentRoleName, false);
database.addDatabaseObject(database.getSystemSession(), currentRole);
database.getSystemSession().commit(false);
updatedDb = true;
}
}
if (currentRole == null) {
continue;
}
if (user.getRightForRole(currentRole) == null) {
// NON PERSISTENT
Right currentRight = new Right(database, -1, user, currentRole);
currentRight.setTemporary(true);
user.grantRole(currentRole, currentRight);
}
}
return updatedDb;
}
@Override
public final User authenticate(AuthenticationInfo authenticationInfo, Database database)
throws AuthenticationException {
String userName = authenticationInfo.getFullyQualifiedName();
User user = database.findUser(userName);
if (user == null && !isAllowUserRegistration()) {
throw new AuthenticationException("User " + userName + " not found in db");
}
CredentialsValidator validator = realms.get(authenticationInfo.getRealm());
if (validator == null) {
throw new AuthenticationException("realm " + authenticationInfo.getRealm() + " not configured");
}
try {
if (!validator.validateCredentials(authenticationInfo)) {
return null;
}
} catch (Exception e) {
throw new AuthenticationException(e);
}
if (user == null) {
synchronized (database.getSystemSession()) {
user = UserBuilder.buildUser(authenticationInfo, database, isPersistUsers());
database.addDatabaseObject(database.getSystemSession(), user);
database.getSystemSession().commit(false);
}
}
user.revokeTemporaryRightsOnRoles();
updateRoles(authenticationInfo, user, database);
return user;
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Describe configuration of H2 DefaultAuthenticator
*/
@XmlRootElement(name = "h2Auth")
@XmlAccessorType(XmlAccessType.FIELD)
public class H2AuthConfig {
@XmlAttribute
private boolean allowUserRegistration=true;
public boolean isAllowUserRegistration() {
return allowUserRegistration;
}
public void setAllowUserRegistration(boolean allowUserRegistration) {
this.allowUserRegistration = allowUserRegistration;
}
@XmlAttribute
boolean createMissingRoles=true;
public boolean isCreateMissingRoles() {
return createMissingRoles;
}
public void setCreateMissingRoles(boolean createMissingRoles) {
this.createMissingRoles = createMissingRoles;
}
@XmlElement(name = "realm")
List<RealmConfig> realms;
public List<RealmConfig> getRealms() {
if (realms == null) {
realms = new ArrayList<>();
}
return realms;
}
public void setRealms(List<RealmConfig> realms) {
this.realms = realms;
}
@XmlElement(name = "userToRolesMapper")
List<UserToRolesMapperConfig> userToRolesMappers;
public List<UserToRolesMapperConfig> getUserToRolesMappers() {
if (userToRolesMappers == null) {
userToRolesMappers = new ArrayList<>();
}
return userToRolesMappers;
}
public void setUserToRolesMappers(List<UserToRolesMapperConfig> userToRolesMappers) {
this.userToRolesMappers = userToRolesMappers;
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
/**
* Configuration property
*/
@XmlAccessorType(XmlAccessType.FIELD)
public class PropertyConfig {
@XmlAttribute(required = true)
private String name;
@XmlAttribute
private String value;
public PropertyConfig() {
}
public PropertyConfig(String name, String value) {
this.name=name;
this.value=value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
@XmlAccessorType(XmlAccessType.FIELD)
public class RealmConfig {
@XmlAttribute(required = true)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@XmlAttribute(required = true)
String validatorClass;
public String getValidatorClass() {
return validatorClass;
}
public void setValidatorClass(String validatorClass) {
this.validatorClass = validatorClass;
}
@XmlElement(name = "property")
List<PropertyConfig> properties;
public List<PropertyConfig> getProperties() {
if (properties == null) {
properties = new ArrayList<>();
}
return properties;
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
@XmlAccessorType(XmlAccessType.FIELD)
public class UserToRolesMapperConfig {
@XmlAttribute(required = true, name="class")
private String className;
@XmlElement(name = "property")
private List<PropertyConfig> properties;
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public List<PropertyConfig> getProperties() {
if (properties == null) {
properties = new ArrayList<>();
}
return properties;
}
}
package org.h2.security.auth.impl;
import java.util.Arrays;
import java.util.Collection;
import org.h2.api.UserToRolesMapper;
import org.h2.security.auth.AuthenticationException;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.security.auth.ConfigProperties;
/**
* Assign to user a role based on realm name
*
* * <p>
* Configuration parameters:
* </p>
* <ul>
* <li> roleNameFormat, optional by default is @{realm}</li>
* </ul>
*/
public class AssignRealmNameRole implements UserToRolesMapper{
private String roleNameFormat;
public AssignRealmNameRole() {
this("@%s");
}
public AssignRealmNameRole(String roleNameFormat) {
this.roleNameFormat = roleNameFormat;
}
@Override
public void configure(ConfigProperties configProperties) {
roleNameFormat=configProperties.getStringValue("roleNameFormat",roleNameFormat);
}
@Override
public Collection<String> mapUserToRoles(AuthenticationInfo authenticationInfo) throws AuthenticationException {
return Arrays.asList(String.format(roleNameFormat, authenticationInfo.getRealm()));
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth.impl;
import java.io.IOException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import org.h2.api.CredentialsValidator;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.security.auth.ConfigProperties;
/**
* Validate credentials by using standard Java Authentication and Authorization Service
*
* <p>
* Configuration parameters:
* </p>
* <ul>
* <li>appName inside the JAAS configuration (by default h2)</li>
* </ul>
*
*/
public class JaasCredentialsValidator implements CredentialsValidator {
public static final String DEFAULT_APPNAME="h2";
private String appName;
public JaasCredentialsValidator() {
this(DEFAULT_APPNAME);
}
/**
* Create the validator with the given name of JAAS configuration
* @param appName = name of JAAS configuration
*/
public JaasCredentialsValidator(String appName) {
this.appName=appName;
}
@Override
public void configure(ConfigProperties configProperties) {
appName=configProperties.getStringValue("appName",appName);
}
class AuthenticationInfoCallbackHandler implements CallbackHandler {
AuthenticationInfo authenticationInfo;
AuthenticationInfoCallbackHandler(AuthenticationInfo authenticationInfo) {
this.authenticationInfo = authenticationInfo;
}
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
if (callbacks[i] instanceof NameCallback) {
((NameCallback) callbacks[i]).setName(authenticationInfo.getUserName());
} else if (callbacks[i] instanceof PasswordCallback) {
((PasswordCallback) callbacks[i]).setPassword(authenticationInfo.getPassword().toCharArray());
}
}
}
}
@Override
public boolean validateCredentials(AuthenticationInfo authenticationInfo) throws Exception {
LoginContext loginContext = new LoginContext(appName,new AuthenticationInfoCallbackHandler(authenticationInfo));
loginContext.login();
authenticationInfo.setNestedIdentity(loginContext.getSubject());
return true;
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth.impl;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import org.h2.api.CredentialsValidator;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.security.auth.ConfigProperties;
/**
* Validate credentials by performing an LDAP bind
* <p>
* Configuration parameters:
* </p>
* <ul>
* <li>bindDnPattern bind dn pattern with %u instead of username (example: uid=%u,ou=users,dc=example,dc=com)</li>
* <li>host ldap server</li>
* <li>port of ldap service; optional, by default 389 for unsecure, 636 for secure</li>
* <li>secure, optional by default is true (use SSL)</li>
* </ul>
*/
public class LdapCredentialsValidator implements CredentialsValidator {
private String bindDnPattern;
private String host;
private int port;
private boolean secure;
private String url;
@Override
public void configure(ConfigProperties configProperties) {
bindDnPattern = configProperties.getStringValue("bindDnPattern");
host = configProperties.getStringValue("host");
secure = configProperties.getBooleanValue("secure", true);
port = configProperties.getIntValue("port", secure ? 636 : 389);
url = "ldap" + (secure ? "s" : "") + "://" + host + ":" + port;
}
@Override
public boolean validateCredentials(AuthenticationInfo authenticationInfo) throws Exception {
DirContext dirContext = null;
try {
String dn=bindDnPattern.replace("%u", authenticationInfo.getUserName());
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, url);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, dn);
env.put(Context.SECURITY_CREDENTIALS, authenticationInfo.getPassword());
if (secure) {
env.put(Context.SECURITY_PROTOCOL,"ssl");
}
dirContext = new InitialDirContext(env);
authenticationInfo.setNestedIdentity(dn);
return true;
} finally {
if (dirContext != null) {
dirContext.close();
}
}
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth.impl;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import org.h2.api.UserToRolesMapper;
import org.h2.security.auth.AuthenticationException;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.security.auth.ConfigProperties;
/**
* Assign static roles to authenticated users
* <p>
* Configuration parameters:
* </p>
* <ul>
* <li>roles role list separated by comma</li>
* </ul>
*
*/
public class StaticRolesMapper implements UserToRolesMapper {
private Collection<String> roles;
public StaticRolesMapper() {
}
public StaticRolesMapper(String... roles) {
this.roles=Arrays.asList(roles);
}
@Override
public void configure(ConfigProperties configProperties) {
String rolesString=configProperties.getStringValue("roles", "");
if (rolesString!=null) {
roles = new HashSet<>(Arrays.asList(rolesString.split(",")));
}
}
@Override
public Collection<String> mapUserToRoles(AuthenticationInfo authenticationInfo) throws AuthenticationException {
return roles;
}
}
/*
* Copyright 2004-2018 H2 Group. Mul tiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.security.auth.impl;
import java.util.regex.Pattern;
import org.h2.api.CredentialsValidator;
import org.h2.security.SHA256;
import org.h2.security.auth.AuthenticationException;
import org.h2.security.auth.AuthenticationInfo;
import org.h2.security.auth.ConfigProperties;
import org.h2.util.MathUtils;
import org.h2.util.StringUtils;
import org.h2.util.Utils;
/**
* This credentials validator matches the user and password with the configured
* Usage should be limited to test purposes
*
*/
public class StaticUserCredentialsValidator implements CredentialsValidator {
private Pattern userNamePattern;
private String password;
private byte[] salt;
private byte[] hashWithSalt;
public StaticUserCredentialsValidator() {
}
public StaticUserCredentialsValidator(String userNamePattern,String password) {
if (userNamePattern!=null) {
this.userNamePattern=Pattern.compile(userNamePattern.toUpperCase());
}
salt=MathUtils.secureRandomBytes(256);
hashWithSalt=SHA256.getHashWithSalt(password.getBytes(), salt);
}
@Override
public boolean validateCredentials(AuthenticationInfo authenticationInfo) throws AuthenticationException {
if (userNamePattern!=null) {
if (!userNamePattern.matcher(authenticationInfo.getUserName()).matches()) {
return false;
}
}
if (password!=null) {
return password.equals(authenticationInfo.getPassword());
}
return Utils.compareSecure(hashWithSalt,SHA256.getHashWithSalt(authenticationInfo.getPassword().getBytes(), salt));
}
@Override
public void configure(ConfigProperties configProperties) {
String userNamePatternString=configProperties.getStringValue("userNamePattern",null);
if (userNamePatternString!=null) {
userNamePattern=userNamePattern.compile(userNamePatternString);
}
password=configProperties.getStringValue("password",password);
String saltString =configProperties.getStringValue("salt",null);
if (saltString!=null) {
salt=StringUtils.convertHexToBytes(saltString);
}
String hashString=configProperties.getStringValue("hash", null);
if (hashString!=null) {
hashWithSalt = SHA256.getHashWithSalt(StringUtils.convertHexToBytes(hashString), salt);
}
}
}
...@@ -15,6 +15,7 @@ import org.h2.Driver; ...@@ -15,6 +15,7 @@ import org.h2.Driver;
import org.h2.engine.Constants; import org.h2.engine.Constants;
import org.h2.store.fs.FilePathRec; import org.h2.store.fs.FilePathRec;
import org.h2.store.fs.FileUtils; import org.h2.store.fs.FileUtils;
import org.h2.test.auth.TestAuthentication;
import org.h2.test.bench.TestPerformance; import org.h2.test.bench.TestPerformance;
import org.h2.test.db.TestAlter; import org.h2.test.db.TestAlter;
import org.h2.test.db.TestAlterSchemaRename; import org.h2.test.db.TestAlterSchemaRename;
...@@ -972,6 +973,7 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1` ...@@ -972,6 +973,7 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1`
addTest(new TestSampleApps()); addTest(new TestSampleApps());
addTest(new TestStringCache()); addTest(new TestStringCache());
addTest(new TestValueMemory()); addTest(new TestValueMemory());
addTest(new TestAuthentication());
runAddedTests(1); runAddedTests(1);
......
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.test.auth;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
/**
* Dummy login module used for test cases
*/
public class MyLoginModule implements LoginModule{
String password;
CallbackHandler callbackHandler;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
Map<String, ?> options) {
this.callbackHandler=callbackHandler;
password=""+options.get("password");
}
@Override
public boolean login() throws LoginException {
if (callbackHandler==null) {
throw new LoginException("no callbackHandler available");
}
NameCallback nameCallback= new NameCallback("user name");
PasswordCallback passwordCallback= new PasswordCallback("user name",false);
try {
callbackHandler.handle(new Callback[] {nameCallback,passwordCallback});
} catch (Exception exception) {
throw new LoginException("an exception occurred during inquiry of username and password");
}
return password.equals(new String(passwordCallback.getPassword()));
}
@Override
public boolean logout() throws LoginException {
return true;
}
@Override
public boolean abort() throws LoginException {
return true;
}
@Override
public boolean commit() throws LoginException {
return true;
}
}
/*
* Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
* and the EPL 1.0 (http://h2database.com/html/license.html).
* Initial Developer: Alessandro Ventura
*/
package org.h2.test.auth;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Properties;
import java.util.UUID;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
import javax.security.auth.login.Configuration;
import org.h2.engine.ConnectionInfo;
import org.h2.engine.Database;
import org.h2.engine.Engine;
import org.h2.engine.Role;
import org.h2.engine.Session;
import org.h2.engine.User;
import org.h2.security.auth.DefaultAuthenticator;
import org.h2.security.auth.impl.AssignRealmNameRole;
import org.h2.security.auth.impl.JaasCredentialsValidator;
import org.h2.security.auth.impl.StaticRolesMapper;
import org.h2.security.auth.impl.StaticUserCredentialsValidator;
import org.h2.test.TestBase;
public class TestAuthentication extends TestBase {
public static void main(String... a) throws Exception {
TestBase.createCaller().init().test();
}
String externalUserPassword;
String getExternalUserPassword() {
if (externalUserPassword == null) {
externalUserPassword = UUID.randomUUID().toString();
}
return externalUserPassword;
}
String getRealmName() {
return "testRealm";
}
String getJaasConfigName() {
return "testJaasH2";
}
String getStaticRoleName() {
return "staticRole";
}
DefaultAuthenticator defaultAuthenticator;
void configureAuthentication(Database database) {
defaultAuthenticator = new DefaultAuthenticator(true);
defaultAuthenticator.setAllowUserRegistration(true);
defaultAuthenticator.setCreateMissingRoles(true);
defaultAuthenticator.addRealm(getRealmName(), new JaasCredentialsValidator(getJaasConfigName()));
defaultAuthenticator.addRealm(getRealmName() + "_STATIC",
new StaticUserCredentialsValidator("staticuser[0-9]", "staticpassword"));
defaultAuthenticator.setUserToRolesMappers(new AssignRealmNameRole("@%s"),
new StaticRolesMapper(getStaticRoleName()));
database.setAuthenticator(defaultAuthenticator);
}
void configureJaas() {
final Configuration innerConfiguration = Configuration.getConfiguration();
Configuration.setConfiguration(new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
if (name.equals(getJaasConfigName())) {
HashMap<String, String> options = new HashMap<>();
options.put("password", getExternalUserPassword());
return new AppConfigurationEntry[] { new AppConfigurationEntry(MyLoginModule.class.getName(),
LoginModuleControlFlag.REQUIRED, options) };
}
return innerConfiguration.getAppConfigurationEntry(name);
}
});
}
protected String getDatabaseURL() {
return "jdbc:h2:mem:" + getClass().getSimpleName();
}
protected String getExternalUser() {
return "user";
}
Session session;
Database database;
@Override
public void test() throws Exception {
Configuration oldConfiguration = Configuration.getConfiguration();
try {
configureJaas();
Properties properties = new Properties();
properties.setProperty("USER", "dba");
ConnectionInfo connectionInfo = new ConnectionInfo(getDatabaseURL(), properties);
session = Engine.getInstance().createSession(connectionInfo);
database = session.getDatabase();
configureAuthentication(database);
try {
allTests();
} finally {
session.close();
}
} finally {
Configuration.setConfiguration(oldConfiguration);
}
}
protected void allTests() throws Exception {
testInvalidPassword();
testExternalUserWihoutRealm();
testExternalUser();
testAssignRealNameRole();
testStaticRole();
testStaticUserCredentials();
testUserRegistration();
testSet();
}
protected void testInvalidPassword() throws Exception {
try {
Connection wrongLoginConnection = DriverManager.getConnection(
getDatabaseURL() + ";AUTHREALM=" + getRealmName().toUpperCase(), getExternalUser(), "");
wrongLoginConnection.close();
throw new Exception("user should not be able to login with an invalid password");
} catch (SQLException e) {
}
}
protected void testExternalUserWihoutRealm() throws Exception {
try {
Connection wrongLoginConnection = DriverManager.getConnection(getDatabaseURL(), getExternalUser(),
getExternalUserPassword());
wrongLoginConnection.close();
throw new Exception("user should not be able to login without a realm");
} catch (SQLException e) {
}
}
protected void testExternalUser() throws Exception {
Connection rightConnection = DriverManager.getConnection(
getDatabaseURL() + ";AUTHREALM=" + getRealmName().toUpperCase(), getExternalUser(),
getExternalUserPassword());
try {
User user = session.getDatabase().findUser((getExternalUser() + "@" + getRealmName()).toUpperCase());
assertNotNull(user);
} finally {
rightConnection.close();
}
}
protected void testAssignRealNameRole() throws Exception {
String realmNameRoleName = "@" + getRealmName().toUpperCase();
Role realmNameRole = database.findRole(realmNameRoleName);
if (realmNameRole == null) {
realmNameRole = new Role(database, database.allocateObjectId(), realmNameRoleName, false);
session.getDatabase().addDatabaseObject(session, realmNameRole);
session.commit(false);
}
Connection rightConnection = DriverManager.getConnection(
getDatabaseURL() + ";AUTHREALM=" + getRealmName().toUpperCase(), getExternalUser(),
getExternalUserPassword());
try {
User user = session.getDatabase().findUser((getExternalUser() + "@" + getRealmName()).toUpperCase());
assertNotNull(user);
assertTrue(user.isRoleGranted(realmNameRole));
} finally {
rightConnection.close();
}
}
protected void testStaticRole() throws Exception {
Connection rightConnection = DriverManager.getConnection(
getDatabaseURL() + ";AUTHREALM=" + getRealmName().toUpperCase(), getExternalUser(),
getExternalUserPassword());
try {
User user = session.getDatabase().findUser((getExternalUser() + "@" + getRealmName()).toUpperCase());
assertNotNull(user);
Role staticRole = session.getDatabase().findRole(getStaticRoleName());
if (staticRole != null) {
assertTrue(user.isRoleGranted(staticRole));
}
} finally {
rightConnection.close();
}
}
protected void testUserRegistration() throws Exception {
boolean initialValueAllow = defaultAuthenticator.isAllowUserRegistration();
defaultAuthenticator.setAllowUserRegistration(false);
try {
try {
Connection wrongLoginConnection = DriverManager.getConnection(
getDatabaseURL() + ";AUTHREALM=" + getRealmName().toUpperCase(), "___" + getExternalUser(), "");
wrongLoginConnection.close();
throw new Exception(
"unregistered external users should not be able to login when allowUserRegistration=false");
} catch (SQLException e) {
}
String validUserName = "new_" + getExternalUser();
User validUser = new User(database, database.allocateObjectId(),
(validUserName.toUpperCase() + "@" + getRealmName()).toUpperCase(), false);
validUser.setUserPasswordHash(new byte[] {});
database.addDatabaseObject(session, validUser);
session.commit(false);
Connection connectionWithRegisterUser = DriverManager.getConnection(
getDatabaseURL() + ";AUTHREALM=" + getRealmName().toUpperCase(), validUserName,
getExternalUserPassword());
connectionWithRegisterUser.close();
} finally {
defaultAuthenticator.setAllowUserRegistration(initialValueAllow);
}
}
public void testStaticUserCredentials() throws Exception {
String userName="STATICUSER3";
Connection rightConnection = DriverManager.getConnection(
getDatabaseURL() + ";AUTHREALM=" + getRealmName().toUpperCase()+"_STATIC",userName,
"staticpassword");
try {
User user = session.getDatabase().findUser(userName+ "@" + getRealmName().toUpperCase()+"_STATIC");
assertNotNull(user);
} finally {
rightConnection.close();
}
}
protected void testSet() throws Exception{
Connection rightConnection = DriverManager.getConnection(
getDatabaseURL()+";AUTHENTICATOR=FALSE","DBA","");
try {
try {
testExternalUser();
throw new Exception("External user shouldnt be allowed");
}catch (Exception e) {
}
} finally {
configureAuthentication(database);
rightConnection.close();
}
testExternalUser();
}
}
\ No newline at end of file
...@@ -161,7 +161,7 @@ public class TestMergeUsing extends TestBase implements Trigger { ...@@ -161,7 +161,7 @@ public class TestMergeUsing extends TestBase implements Trigger {
GATHER_ORDERED_RESULTS_SQL, GATHER_ORDERED_RESULTS_SQL,
"SELECT X AS ID, 'Marcy'||X AS NAME FROM SYSTEM_RANGE(1,3) WHERE X<0", "SELECT X AS ID, 'Marcy'||X AS NAME FROM SYSTEM_RANGE(1,3) WHERE X<0",
0, 0,
"At least UPDATE, DELETE or INSERT embedded statement must be supplied."); "expected \"WHEN\"");
// Two updates to same row - update and delete together - emptying the // Two updates to same row - update and delete together - emptying the
// parent table // parent table
testMergeUsing( testMergeUsing(
......
...@@ -149,3 +149,29 @@ SELECT * FROM TEST ORDER BY C1, C2; ...@@ -149,3 +149,29 @@ SELECT * FROM TEST ORDER BY C1, C2;
DROP TABLE TEST; DROP TABLE TEST;
> ok > ok
CREATE TABLE TEST (ID INT, VALUE INT);
> ok
MERGE INTO TEST USING DUAL ON (ID = 1)
WHEN MATCHED THEN UPDATE SET VALUE = 1
WHEN;
> exception SYNTAX_ERROR_2
MERGE INTO TEST USING DUAL ON (ID = 1)
WHEN MATCHED THEN UPDATE SET VALUE = 1
WHEN NOT MATCHED THEN;
> exception SYNTAX_ERROR_2
MERGE INTO TEST USING DUAL ON (ID = 1)
WHEN NOT MATCHED THEN INSERT (ID, VALUE) VALUES (1, 1)
WHEN;
> exception SYNTAX_ERROR_2
MERGE INTO TEST USING DUAL ON (ID = 1)
WHEN NOT MATCHED THEN INSERT (ID, VALUE) VALUES (1, 1)
WHEN MATCHED THEN;
> exception SYNTAX_ERROR_2
DROP TABLE TEST;
> ok
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论