Skip to content

Commit 340c7d2

Browse files
committed
Bypass dry-run job for read-only tokens.
1 parent 85f51b2 commit 340c7d2

5 files changed

Lines changed: 111 additions & 0 deletions

File tree

java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@
4141
import com.google.cloud.bigquery.storage.v1.BigQueryWriteClient;
4242
import com.google.cloud.bigquery.storage.v1.BigQueryWriteSettings;
4343
import com.google.cloud.http.HttpTransportOptions;
44+
import com.google.gson.JsonObject;
45+
import com.google.gson.JsonParser;
4446
import java.io.IOException;
4547
import java.io.InputStream;
48+
import java.nio.charset.StandardCharsets;
4649
import java.sql.CallableStatement;
4750
import java.sql.Connection;
4851
import java.sql.DatabaseMetaData;
@@ -53,6 +56,7 @@
5356
import java.sql.Statement;
5457
import java.time.Duration;
5558
import java.util.ArrayList;
59+
import java.util.Base64;
5660
import java.util.ConcurrentModificationException;
5761
import java.util.List;
5862
import java.util.Map;
@@ -143,6 +147,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection {
143147
String partnerToken;
144148
DatabaseMetaData databaseMetaData;
145149
Boolean reqGoogleDriveScope;
150+
private boolean isReadOnlyTokenUsed = false;
146151

147152
BigQueryConnection(String url) throws IOException {
148153
this(url, DataSource.fromUrl(url));
@@ -172,6 +177,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection {
172177
this.jobTimeoutInSeconds = ds.getJobTimeout();
173178
this.authProperties =
174179
BigQueryJdbcOAuthUtility.parseOAuthProperties(ds, this.connectionClassName);
180+
this.isReadOnlyTokenUsed = checkisReadOnlyTokenUsed(this.authProperties);
175181
this.catalog = ds.getProjectId();
176182
this.universeDomain = ds.getUniverseDomain();
177183

@@ -1193,4 +1199,45 @@ public CallableStatement prepareCall(
11931199
}
11941200
return prepareCall(sql);
11951201
}
1202+
1203+
public boolean isReadOnlyTokenUsed() {
1204+
return this.isReadOnlyTokenUsed;
1205+
}
1206+
1207+
private boolean checkisReadOnlyTokenUsed(Map<String, String> authProps) {
1208+
if (authProps == null) {
1209+
return false;
1210+
}
1211+
BigQueryJdbcOAuthUtility.AuthType oauthType =
1212+
BigQueryJdbcOAuthUtility.AuthType.fromValue(
1213+
authProps.get(BigQueryJdbcUrlUtility.OAUTH_TYPE_PROPERTY_NAME));
1214+
if (oauthType != BigQueryJdbcOAuthUtility.AuthType.PRE_GENERATED_TOKEN) {
1215+
return false;
1216+
}
1217+
String token = authProps.get(BigQueryJdbcUrlUtility.OAUTH_ACCESS_TOKEN_PROPERTY_NAME);
1218+
if (token == null || token.isEmpty()) {
1219+
return false;
1220+
}
1221+
1222+
// Try to parse scope from the token if it is a JWT
1223+
try {
1224+
String[] parts = token.split("\\.");
1225+
if (parts.length != 3) {
1226+
return false;
1227+
}
1228+
String payloadJson =
1229+
new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
1230+
JsonObject payload = JsonParser.parseString(payloadJson).getAsJsonObject();
1231+
if (!payload.has("scope")) {
1232+
return false;
1233+
}
1234+
String scope = payload.get("scope").getAsString();
1235+
if (scope.contains("https://www.googleapis.com/auth/bigquery.readonly")) {
1236+
return true;
1237+
}
1238+
} catch (Exception e) {
1239+
// Likely invalid token and auth is going to fail later.
1240+
}
1241+
return false;
1242+
}
11961243
}

java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOAuthUtility.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,5 +749,16 @@ static AuthType fromValue(int value) {
749749
LOG.severe(ex.getMessage(), ex);
750750
throw ex;
751751
}
752+
753+
static AuthType fromValue(String value) {
754+
for (AuthType authType : values()) {
755+
if (authType.name().equalsIgnoreCase(value)) {
756+
return authType;
757+
}
758+
}
759+
IllegalStateException ex = new IllegalStateException(OAUTH_TYPE_ERROR_MESSAGE + ": " + value);
760+
LOG.severe(ex.getMessage(), ex);
761+
throw ex;
762+
}
752763
}
753764
}

