-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Redact sensitive information in catalog queries #24563
base: master
Are you sure you want to change the base?
Changes from all commits
1c360fd
8018b45
8b7148d
042afdf
ed595a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,7 @@ | |
|
||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import java.util.concurrent.ConcurrentMap; | ||
|
||
|
@@ -144,6 +145,18 @@ public CatalogConnector createCatalog(CatalogHandle catalogHandle, ConnectorName | |
return createCatalog(catalogHandle, connectorName, connector, Optional.empty()); | ||
} | ||
|
||
@Override | ||
public Set<String> getRedactablePropertyNames(ConnectorName connectorName, Set<String> propertyNames) | ||
{ | ||
ConnectorFactory connectorFactory = connectorFactories.get(connectorName); | ||
if (connectorFactory == null) { | ||
// If someone tries to use a non-existent connector, we assume they | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great catch, I didn't think of this case. |
||
// misspelled the name and, for safety, we redact all the properties. | ||
return propertyNames; | ||
} | ||
return connectorFactory.getRedactablePropertyNames(propertyNames); | ||
} | ||
|
||
private CatalogConnector createCatalog(CatalogHandle catalogHandle, ConnectorName connectorName, Connector connector, Optional<CatalogProperties> catalogProperties) | ||
{ | ||
Tracer tracer = createTracer(catalogHandle); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ | |
import io.trino.spi.TrinoException; | ||
import io.trino.spi.resourcegroups.SelectionContext; | ||
import io.trino.spi.resourcegroups.SelectionCriteria; | ||
import io.trino.sql.SensitiveStatementRedactor; | ||
import jakarta.annotation.PostConstruct; | ||
import jakarta.annotation.PreDestroy; | ||
import org.weakref.jmx.Flatten; | ||
|
@@ -84,6 +85,7 @@ public class DispatchManager | |
private final SessionPropertyDefaults sessionPropertyDefaults; | ||
private final SessionPropertyManager sessionPropertyManager; | ||
private final Tracer tracer; | ||
private final SensitiveStatementRedactor sensitiveStatementRedactor; | ||
|
||
private final int maxQueryLength; | ||
|
||
|
@@ -107,6 +109,7 @@ public DispatchManager( | |
SessionPropertyDefaults sessionPropertyDefaults, | ||
SessionPropertyManager sessionPropertyManager, | ||
Tracer tracer, | ||
SensitiveStatementRedactor sensitiveStatementRedactor, | ||
QueryManagerConfig queryManagerConfig, | ||
DispatchExecutor dispatchExecutor, | ||
QueryMonitor queryMonitor) | ||
|
@@ -121,6 +124,7 @@ public DispatchManager( | |
this.sessionPropertyDefaults = requireNonNull(sessionPropertyDefaults, "sessionPropertyDefaults is null"); | ||
this.sessionPropertyManager = sessionPropertyManager; | ||
this.tracer = requireNonNull(tracer, "tracer is null"); | ||
this.sensitiveStatementRedactor = requireNonNull(sensitiveStatementRedactor, "sensitiveStatementRedactor is null"); | ||
|
||
this.maxQueryLength = queryManagerConfig.getMaxQueryLength(); | ||
|
||
|
@@ -207,6 +211,7 @@ private <C> void createQueryInternal(QueryId queryId, Span querySpan, Slug slug, | |
{ | ||
Session session = null; | ||
PreparedQuery preparedQuery = null; | ||
String redactedQuery = null; | ||
try { | ||
if (query.length() > maxQueryLength) { | ||
int queryLength = query.length(); | ||
|
@@ -223,6 +228,9 @@ private <C> void createQueryInternal(QueryId queryId, Span querySpan, Slug slug, | |
// prepare query | ||
preparedQuery = queryPreparer.prepareQuery(session, query); | ||
|
||
// redact security-sensitive information that query may contain | ||
redactedQuery = sensitiveStatementRedactor.redact(query, preparedQuery.getStatement()); | ||
|
||
// select resource group | ||
Optional<String> queryType = getQueryType(preparedQuery.getStatement()).map(Enum::name); | ||
SelectionContext<C> selectionContext = resourceGroupManager.selectGroup(new SelectionCriteria( | ||
|
@@ -240,7 +248,7 @@ private <C> void createQueryInternal(QueryId queryId, Span querySpan, Slug slug, | |
DispatchQuery dispatchQuery = dispatchQueryFactory.createDispatchQuery( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this automatically also handles things like event listener and Might be worth to explicitly call it out in the commit message (although you do imply that by mentioning anything using QueryInfo/BasicQueryInfo). |
||
session, | ||
sessionContext.getTransactionId(), | ||
query, | ||
redactedQuery, | ||
preparedQuery, | ||
slug, | ||
selectionContext.getResourceGroupId()); | ||
|
@@ -266,8 +274,16 @@ private <C> void createQueryInternal(QueryId queryId, Span querySpan, Slug slug, | |
.setSource(sessionContext.getSource().orElse(null)) | ||
.build(); | ||
} | ||
if (redactedQuery == null) { | ||
redactedQuery = sensitiveStatementRedactor.redact(query); | ||
} | ||
Optional<String> preparedSql = Optional.ofNullable(preparedQuery).flatMap(PreparedQuery::getPrepareSql); | ||
DispatchQuery failedDispatchQuery = failedDispatchQueryFactory.createFailedDispatchQuery(session, query, preparedSql, Optional.empty(), throwable); | ||
DispatchQuery failedDispatchQuery = failedDispatchQueryFactory.createFailedDispatchQuery( | ||
session, | ||
redactedQuery, | ||
preparedSql, | ||
Optional.empty(), | ||
throwable); | ||
queryCreated(failedDispatchQuery); | ||
// maintain proper order of calls such that EventListener has access to QueryInfo | ||
// - add query to tracker | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package io.trino.sql; | ||
|
||
import com.google.inject.Inject; | ||
import io.trino.FeaturesConfig; | ||
import io.trino.connector.CatalogFactory; | ||
import io.trino.spi.connector.ConnectorName; | ||
import io.trino.sql.tree.AstVisitor; | ||
import io.trino.sql.tree.CreateCatalog; | ||
import io.trino.sql.tree.Explain; | ||
import io.trino.sql.tree.ExplainAnalyze; | ||
import io.trino.sql.tree.Identifier; | ||
import io.trino.sql.tree.Node; | ||
import io.trino.sql.tree.Property; | ||
import io.trino.sql.tree.Statement; | ||
import io.trino.sql.tree.StringLiteral; | ||
|
||
import java.util.List; | ||
import java.util.Set; | ||
|
||
import static com.google.common.collect.ImmutableList.toImmutableList; | ||
import static com.google.common.collect.ImmutableSet.toImmutableSet; | ||
|
||
public class SensitiveStatementRedactor | ||
{ | ||
public static final String REDACTED_VALUE = "***"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should consider a better value here than just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Can you expand on the function idea? Is that to make it so that the output of |
||
|
||
private final boolean enabled; | ||
private final CatalogFactory catalogFactory; | ||
|
||
@Inject | ||
public SensitiveStatementRedactor(FeaturesConfig config, CatalogFactory catalogFactory) | ||
{ | ||
this.enabled = config.isStatementRedactingEnabled(); | ||
this.catalogFactory = catalogFactory; | ||
} | ||
|
||
public String redact(String rawQuery, Statement statement) | ||
{ | ||
if (enabled) { | ||
RedactingVisitor visitor = new RedactingVisitor(); | ||
Node redactedStatement = visitor.process(statement); | ||
if (visitor.isRedacted()) { | ||
return SqlFormatter.formatSql(redactedStatement); | ||
} | ||
} | ||
return rawQuery; | ||
} | ||
|
||
public String redact(String rawQuery) | ||
{ | ||
if (enabled) { | ||
return REDACTED_VALUE; | ||
} | ||
return rawQuery; | ||
} | ||
|
||
private class RedactingVisitor | ||
extends AstVisitor<Node, Void> | ||
{ | ||
private boolean redacted; | ||
|
||
public boolean isRedacted() | ||
{ | ||
return redacted; | ||
} | ||
|
||
@Override | ||
protected Node visitNode(Node node, Void context) | ||
{ | ||
return node; | ||
} | ||
|
||
@Override | ||
protected Node visitExplain(Explain explain, Void context) | ||
{ | ||
Statement statement = (Statement) process(explain.getStatement()); | ||
return new Explain(explain.getLocation().orElseThrow(), statement, explain.getOptions()); | ||
} | ||
|
||
@Override | ||
protected Node visitExplainAnalyze(ExplainAnalyze explainAnalyze, Void context) | ||
{ | ||
Statement statement = (Statement) process(explainAnalyze.getStatement()); | ||
return new ExplainAnalyze(explainAnalyze.getLocation().orElseThrow(), statement, explainAnalyze.isVerbose()); | ||
} | ||
|
||
@Override | ||
protected Node visitCreateCatalog(CreateCatalog createCatalog, Void context) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there some way to notice when we need to add new node visitors here? Should this be a "wrapper" like the various WDYT? Might be overkill for now so need to change anything - just to have a discussion. |
||
{ | ||
ConnectorName connectorName = new ConnectorName(createCatalog.getConnectorName().getValue()); | ||
List<Property> redactedProperties = redact(connectorName, createCatalog.getProperties()); | ||
return createCatalog.withProperties(redactedProperties); | ||
} | ||
|
||
private List<Property> redact(ConnectorName connectorName, List<Property> properties) | ||
{ | ||
redacted = true; | ||
Set<String> propertyNames = properties.stream() | ||
.map(Property::getName) | ||
.map(Identifier::getValue) | ||
.collect(toImmutableSet()); | ||
Set<String> redactableProperties = catalogFactory.getRedactablePropertyNames(connectorName, propertyNames); | ||
return redactProperties(properties, redactableProperties); | ||
} | ||
|
||
private List<Property> redactProperties(List<Property> properties, Set<String> redactableProperties) | ||
{ | ||
return properties.stream() | ||
.map(property -> { | ||
if (redactableProperties.contains(property.getName().getValue())) { | ||
return new Property(property.getName(), new StringLiteral(REDACTED_VALUE)); | ||
} | ||
return property; | ||
}) | ||
.collect(toImmutableList()); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mosabua for suggestions about config naming. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we want an option to disable this. Maybe as a temporary kill switch, but we should remove this as soon as we are happy with this feature
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, we can prefix with
experimental.
in that case like we have done in past to clarify this. Or maybedeprecated.
from the beginning.