github-advanced-security[bot] commented on code in PR #20586: URL: https://github.com/apache/druid/pull/20586#discussion_r4270612803
########## sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemQueryContextParametersTable.java: ########## @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.druid.sql.calcite.schema; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.calcite.DataContext; +import org.apache.calcite.linq4j.Enumerable; +import org.apache.calcite.linq4j.Linq4j; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.schema.ScannableTable; +import org.apache.calcite.schema.Schema; +import org.apache.calcite.schema.impl.AbstractTable; +import org.apache.druid.error.DruidException; +import org.apache.druid.java.util.common.HumanReadableBytes; +import org.apache.druid.query.BadQueryContextException; +import org.apache.druid.query.QueryConfigProvider; +import org.apache.druid.query.context.QueryContextParameter; +import org.apache.druid.query.context.QueryContextParameters; +import org.apache.druid.query.context.constraint.ParameterConstraint; +import org.apache.druid.query.context.constraint.Range; +import org.apache.druid.query.context.docs.ParameterDocumentation; +import org.apache.druid.segment.column.ColumnType; +import org.apache.druid.segment.column.RowSignature; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; +import org.apache.druid.sql.calcite.table.RowSignatures; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * System table {@code sys.query_context_parameters}, listing the query context parameters declared in + * {@link QueryContextParameters#ALL} that users can set, together with their type, default, applicability and + * documentation. {@link QueryContextParameter.Visibility#SYSTEM} parameters, which only Druid sets, are excluded. + */ +public class SystemQueryContextParametersTable extends AbstractTable implements ScannableTable +{ + public static final String TABLE_NAME = "query_context_parameters"; + + static final RowSignature ROW_SIGNATURE = RowSignature + .builder() + .add("name", ColumnType.STRING) + .add("type", ColumnType.STRING) + .add("range", ColumnType.STRING) + .add("default_value", ColumnType.STRING) + .add("effective_value", ColumnType.STRING) + .add("effective_source", ColumnType.STRING) + .add("default_description", ColumnType.STRING) + .add("description", ColumnType.STRING) + .add("visibility", ColumnType.STRING) + .add("query_languages", ColumnType.STRING_ARRAY) + .add("engines", ColumnType.STRING_ARRAY) + .add("native_query_types", ColumnType.STRING_ARRAY) + .add("statement_types", ColumnType.STRING_ARRAY) + .add("since", ColumnType.STRING) + .add("is_nullable", ColumnType.LONG) + .add("deprecation_message", ColumnType.STRING) + .build(); + + /** + * Where the effective default of a parameter comes from, in increasing precedence + */ + enum EffectiveSource + { + /** The default declared with the parameter */ + BUILT_IN, + /** A {@code druid.query.default.context.<name>} runtime property */ + RUNTIME_PROPERTY, + /** The {@code queryContext} of the Broker dynamic configuration */ + BROKER_DYNAMIC_CONFIG, + /** The user may not read the Broker configuration, so the effective default is not shown */ + UNAUTHORIZED + } + + private static final Resource CONFIG_RESOURCE = new Resource("CONFIG", ResourceType.CONFIG); + + private final ObjectMapper jsonMapper; + private final QueryConfigProvider queryConfigProvider; + private final AuthorizerMapper authorizerMapper; + private final AuthenticationResult authenticationResult; + + public SystemQueryContextParametersTable( + final ObjectMapper jsonMapper, + final QueryConfigProvider queryConfigProvider, + final AuthorizerMapper authorizerMapper, + final AuthenticationResult authenticationResult + ) + { + this.jsonMapper = jsonMapper; + this.queryConfigProvider = queryConfigProvider; + this.authorizerMapper = authorizerMapper; + this.authenticationResult = authenticationResult; + } + + @Override + public RelDataType getRowType(final RelDataTypeFactory typeFactory) + { + return RowSignatures.toRelDataType(ROW_SIGNATURE, typeFactory); + } + + @Override + public Schema.TableType getJdbcTableType() + { + return Schema.TableType.SYSTEM_TABLE; + } + + @Override + public Enumerable<Object[]> scan(final DataContext root) + { + final boolean canReadConfig = canReadConfig(); + + // Read once per scan, so that a Broker dynamic configuration change shows up on the next query + final QueryConfigProvider.ContextWithOverrides contextWithOverrides = queryConfigProvider.getContextWithOverrides(); + final Map<String, Object> effectiveContext = contextWithOverrides.context(); + final Map<String, Object> dynamicContext = contextWithOverrides.dynamicOverrides(); + final List<Object[]> rows = new ArrayList<>(); + for (final QueryContextParameter<?> parameter : QueryContextParameters.ALL.get().values()) { + if (parameter.getVisibility() != QueryContextParameter.Visibility.SYSTEM) { + rows.add(toRow(parameter, canReadConfig, effectiveContext, dynamicContext)); + } + } + return Linq4j.asEnumerable(rows); + } + + /** + * The parameter catalog is public, but the effective defaults come from the Broker configuration, so showing them + * requires the same permission as reading that configuration through the API. + */ + private boolean canReadConfig() + { + return AuthorizationUtils.authorizeAllResourceActions( + authenticationResult, + Collections.singletonList(new ResourceAction(CONFIG_RESOURCE, Action.READ)), + authorizerMapper + ).allowAccessWithNoRestriction(); + } + + /** + * @param canReadConfig whether the user may see the effective default, see {@link #canReadConfig()} + * @param effectiveContext the default query context that queries get, the runtime properties merged with the + * Broker dynamic configuration + * @param dynamicContext the Broker dynamic configuration part of it, to tell where a value comes from + */ + private Object[] toRow( + final QueryContextParameter<?> parameter, + final boolean canReadConfig, + final Map<String, Object> effectiveContext, + final Map<String, Object> dynamicContext + ) + { + final Object contextValue = effectiveContext.get(parameter.getName()); + final Object builtInValue = parameter.getDefaultValue().orElse(null); + final Object effectiveValue; + final EffectiveSource effectiveSource; + if (!canReadConfig) { + // Same for every row, so that the user can't tell which parameters the operator has configured + effectiveValue = null; + effectiveSource = EffectiveSource.UNAUTHORIZED; + } else if (contextValue != null) { + effectiveValue = parseOrRaw(parameter, contextValue); + effectiveSource = dynamicContext.get(parameter.getName()) != null + ? EffectiveSource.BROKER_DYNAMIC_CONFIG + : EffectiveSource.RUNTIME_PROPERTY; + } else if (builtInValue != null) { + effectiveValue = builtInValue; + effectiveSource = EffectiveSource.BUILT_IN; + } else { + effectiveValue = null; + effectiveSource = null; + } + + final ParameterDocumentation docs = parameter.getDocumentation(); + final Class<?> valueType = parameter.getValueType(); + return new Object[]{ + parameter.getName(), + typeName(valueType), + range(parameter), + builtInValue == null ? null : toJsonForm(builtInValue), + effectiveValue == null ? null : toJsonForm(effectiveValue), + effectiveSource == null ? null : effectiveSource.name(), + docs.getDefaultDescription().orElse(null), + docs.getDescription(), + parameter.getVisibility().name(), + names(docs.getQueries()), + names(docs.getEngines()), + docs.getEngines().contains(ParameterDocumentation.Engine.NATIVE) ? values(docs.getQueryTypes()) : null, + docs.getQueries().contains(ParameterDocumentation.Query.SQL) ? names(docs.getStatementTypes()) : null, + parameter.getSince(), + parameter.isNullable() ? 1L : 0L, + parameter.getDeprecationMessage() + }; + } + + private static String typeName(final Class<?> valueType) + { + if (valueType.isEnum()) { + return "ENUM"; + } else if (Boolean.class.equals(valueType)) { + return "BOOLEAN"; + } else if (Integer.class.equals(valueType)) { + return "INTEGER"; + } else if (Long.class.equals(valueType)) { + return "LONG"; + } else if (Float.class.equals(valueType)) { + return "FLOAT"; + } else if (String.class.equals(valueType)) { + return "STRING"; + } else if (HumanReadableBytes.class.equals(valueType)) { + return "HUMAN_READABLE_NUMBER"; + } else { + return "OBJECT"; + } + } + + @Nullable + private static String range(final QueryContextParameter<?> parameter) + { + for (final ParameterConstraint<?> constraint : parameter.getConstraints()) { + if (constraint instanceof Range.Constraint<?> range) { + return "[" + range.getLowerBound() + ", " + range.getUpperBound() + "]"; + } + } + return null; + } + + /** + * Returns a configured value as the parameter reads it, so that it is shown in the same form as + * {@code default_value}. A value the parameter rejects is shown as configured. + */ + private static Object parseOrRaw(final QueryContextParameter<?> parameter, final Object value) + { + try { + return parameter.parse(value); + } + catch (BadQueryContextException e) { + return value; + } + } + + private static List<String> names(final Collection<? extends Enum<?>> values) + { + return values.stream().sorted().map(Enum::name).collect(Collectors.toCollection(SortableStringList::new)); + } + + private static List<String> values(final Collection<? extends Enum<?>> values) + { + return values.stream().sorted().map(Enum::toString).collect(Collectors.toCollection(SortableStringList::new)); + } + + /** + * Value of an {@code ARRAY<VARCHAR>} column. The Bindable sort casts column values to {@link Comparable}, so array + * values must be comparable for {@code ORDER BY} on these columns to work. Arrays compare element by element, and a + * shorter array that is a prefix of a longer one sorts first. + */ + static final class SortableStringList extends ArrayList<String> implements Comparable<SortableStringList> Review Comment: ## CodeQL / Inconsistent compareTo This class declares [compareTo](1) but inherits equals; the two could be inconsistent. [Show more details](https://github.com/apache/druid/security/code-scanning/12082) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
