Skip to content

Commit a5a64d3

Browse files
committed
Add support for Connection.abort
Include JDBC security requirements for both Connection.abort and Connection.setNetworkTimeout methods. Closes: #71
1 parent 9392133 commit a5a64d3

File tree

3 files changed

+260
-2
lines changed

3 files changed

+260
-2
lines changed

src/main/java/org/tarantool/jdbc/SQLConnection.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.sql.SQLFeatureNotSupportedException;
2828
import java.sql.SQLNonTransientConnectionException;
2929
import java.sql.SQLNonTransientException;
30+
import java.sql.SQLPermission;
3031
import java.sql.SQLWarning;
3132
import java.sql.SQLXML;
3233
import java.sql.Savepoint;
@@ -42,6 +43,7 @@
4243
import java.util.concurrent.Executor;
4344
import java.util.concurrent.Future;
4445
import java.util.concurrent.TimeoutException;
46+
import java.util.concurrent.atomic.AtomicBoolean;
4547
import java.util.function.Function;
4648

4749
/**
@@ -51,6 +53,9 @@
5153
*/
5254
public class SQLConnection implements TarantoolConnection {
5355

56+
private static final SQLPermission CALL_ABORT_PERMISSION = new SQLPermission("callAbort");
57+
private static final SQLPermission SET_NETWORK_TIMEOUT_PERMISSION = new SQLPermission("setNetworkTimeout");
58+
5459
private static final int UNSET_HOLDABILITY = 0;
5560
private static final String PING_QUERY = "SELECT 1";
5661

@@ -60,6 +65,8 @@ public class SQLConnection implements TarantoolConnection {
6065
private DatabaseMetaData cachedMetadata;
6166
private int resultSetHoldability = UNSET_HOLDABILITY;
6267

68+
private final AtomicBoolean isClosed = new AtomicBoolean(false);
69+
6370
public SQLConnection(String url, Properties properties) throws SQLException {
6471
this.url = url;
6572
this.properties = properties;
@@ -205,6 +212,12 @@ public boolean getAutoCommit() throws SQLException {
205212

206213
@Override
207214
public void close() throws SQLException {
215+
if (isClosed.compareAndSet(false, true)) {
216+
closeInternal();
217+
}
218+
}
219+
220+
private void closeInternal() {
208221
client.close();
209222
}
210223

@@ -234,7 +247,7 @@ public void rollback(Savepoint savepoint) throws SQLException {
234247

235248
@Override
236249
public boolean isClosed() throws SQLException {
237-
return client.isClosed();
250+
return isClosed.get() || client.isClosed();
238251
}
239252

240253
@Override
@@ -417,6 +430,7 @@ public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLExc
417430
if (milliseconds < 0) {
418431
throw new SQLException("Network timeout cannot be negative.");
419432
}
433+
SET_NETWORK_TIMEOUT_PERMISSION.checkGuard(this);
420434
client.setOperationTimeout(milliseconds);
421435
}
422436

@@ -515,7 +529,16 @@ public void abort(Executor executor) throws SQLException {
515529
if (isClosed()) {
516530
return;
517531
}
518-
throw new SQLFeatureNotSupportedException();
532+
if (executor == null) {
533+
throw new SQLNonTransientException(
534+
"Executor cannot be null",
535+
SQLStates.INVALID_PARAMETER_VALUE.getSqlState()
536+
);
537+
}
538+
CALL_ABORT_PERMISSION.checkGuard(this);
539+
if (isClosed.compareAndSet(false, true)) {
540+
executor.execute(this::closeInternal);
541+
}
519542
}
520543

521544
@Override

src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertFalse;
55
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
import static org.junit.jupiter.api.Assertions.assertNull;
67
import static org.junit.jupiter.api.Assertions.assertThrows;
78
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
import static org.junit.jupiter.api.Assertions.fail;
810
import static org.tarantool.TestAssumptions.assumeMinimalServerVersion;
911

1012
import org.tarantool.ServerVersion;
1113
import org.tarantool.TarantoolTestHelper;
14+
import org.tarantool.util.SQLStates;
1215

1316
import org.junit.jupiter.api.AfterAll;
1417
import org.junit.jupiter.api.AfterEach;
@@ -26,8 +29,14 @@
2629
import java.sql.SQLClientInfoException;
2730
import java.sql.SQLException;
2831
import java.sql.SQLFeatureNotSupportedException;
32+
import java.sql.SQLNonTransientException;
2933
import java.sql.Statement;
3034
import java.util.Map;
35+
import java.util.concurrent.ExecutionException;
36+
import java.util.concurrent.ExecutorService;
37+
import java.util.concurrent.Executors;
38+
import java.util.concurrent.Future;
39+
import java.util.concurrent.TimeUnit;
3140

3241
public class JdbcConnectionIT {
3342

@@ -456,5 +465,81 @@ void testSetClientInfoProperties() {
456465
assertEquals(ClientInfoStatus.REASON_UNKNOWN_PROPERTY, failedProperties.get(targetProperty));
457466
}
458467

468+
@Test
469+
void testConnectionAbort() throws SQLException {
470+
assertFalse(conn.isClosed());
471+
try (Statement statement = conn.createStatement()) {
472+
conn.abort(Executors.newSingleThreadExecutor());
473+
assertTrue(conn.isClosed());
474+
SQLNonTransientException exception = assertThrows(
475+
SQLNonTransientException.class,
476+
() -> statement.executeQuery("SELECT 1")
477+
);
478+
assertEquals(exception.getMessage(), "Statement is closed.");
479+
}
480+
}
481+
482+
@Test
483+
void testOperationInProgressAbort() throws SQLException, ExecutionException, InterruptedException {
484+
testHelper.executeLua("box.internal.sql_create_function('TNT_SLEEP', 'INT'," +
485+
" function(s) require('fiber').sleep(s); return s; end)");
486+
final ExecutorService executor = Executors.newFixedThreadPool(2);
487+
final int sleepSeconds = 10;
488+
489+
long startTime = System.currentTimeMillis();
490+
491+
Future<SQLException> workerFuture = executor.submit(() -> {
492+
try {
493+
Statement statement = conn.createStatement();
494+
statement.execute("SELECT tnt_sleep(" + sleepSeconds + ")");
495+
} catch (SQLException cause) {
496+
return cause;
497+
}
498+
return null;
499+
});
500+
501+
Future<SQLException> abortFuture = executor.submit(() -> {
502+
ExecutorService abortExecutor = Executors.newSingleThreadExecutor();
503+
try {
504+
conn.abort(abortExecutor);
505+
} catch (SQLException cause) {
506+
return cause;
507+
}
508+
abortExecutor.shutdown();
509+
try {
510+
abortExecutor.awaitTermination(sleepSeconds, TimeUnit.SECONDS);
511+
} catch (InterruptedException ignored) {
512+
}
513+
return null;
514+
});
515+
516+
SQLException workerException = workerFuture.get();
517+
long endTime = System.currentTimeMillis();
518+
assertNotNull(workerException, "Statement execution should have been aborted, thus throwing an exception");
519+
520+
SQLException abortException = abortFuture.get();
521+
assertNull(abortException, () -> abortException.getMessage());
522+
523+
// It is expected to abort the statement as soon as possible.
524+
// If the execution takes time more than 95% of the estimation the aborting fails.
525+
assertTrue((endTime - startTime) < (sleepSeconds * 95 * 10));
526+
assertTrue(conn.isClosed());
527+
}
528+
529+
@Test
530+
void testAlreadyClosedConnectionAbort() throws SQLException {
531+
conn.close();
532+
try {
533+
conn.abort(Executors.newSingleThreadExecutor());
534+
} catch (SQLException cause) {
535+
fail("Unexpected error", cause);
536+
}
537+
}
538+
539+
@Test
540+
void testNullParameterConnectionAbort() {
541+
SQLException exception = assertThrows(SQLException.class, () -> conn.abort(null));
542+
assertEquals(SQLStates.INVALID_PARAMETER_VALUE.getSqlState(), exception.getSQLState());
543+
}
459544
}
460545

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package org.tarantool.jdbc;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.tarantool.TestAssumptions.assumeMinimalServerVersion;
6+
7+
import org.tarantool.ServerVersion;
8+
import org.tarantool.TarantoolTestHelper;
9+
10+
import org.junit.jupiter.api.AfterAll;
11+
import org.junit.jupiter.api.AfterEach;
12+
import org.junit.jupiter.api.BeforeAll;
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.jupiter.api.Test;
15+
16+
import java.security.Permission;
17+
import java.sql.Connection;
18+
import java.sql.DriverManager;
19+
import java.sql.SQLException;
20+
import java.util.EnumSet;
21+
import java.util.concurrent.Executors;
22+
23+
public class JdbcSecurityIT {
24+
25+
private static TarantoolTestHelper testHelper;
26+
27+
private Connection connection;
28+
private SecurityManager originalSecurityManager;
29+
30+
@BeforeAll
31+
public static void setupEnv() {
32+
testHelper = new TarantoolTestHelper("jdbc-security-it");
33+
testHelper.createInstance();
34+
testHelper.startInstance();
35+
}
36+
37+
@AfterAll
38+
public static void teardownEnv() {
39+
testHelper.stopInstance();
40+
}
41+
42+
@BeforeEach
43+
public void setUpTest() throws SQLException {
44+
assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_1);
45+
connection = DriverManager.getConnection(SqlTestUtils.makeDefaultJdbcUrl());
46+
originalSecurityManager = System.getSecurityManager();
47+
}
48+
49+
@AfterEach
50+
public void tearDownTest() throws SQLException {
51+
assumeMinimalServerVersion(testHelper.getInstanceVersion(), ServerVersion.V_2_1);
52+
if (connection != null) {
53+
connection.close();
54+
}
55+
System.setSecurityManager(originalSecurityManager);
56+
}
57+
58+
@Test
59+
void testDeniedConnectionAbort() {
60+
EnumSet<JdbcPermission> exclusions = EnumSet.of(JdbcPermission.CALL_ABORT);
61+
System.setSecurityManager(new JdbcSecurityManager(true, exclusions));
62+
63+
SecurityException securityException = assertThrows(
64+
SecurityException.class,
65+
() -> connection.abort(Executors.newSingleThreadExecutor())
66+
);
67+
assertEquals(securityException.getMessage(), "Permission callAbort is not allowed");
68+
}
69+
70+
@Test
71+
void testDeniedSetConnectionTimeout() {
72+
EnumSet<JdbcPermission> exclusions = EnumSet.of(JdbcPermission.SET_NETWORK_TIMEOUT);
73+
System.setSecurityManager(new JdbcSecurityManager(true, exclusions));
74+
75+
SecurityException securityException = assertThrows(
76+
SecurityException.class,
77+
() -> connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 1000)
78+
);
79+
assertEquals(securityException.getMessage(), "Permission setNetworkTimeout is not allowed");
80+
}
81+
82+
/**
83+
* Lists permissions supported by JDBC API.
84+
*
85+
* <ul>
86+
* <li>setLog</li>
87+
* <li>callAbort</li>
88+
* <li>setSyncFactory<</li>
89+
* <li>setNetworkTimeout</li>
90+
* <li>deregisterDriver</li>
91+
* </ul>
92+
*
93+
* @see java.sql.SQLPermission
94+
*/
95+
private enum JdbcPermission {
96+
SET_LOG("setLog"),
97+
CALL_ABORT("callAbort"),
98+
SET_SYNC_FACTORY("setSyncFactory"),
99+
SET_NETWORK_TIMEOUT("setNetworkTimeout"),
100+
DEREGISTER_DRIVER("deregisterDriver");
101+
102+
private final String permissionName;
103+
104+
JdbcPermission(String permissionName) {
105+
this.permissionName = permissionName;
106+
}
107+
108+
public String getPermissionName() {
109+
return permissionName;
110+
}
111+
112+
public static JdbcPermission fromName(String name) {
113+
for (JdbcPermission values : JdbcPermission.values()) {
114+
if (values.permissionName.equals(name)) {
115+
return values;
116+
}
117+
}
118+
return null;
119+
}
120+
}
121+
122+
private static class JdbcSecurityManager extends SecurityManager {
123+
private final boolean allowAll;
124+
private final EnumSet<JdbcPermission> exclusions;
125+
126+
/**
127+
* Configures a new {@link SecurityManager} that follows the custom rules.
128+
*
129+
* @param allowAll whether permissions are allowed by default or not
130+
* @param exclusions optional set of exclusions
131+
*/
132+
private JdbcSecurityManager(boolean allowAll, EnumSet<JdbcPermission> exclusions) {
133+
this.exclusions = exclusions;
134+
this.allowAll = allowAll;
135+
}
136+
137+
@Override
138+
public void checkPermission(Permission permission) {
139+
JdbcPermission jdbcPermission = JdbcPermission.fromName(permission.getName());
140+
if (jdbcPermission == null) {
141+
return;
142+
}
143+
boolean allowed = allowAll ^ exclusions.contains(jdbcPermission);
144+
if (!allowed) {
145+
throw new SecurityException("Permission " + jdbcPermission.getPermissionName() + " is not allowed");
146+
}
147+
}
148+
}
149+
}
150+

0 commit comments

Comments
 (0)