java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,13 @@ private boolean executeImpl(String sql) throws SQLException {
333333

334334
StatementType getStatementType(QueryJobConfiguration queryJobConfiguration) throws SQLException {
335335
LOG.finest("++enter++");
336+
// BQ Read-only tokens are not recommended to use, they have a lot of known flaws.
337+
// We're supporting them in a limited capacity, for pure SELECT statements.
338+
if (this.connection.isReadOnlyTokenUsed()) {
339+
LOG.warning(
340+
"Read-only token detected, skipping dry run and assuming StatementType is SELECT.");
341+
return StatementType.SELECT;
342+
}
336343
QueryJobConfiguration dryRunJobConfiguration =
337344
queryJobConfiguration.toBuilder().setDryRun(true).build();
338345
Job job;

java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@
3030
import java.io.InputStream;
3131
import java.sql.SQLException;
3232
import java.util.Properties;
33+
import java.nio.charset.StandardCharsets;
34+
import java.util.Base64;
3335
import org.junit.jupiter.api.BeforeEach;
3436
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.CsvSource;
3539

3640
public class BigQueryConnectionTest {
3741

@@ -437,4 +441,26 @@ public void testWithDriveScopeDefault() throws Exception {
437441
assertFalse(connection.reqGoogleDriveScope);
438442
}
439443
}
444+
445+
@ParameterizedTest
446+
@CsvSource({
447+
"https://www.googleapis.com/auth/bigquery.readonly, true",
448+
"https://www.googleapis.com/auth/bigquery, false"
449+
})
450+
public void testIsReadOnlyTokenProvided(String scope, boolean expectedIsReadOnly) throws Exception {
451+
String payload = "{\"scope\":\"" + scope + "\"}";
452+
String encodedPayload =
453+
Base64.getUrlEncoder()
454+
.encodeToString(payload.getBytes(StandardCharsets.UTF_8));
455+
String url =
456+
"jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;"
457+
+ "OAuthType=2;ProjectId=MyBigQueryProject;"
458+
+ "OAuthAccessToken=header."
459+
+ encodedPayload
460+
+ ".signature;";
461+
462+
try (BigQueryConnection connection = new BigQueryConnection(url)) {
463+
assertEquals(expectedIsReadOnly, connection.isReadOnlyTokenUsed());
464+
}
465+
}
440466
}

java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
import org.junit.jupiter.api.BeforeEach;
6969
import org.junit.jupiter.api.Disabled;
7070
import org.junit.jupiter.api.Test;
71+
import org.junit.jupiter.params.ParameterizedTest;
72+
import org.junit.jupiter.params.provider.ValueSource;
7173
import org.mockito.ArgumentCaptor;
7274
import org.mockito.Mockito;
7375

@@ -480,4 +482,22 @@ public void testCancelWithJoblessQuery() throws SQLException, InterruptedExcepti
480482
// And no backend cancellation was attempted
481483
verify(bigquery, Mockito.never()).cancel(any(JobId.class));
482484
}
485+
486+
@ParameterizedTest
487+
@ValueSource(booleans = {true, false})
488+
public void testGetStatementType(boolean isReadOnlyTokenUsed) throws Exception {
489+
doReturn(isReadOnlyTokenUsed).when(bigQueryConnection).isReadOnlyTokenUsed();
490+
491+
Job dryRunJobMock = getJobMock(null, null, StatementType.SELECT);
492+
doReturn(dryRunJobMock).when(bigquery).create(any(JobInfo.class));
493+
494+
BigQueryStatement statementSpy = Mockito.spy(bigQueryStatement);
495+
QueryJobConfiguration queryJobConfiguration = QueryJobConfiguration.newBuilder(query).build();
496+
497+
StatementType type = statementSpy.getStatementType(queryJobConfiguration);
498+
499+
assertThat(type).isEqualTo(StatementType.SELECT);
500+
verify(bigquery, isReadOnlyTokenUsed ? Mockito.never() : Mockito.times(1))
501+
.create(any(JobInfo.class));
502+
}
483503
}

0 commit comments

Comments
 (0)