Skip to content

Commit

Permalink
[PLAT-12013] API to run SQL query against particular DB node
Browse files Browse the repository at this point in the history
Summary:
Allow running SQL query againt YB DB or particular YB DB node via API.
Removing OSS mode completely as it's not working properly anyway and is only checked in run_query API.
The ability to run SQL query is disabled by default and should be enabled manually via runtime config (for security purposes).

Test Plan:
Create universe.
Try to call run_query API. Make sure "API is disabled..." error is returned.
Enable API via yb.security.enable_db_query_api runtime configuration flag for particular universe.
Try to call run_query API few times without specifying a DB node. Make sure the query is executed against random live tserver.
Try to call run_query API few times specifying a DB node. Make sure the query is executed against particular DB node.

Reviewers: vbansal, cdavid, #yba-api-review!

Reviewed By: vbansal, cdavid

Subscribers: yugaware

Differential Revision: https://phorge.dev.yugabyte.com/D31321
  • Loading branch information
anmalysh-yb committed Dec 26, 2023
1 parent 639e3ef commit 0eedfbc
Show file tree
Hide file tree
Showing 18 changed files with 175 additions and 209 deletions.
111 changes: 54 additions & 57 deletions managed/src/main/java/Module.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,67 +168,64 @@ public void configure() {
bind(ExecutorServiceProvider.class).to(DefaultExecutorServiceProvider.class);
bind(NodeManagerInterface.class).to(PerfAdvisorNodeManager.class);

// We only needed to bind below ones for Platform mode.
if (config.getString("yb.mode").equals("PLATFORM")) {
bind(PerfAdvisor.class).asEagerSingleton();
bind(SwamperHelper.class).asEagerSingleton();
bind(NodeManager.class).asEagerSingleton();
bind(MetricQueryHelper.class).asEagerSingleton();
bind(QueryHelper.class).asEagerSingleton();
bind(ShellProcessHandler.class).asEagerSingleton();
bind(NetworkManager.class).asEagerSingleton();
bind(AccessManager.class).asEagerSingleton();
bind(ReleaseManager.class).asEagerSingleton();
bind(TemplateManager.class).asEagerSingleton();
bind(ExtraMigrationManager.class).asEagerSingleton();
bind(AWSInitializer.class).asEagerSingleton();
bind(CallHome.class).asEagerSingleton();
bind(Scheduler.class).asEagerSingleton();
bind(HealthChecker.class).asEagerSingleton();
bind(TaskGarbageCollector.class).asEagerSingleton();
bind(PitrConfigPoller.class).asEagerSingleton();
bind(BackupGarbageCollector.class).asEagerSingleton();
bind(SupportBundleCleanup.class).asEagerSingleton();
bind(EncryptionAtRestManager.class).asEagerSingleton();
bind(EncryptionAtRestUniverseKeyCache.class).asEagerSingleton();
bind(SetUniverseKey.class).asEagerSingleton();
bind(RefreshKmsService.class).asEagerSingleton();
bind(CustomerTaskManager.class).asEagerSingleton();
bind(YamlWrapper.class).asEagerSingleton();
bind(AlertManager.class).asEagerSingleton();
bind(QueryAlerts.class).asEagerSingleton();
bind(PlatformMetricsProcessor.class).asEagerSingleton();
bind(AlertsGarbageCollector.class).asEagerSingleton();
bind(AlertConfigurationWriter.class).asEagerSingleton();
bind(SwamperTargetsFileUpdater.class).asEagerSingleton();
bind(PlatformReplicationManager.class).asEagerSingleton();
bind(PlatformReplicationHelper.class).asEagerSingleton();
bind(GFlagsValidation.class).asEagerSingleton();
bind(XClusterUniverseService.class).asEagerSingleton();
bind(TaskExecutor.class).asEagerSingleton();
bind(ShellKubernetesManager.class).asEagerSingleton();
bind(NativeKubernetesManager.class).asEagerSingleton();
bind(SupportBundleUtil.class).asEagerSingleton();
bind(MetricGrafanaController.class).asEagerSingleton();
bind(PlatformScheduler.class).asEagerSingleton();
bind(AccessKeyRotationUtil.class).asEagerSingleton();
bind(GcpEARServiceUtil.class).asEagerSingleton();
bind(YbcUpgrade.class).asEagerSingleton();
bind(PerfAdvisorScheduler.class).asEagerSingleton();
bind(PermissionUtil.class).asEagerSingleton();
bind(RoleUtil.class).asEagerSingleton();
bind(RoleBindingUtil.class).asEagerSingleton();
bind(PrometheusConfigManager.class).asEagerSingleton();
bind(PrometheusConfigHelper.class).asEagerSingleton();
requestStaticInjection(CertificateInfo.class);
requestStaticInjection(HealthCheck.class);
requestStaticInjection(AppConfigHelper.class);
}

bind(PerfAdvisor.class).asEagerSingleton();
bind(SwamperHelper.class).asEagerSingleton();
bind(NodeManager.class).asEagerSingleton();
bind(MetricQueryHelper.class).asEagerSingleton();
bind(QueryHelper.class).asEagerSingleton();
bind(ShellProcessHandler.class).asEagerSingleton();
bind(NetworkManager.class).asEagerSingleton();
bind(AccessManager.class).asEagerSingleton();
bind(ReleaseManager.class).asEagerSingleton();
bind(TemplateManager.class).asEagerSingleton();
bind(ExtraMigrationManager.class).asEagerSingleton();
bind(AWSInitializer.class).asEagerSingleton();
bind(CallHome.class).asEagerSingleton();
bind(Scheduler.class).asEagerSingleton();
bind(HealthChecker.class).asEagerSingleton();
bind(TaskGarbageCollector.class).asEagerSingleton();
bind(PitrConfigPoller.class).asEagerSingleton();
bind(BackupGarbageCollector.class).asEagerSingleton();
bind(SupportBundleCleanup.class).asEagerSingleton();
bind(EncryptionAtRestManager.class).asEagerSingleton();
bind(EncryptionAtRestUniverseKeyCache.class).asEagerSingleton();
bind(SetUniverseKey.class).asEagerSingleton();
bind(RefreshKmsService.class).asEagerSingleton();
bind(CustomerTaskManager.class).asEagerSingleton();
bind(YamlWrapper.class).asEagerSingleton();
bind(AlertManager.class).asEagerSingleton();
bind(QueryAlerts.class).asEagerSingleton();
bind(PlatformMetricsProcessor.class).asEagerSingleton();
bind(AlertsGarbageCollector.class).asEagerSingleton();
bind(AlertConfigurationWriter.class).asEagerSingleton();
bind(SwamperTargetsFileUpdater.class).asEagerSingleton();
bind(PlatformReplicationManager.class).asEagerSingleton();
bind(PlatformReplicationHelper.class).asEagerSingleton();
bind(GFlagsValidation.class).asEagerSingleton();
bind(XClusterUniverseService.class).asEagerSingleton();
bind(TaskExecutor.class).asEagerSingleton();
bind(ShellKubernetesManager.class).asEagerSingleton();
bind(NativeKubernetesManager.class).asEagerSingleton();
bind(SupportBundleUtil.class).asEagerSingleton();
bind(MetricGrafanaController.class).asEagerSingleton();
bind(PlatformScheduler.class).asEagerSingleton();
bind(AccessKeyRotationUtil.class).asEagerSingleton();
bind(GcpEARServiceUtil.class).asEagerSingleton();
bind(YbcUpgrade.class).asEagerSingleton();
bind(PerfAdvisorScheduler.class).asEagerSingleton();
bind(PermissionUtil.class).asEagerSingleton();
bind(RoleUtil.class).asEagerSingleton();
bind(RoleBindingUtil.class).asEagerSingleton();
bind(PrometheusConfigManager.class).asEagerSingleton();
bind(PrometheusConfigHelper.class).asEagerSingleton();
bind(YbClientConfigFactory.class).asEagerSingleton();
bind(OperatorStatusUpdaterFactory.class).asEagerSingleton();
bind(YBInformerFactory.class).asEagerSingleton();
bind(YBReconcilerFactory.class).asEagerSingleton();

requestStaticInjection(CertificateInfo.class);
requestStaticInjection(HealthCheck.class);
requestStaticInjection(AppConfigHelper.class);
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ public void run() {

private boolean checkPostgresStatus(Universe universe) {
RunQueryFormData runQueryFormData = new RunQueryFormData();
runQueryFormData.query = "SELECT version()";
runQueryFormData.db_name = "system_platform";
runQueryFormData.setQuery("SELECT version()");
runQueryFormData.setDbName("system_platform");
UniverseDefinitionTaskParams.UserIntent userIntent = taskParams().userIntent;
if (userIntent == null) {
userIntent = universe.getUniverseDetails().getPrimaryCluster().userIntent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ private HashMap<String, List<String>> queryDb(boolean isYsql, String dbUser, Str
respDB = ysqlQueryExecutor.runUserDbCommands(YSQL_GET_USERS, "", universe);
} else {
RunQueryFormData queryFormData = new RunQueryFormData();
queryFormData.query = YCQL_GET_USERS;
queryFormData.tableType = TableType.YQL_TABLE_TYPE;
queryFormData.setQuery(YCQL_GET_USERS);
queryFormData.setTableType(TableType.YQL_TABLE_TYPE);
respDB = ycqlQueryExecutor.executeQuery(universe, queryFormData, true, dbUser, password);
}

Expand Down Expand Up @@ -266,8 +266,8 @@ public void run() {
int i = 0;
for (String query : queries) {
RunQueryFormData queryFormData = new RunQueryFormData();
queryFormData.query = query;
queryFormData.tableType = TableType.YQL_TABLE_TYPE;
queryFormData.setQuery(query);
queryFormData.setTableType(TableType.YQL_TABLE_TYPE);
JsonNode resp =
ycqlQueryExecutor.executeQuery(
getUniverse(),
Expand Down
16 changes: 6 additions & 10 deletions managed/src/main/java/com/yugabyte/yw/common/AppInit.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ public AppInit(
try {
log.info("Yugaware Application has started");

String mode = config.getString("yb.mode");

if (!environment.isTest()) {
// only start thread dump collection for YBM at this time
if (config.getBoolean("yb.cloud.enabled")) {
Expand Down Expand Up @@ -146,15 +144,13 @@ public AppInit(
.toString());
fileDataService.syncFileData(storagePath, ywFileDataSynced);

if (mode.equals("PLATFORM")) {
String devopsHome = config.getString("yb.devops.home");
if (devopsHome == null || devopsHome.length() == 0) {
throw new RuntimeException("yb.devops.home is not set in application.conf");
}
String devopsHome = config.getString("yb.devops.home");
if (devopsHome == null || devopsHome.length() == 0) {
throw new RuntimeException("yb.devops.home is not set in application.conf");
}

if (storagePath == null || storagePath.length() == 0) {
throw new RuntimeException(("yb.storage.path is not set in application.conf"));
}
if (storagePath == null || storagePath.length() == 0) {
throw new RuntimeException(("yb.storage.path is not set in application.conf"));
}

boolean vmOsPatchingEnabled = confGetter.getGlobalConf(GlobalConfKeys.enableVMOSPatching);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ public void createUser(Universe universe, DatabaseUserFormData data) {
// --use_cassandra_authentication=true
// This is always true if the universe was created via cloud.
RunQueryFormData ycqlQuery = new RunQueryFormData();
ycqlQuery.query =
ycqlQuery.setQuery(
String.format(
"CREATE ROLE '%s' WITH SUPERUSER=true AND LOGIN=true AND PASSWORD='%s'",
Util.escapeSingleQuotesOnly(data.username), Util.escapeSingleQuotesOnly(data.password));
Util.escapeSingleQuotesOnly(data.username),
Util.escapeSingleQuotesOnly(data.password)));
JsonNode ycqlResponse =
executeQuery(universe, ycqlQuery, true, data.ycqlAdminUsername, data.ycqlAdminPassword);
LOG.info("Creating YCQL user, result: " + ycqlResponse.toString());
Expand Down Expand Up @@ -79,11 +80,11 @@ public void updateAdminPassword(Universe universe, DatabaseSecurityFormData data
// --use_cassandra_authentication=true
// This is always true if the universe was created via cloud.
RunQueryFormData ycqlQuery = new RunQueryFormData();
ycqlQuery.query =
ycqlQuery.setQuery(
String.format(
"ALTER ROLE '%s' WITH PASSWORD='%s'",
Util.escapeSingleQuotesOnly(data.ycqlAdminUsername),
Util.escapeSingleQuotesOnly(data.ycqlAdminPassword));
Util.escapeSingleQuotesOnly(data.ycqlAdminPassword)));
JsonNode ycqlResponse =
executeQuery(universe, ycqlQuery, true, data.ycqlAdminUsername, data.ycqlCurrAdminPassword);
LOG.info("Updating YCQL user, result: " + ycqlResponse.toString());
Expand Down Expand Up @@ -181,18 +182,18 @@ public JsonNode executeQuery(
}

try {
ResultSet rs = cc.session.execute(queryParams.query);
ResultSet rs = cc.session.execute(queryParams.getQuery());
if (rs.iterator().hasNext()) {
List<Map<String, Object>> rows = resultSetToMap(rs);
response.set("result", toJson(rows));
} else {
// For commands without a result we return only executed command identifier
// (SELECT/UPDATE/...). We can't return query itself to avoid logging of
// sensitive data.
response.put("queryType", getQueryType(queryParams.query));
response.put("queryType", getQueryType(queryParams.getQuery()));
}
} catch (Exception e) {
response.put("error", removeQueryFromErrorMessage(e.getMessage(), queryParams.query));
response.put("error", removeQueryFromErrorMessage(e.getMessage(), queryParams.getQuery()));
} finally {
if (cc != null) {
cc.close();
Expand Down
31 changes: 13 additions & 18 deletions managed/src/main/java/com/yugabyte/yw/common/YsqlQueryExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@
@Singleton
public class YsqlQueryExecutor {
private static final Logger LOG = LoggerFactory.getLogger(YsqlQueryExecutor.class);
private static final String DEFAULT_DB_USER = Util.DEFAULT_YSQL_USERNAME;
private static final String DEFAULT_DB_PASSWORD = Util.DEFAULT_YSQL_PASSWORD;
private static final String DB_ADMIN_ROLE_NAME = Util.DEFAULT_YSQL_ADMIN_ROLE_NAME;
private static final String PRECREATED_DB_ADMIN = "yb_db_admin";

Expand Down Expand Up @@ -136,17 +134,14 @@ private List<Map<String, Object>> resultSetToMap(ResultSet result) throws SQLExc
return rows;
}

public JsonNode executeQuery(Universe universe, RunQueryFormData queryParams) {
return executeQuery(universe, queryParams, DEFAULT_DB_USER, DEFAULT_DB_PASSWORD);
}

public JsonNode executeQuery(
Universe universe, RunQueryFormData queryParams, String username, String password) {
ObjectNode response = newObject();

String ysqlEndpoints = universe.getYSQLServerAddresses();
String connectString =
String.format("jdbc:postgresql://%s/%s", ysqlEndpoints.split(",")[0], queryParams.db_name);
String.format(
"jdbc:postgresql://%s/%s", ysqlEndpoints.split(",")[0], queryParams.getDbName());
Properties props = new Properties();
props.put("user", username);
props.put("password", password);
Expand All @@ -162,20 +157,20 @@ public JsonNode executeQuery(
if (conn == null) {
response.put("error", "Unable to connect to DB");
} else {
PreparedStatement p = conn.prepareStatement(queryParams.query);
PreparedStatement p = conn.prepareStatement(queryParams.getQuery());
boolean hasResult = p.execute();
if (hasResult) {
ResultSet result = p.getResultSet();
List<Map<String, Object>> rows = resultSetToMap(result);
response.set("result", toJson(rows));
} else {
response
.put("queryType", getQueryType(queryParams.query))
.put("queryType", getQueryType(queryParams.getQuery()))
.put("count", p.getUpdateCount());
}
}
} catch (SQLException | RuntimeException e) {
response.put("error", removeQueryFromErrorMessage(e.getMessage(), queryParams.query));
response.put("error", removeQueryFromErrorMessage(e.getMessage(), queryParams.getQuery()));
}
return response;
}
Expand Down Expand Up @@ -206,7 +201,7 @@ public JsonNode executeQueryInNodeShell(
universe,
queryParams,
node,
runtimeConfigFactory.forUniverse(universe).getLong("yb.ysql_timeout_secs"),
timeoutSec,
universe.getUniverseDetails().getPrimaryCluster().userIntent.isYSQLAuthEnabled());
}

Expand All @@ -218,15 +213,15 @@ public JsonNode executeQueryInNodeShell(
boolean authEnabled) {
ObjectNode response = newObject();
response.put("type", "ysql");
String queryType = getQueryType(queryParams.query);
String queryType = getQueryType(queryParams.getQuery());
String queryString =
queryType.equals("SELECT") ? wrapJsonAgg(queryParams.query) : queryParams.query;
queryType.equals("SELECT") ? wrapJsonAgg(queryParams.getQuery()) : queryParams.getQuery();
ShellResponse shellResponse = new ShellResponse();
try {
shellResponse =
nodeUniverseManager
.runYsqlCommand(
node, universe, queryParams.db_name, queryString, timeoutSec, authEnabled)
node, universe, queryParams.getDbName(), queryString, timeoutSec, authEnabled)
.processErrors("Ysql Query Execution Error");
} catch (RuntimeException e) {
response.put("error", ShellResponse.cleanedUpErrorMessage(e.getMessage()));
Expand Down Expand Up @@ -431,8 +426,8 @@ public JsonNode runUserDbCommands(String query, String dbName, Universe universe
Http.Status.INTERNAL_SERVER_ERROR, "DB not ready to create a user");
}
RunQueryFormData ysqlQuery = new RunQueryFormData();
ysqlQuery.query = query;
ysqlQuery.db_name = dbName;
ysqlQuery.setQuery(query);
ysqlQuery.setDbName(dbName);

JsonNode ysqlResponse = executeQueryInNodeShell(universe, ysqlQuery, nodeToUse);
if (ysqlResponse.has("error")) {
Expand All @@ -445,8 +440,8 @@ public JsonNode runUserDbCommands(String query, String dbName, Universe universe

public void validateAdminPassword(Universe universe, DatabaseSecurityFormData data) {
RunQueryFormData ysqlQuery = new RunQueryFormData();
ysqlQuery.db_name = data.dbName;
ysqlQuery.query = "SELECT 1";
ysqlQuery.setDbName(data.dbName);
ysqlQuery.setQuery("SELECT 1");
JsonNode ysqlResponse =
executeQuery(universe, ysqlQuery, data.ysqlAdminUsername, data.ysqlAdminPassword);
if (ysqlResponse.has("error")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -987,4 +987,13 @@ public class UniverseConfKeys extends RuntimeConfigKeysModule {
+ " This skips the storage config/success marker based validations done before B/R.",
ConfDataType.BooleanType,
ImmutableList.of(ConfKeyTags.INTERNAL));

public static final ConfKeyInfo<Boolean> enableDbQueryApi =
new ConfKeyInfo<>(
"yb.security.enable_db_query_api",
ScopeType.UNIVERSE,
"Enable .../run_query API for the universe",
"Enables the ability to execute SQL queries through YBA API",
ConfDataType.BooleanType,
ImmutableList.of(ConfKeyTags.INTERNAL));
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Authorization;
import java.util.UUID;
import play.data.Form;
import play.mvc.Http;
import play.mvc.Result;

Expand Down Expand Up @@ -225,11 +224,10 @@ public Result createUserInDB(UUID customerUUID, UUID universeUUID, Http.Request
public Result runQuery(UUID customerUUID, UUID universeUUID, Http.Request request) {
Customer customer = Customer.getOrBadRequest(customerUUID);
Universe universe = Universe.getOrBadRequest(universeUUID, customer);
Form<RunQueryFormData> formData =
formFactory.getFormDataOrBadRequest(request, RunQueryFormData.class);
RunQueryFormData formData = parseJsonAndValidate(request, RunQueryFormData.class);

JsonNode queryResult =
universeYbDbAdminHandler.validateRequestAndExecuteQuery(universe, formData.get(), request);
universeYbDbAdminHandler.validateRequestAndExecuteQuery(universe, formData);
auditService()
.createAuditEntryWithReqBody(
request,
Expand Down

0 comments on commit 0eedfbc

Please sign in to comment.