This is an automated email from the ASF dual-hosted git repository.
saihemanth-cloudera pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new 45049202df3 HIVE-29671: Harden Schema path authorization for avro
serde (#6549)
45049202df3 is described below
commit 45049202df35cea382616624de3fe8d252aa2d00
Author: Sai Hemanth Gantasala
<[email protected]>
AuthorDate: Thu Jul 9 09:42:17 2026 -0700
HIVE-29671: Harden Schema path authorization for avro serde (#6549)
* HIVE-29671: Harden Schema path authorization for avro serde
* Address test failures
* Address test failures & one Review comment
* Address co-pilot concerns
* Address review comments
* Address review comments - Round 2
---
.../java/org/apache/hadoop/hive/conf/HiveConf.java | 11 ++
.../org/apache/hadoop/hive/ql/plan/PlanUtils.java | 2 +
.../security/authorization/AuthorizationUtils.java | 91 +++++++++++++-
.../authorization/command/CommandAuthorizerV2.java | 8 ++
.../plugin/metastore/events/AlterTableEvent.java | 20 +++
.../plugin/metastore/events/CreateTableEvent.java | 22 +++-
.../TestAvroSchemaUrlAuthorizationUtils.java | 139 +++++++++++++++++++++
.../metastore/TestHiveMetaStoreAuthorizer.java | 77 +++++++++++-
.../llap/avro_extschema_insert.q.out | 4 +
.../llap/avro_tableproperty_optimize.q.out | 2 +
.../results/clientpositive/llap/avrotblsjoin.q.out | 2 +
.../clientpositive/llap/compustat_avro.q.out | 2 +
.../hadoop/hive/serde2/avro/AvroSerdeUtils.java | 138 +++++++++++++++++++-
.../hive/serde2/avro/TestAvroSerdeUtils.java | 101 ++++++++++++++-
14 files changed, 600 insertions(+), 19 deletions(-)
diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
index 0ecf541a789..b10509b85f1 100644
--- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
+++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java
@@ -2191,6 +2191,17 @@ public static enum ConfVars {
"Whether to use former Java date/time APIs to convert between
timezones when writing timestamps in " +
"Avro files. Once data are written to the file the effect is permanent
(also reflected in the metadata)." +
"Changing the value of this property affects only new data written to
the file."),
+
HIVE_AVRO_SCHEMA_URL_ALLOWED_SCHEMES("hive.avro.schema.url.allowed.schemes",
+ "hdfs,s3,s3a,s3n,abfs,abfss,gs,wasb,wasbs,viewfs,o3fs,ofs",
+ "Comma-separated list of URI schemes permitted for avro.schema.url
when loading schemas from a remote " +
+ "location. HTTP/HTTPS and file:// are never allowed via this
setting. A URI with no scheme is " +
+ "resolved against the default filesystem."),
+
HIVE_AVRO_SCHEMA_URL_REMOTE_HTTP_ENABLED("hive.avro.schema.url.remote.http.enabled",
false,
+ "Whether to allow avro.schema.url values that use http or https. When
enabled, the host must also appear " +
+ "in hive.avro.schema.url.http.allowed.hosts. Disabled by default
to prevent server-side request forgery."),
+
HIVE_AVRO_SCHEMA_URL_HTTP_ALLOWED_HOSTS("hive.avro.schema.url.http.allowed.hosts",
"",
+ "Comma-separated list of hosts permitted for avro.schema.url when
hive.avro.schema.url.remote.http.enabled " +
+ "is true. HTTP/HTTPS schema fetch is rejected when this list is
empty."),
HIVE_INT_TIMESTAMP_CONVERSION_IN_SECONDS("hive.int.timestamp.conversion.in.seconds",
false,
"Boolean/tinyint/smallint/int/bigint value is interpreted as
milliseconds during the timestamp conversion.\n" +
"Set this flag to true to interpret the value as seconds to be
consistent with float/double." ),
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/plan/PlanUtils.java
b/ql/src/java/org/apache/hadoop/hive/ql/plan/PlanUtils.java
index fa45fbbef70..9c94ed457f6 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/plan/PlanUtils.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/plan/PlanUtils.java
@@ -65,6 +65,7 @@
import org.apache.hadoop.hive.ql.metadata.Partition;
import org.apache.hadoop.hive.ql.parse.SemanticAnalyzer;
import org.apache.hadoop.hive.ql.parse.SemanticException;
+import org.apache.hadoop.hive.ql.security.authorization.AuthorizationUtils;
import org.apache.hadoop.hive.ql.parse.type.ExprNodeTypeCheck;
import org.apache.hadoop.hive.ql.session.SessionState;
import org.apache.hadoop.hive.ql.util.NullOrdering;
@@ -1002,6 +1003,7 @@ public static ReadEntity addInput(Set<ReadEntity> inputs,
ReadEntity newInput, b
assert false;
} else {
inputs.add(newInput);
+ AuthorizationUtils.addAvroSchemaUrlInputForReadEntity(inputs, newInput);
return newInput;
}
// make compile happy
diff --git
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/AuthorizationUtils.java
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/AuthorizationUtils.java
index fe0d2de242a..c92083b0b58 100644
---
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/AuthorizationUtils.java
+++
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/AuthorizationUtils.java
@@ -17,11 +17,17 @@
*/
package org.apache.hadoop.hive.ql.security.authorization;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.classification.InterfaceAudience.LimitedPrivate;
+import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.metastore.api.HiveObjectPrivilege;
import org.apache.hadoop.hive.metastore.api.HiveObjectRef;
import org.apache.hadoop.hive.metastore.api.HiveObjectType;
@@ -31,12 +37,14 @@
import org.apache.hadoop.hive.ql.ddl.privilege.PrincipalDesc;
import org.apache.hadoop.hive.ql.ddl.privilege.PrivilegeDesc;
import org.apache.hadoop.hive.ql.ddl.privilege.PrivilegeObjectDesc;
-import org.apache.hadoop.hive.ql.exec.Utilities;
import org.apache.hadoop.hive.ql.hooks.Entity;
import org.apache.hadoop.hive.ql.hooks.Entity.Type;
+import org.apache.hadoop.hive.ql.hooks.ReadEntity;
import org.apache.hadoop.hive.ql.hooks.WriteEntity;
import org.apache.hadoop.hive.ql.hooks.WriteEntity.WriteType;
import org.apache.hadoop.hive.ql.metadata.HiveException;
+import org.apache.hadoop.hive.ql.metadata.Table;
+import org.apache.hadoop.hive.ql.plan.PlanUtils;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAuthorizationTranslator;
import org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrincipal;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrincipal.HivePrincipalType;
@@ -46,6 +54,9 @@
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject.HivePrivObjectActionType;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject.HivePrivilegeObjectType;
import org.apache.hadoop.hive.ql.session.SessionState;
+import org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils;
+import org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.AvroTableProperties;
+import org.apache.hadoop.hive.serde2.avro.AvroSerDe;
/**
* Utility code shared by hive internal code and sql standard authorization
plugin implementation
@@ -307,4 +318,82 @@ public static HivePrivObjectActionType
getActionType(Entity privObject) {
return actionType;
}
+ /**
+ * When a table or partition is read, add a DFS ReadEntity for its
avro.schema.url if the
+ * schema would be fetched from a filesystem location at query time.
+ */
+ public static void addAvroSchemaUrlInputForReadEntity(Collection<ReadEntity>
inputs,
+ ReadEntity readEntity) {
+ if (readEntity == null || !readEntity.isDirect() ||
readEntity.isUpdateOrDelete()) {
+ return;
+ }
+ switch (readEntity.getTyp()) {
+ case TABLE:
+ if (readEntity.getTable() != null) {
+ addAvroSchemaUrlInputIfNeeded(inputs, readEntity.getTable());
+ }
+ break;
+ case PARTITION:
+ case DUMMYPARTITION:
+ if (readEntity.getPartition() != null) {
+ addAvroSchemaUrlInputIfNeeded(inputs,
readEntity.getPartition().getTable());
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ public static void addAvroSchemaUrlInputIfNeeded(Collection<ReadEntity>
inputs, Table table) {
+ String schemaUrl = getFilesystemAvroSchemaUrlToAuthorize(table);
+ if (schemaUrl == null) {
+ return;
+ }
+ ReadEntity schemaUrlInput = toAvroSchemaUrlReadEntity(schemaUrl);
+ if (inputs instanceof Set) {
+ PlanUtils.addInput((Set<ReadEntity>) inputs, schemaUrlInput);
+ } else if (!inputs.contains(schemaUrlInput)) {
+ inputs.add(schemaUrlInput);
+ }
+ }
+
+ private static ReadEntity toAvroSchemaUrlReadEntity(String schemaUrl) {
+ Path path = new Path(schemaUrl);
+ return new ReadEntity(path, isLocalFilesystemSchemaUrl(schemaUrl));
+ }
+
+ private static boolean isLocalFilesystemSchemaUrl(String schemaUrl) {
+ try {
+ String scheme = new URI(schemaUrl).getScheme();
+ return scheme != null && "file".equalsIgnoreCase(scheme);
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the avro.schema.url when it refers to a filesystem location that
will be read to
+ * resolve the schema, or null when no schema-url authorization is required.
+ */
+ public static String getFilesystemAvroSchemaUrlToAuthorize(Table table) {
+ if (table == null || table.isTemporary()) {
+ return null;
+ }
+ if (!AvroSerDe.class.getName().equals(table.getSerializationLib())) {
+ return null;
+ }
+ String literal =
table.getProperty(AvroTableProperties.SCHEMA_LITERAL.getPropName());
+ if (StringUtils.isNotEmpty(literal) &&
!AvroSerdeUtils.SCHEMA_NONE.equals(literal)) {
+ return null;
+ }
+ String schemaUrl =
table.getProperty(AvroTableProperties.SCHEMA_URL.getPropName());
+ if (StringUtils.isEmpty(schemaUrl) ||
AvroSerdeUtils.SCHEMA_NONE.equals(schemaUrl)) {
+ return null;
+ }
+ if (!AvroSerdeUtils.isFilesystemSchemaUrl(schemaUrl)) {
+ return null;
+ }
+ return schemaUrl;
+ }
+
}
diff --git
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/command/CommandAuthorizerV2.java
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/command/CommandAuthorizerV2.java
index 665a23a1fde..c6e416dbc28 100644
---
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/command/CommandAuthorizerV2.java
+++
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/command/CommandAuthorizerV2.java
@@ -72,6 +72,7 @@ static void doAuthorization(HiveOperation op,
BaseSemanticAnalyzer sem, SessionS
List<ReadEntity> inputList = new ArrayList<ReadEntity>(inputs);
List<WriteEntity> outputList = new ArrayList<WriteEntity>(outputs);
addPermanentFunctionEntities(ss, inputList);
+ enrichAvroSchemaUrlInputs(inputList);
List<HivePrivilegeObject> inputsHObjs = getHivePrivObjects(inputList,
selectTab2Cols, hiveOpType, sem);
List<HivePrivilegeObject> outputHObjs = getHivePrivObjects(outputList,
updateTab2Cols, hiveOpType, sem);
@@ -84,6 +85,13 @@ static void doAuthorization(HiveOperation op,
BaseSemanticAnalyzer sem, SessionS
ss.getAuthorizerV2().checkPrivileges(hiveOpType, inputsHObjs, outputHObjs,
authzContextBuilder.build());
}
+ private static void enrichAvroSchemaUrlInputs(List<ReadEntity> inputList) {
+ List<ReadEntity> snapshot = new ArrayList<>(inputList);
+ for (ReadEntity readEntity : snapshot) {
+ AuthorizationUtils.addAvroSchemaUrlInputForReadEntity(inputList,
readEntity);
+ }
+ }
+
private static void addPermanentFunctionEntities(SessionState ss,
List<ReadEntity> inputList) throws HiveException {
for (Entry<String, FunctionInfo> function :
ss.getCurrentFunctionsInUse().entrySet()) {
if (function.getValue().getFunctionType() != FunctionType.PERSISTENT) {
diff --git
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/events/AlterTableEvent.java
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/events/AlterTableEvent.java
index 78793b74325..f58ff338303 100644
---
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/events/AlterTableEvent.java
+++
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/events/AlterTableEvent.java
@@ -25,6 +25,7 @@
import org.apache.hadoop.hive.metastore.api.hive_metastoreConstants;
import org.apache.hadoop.hive.metastore.events.PreAlterTableEvent;
import org.apache.hadoop.hive.metastore.events.PreEventContext;
+import org.apache.hadoop.hive.ql.security.authorization.AuthorizationUtils;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveOperationType;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.metastore.HiveMetaStoreAuthorizableEvent;
@@ -78,6 +79,8 @@ private List<HivePrivilegeObject> getInputHObjs() {
ret.add(getHivePrivilegeObject(oldTable));
+ addAvroSchemaUrlInputAuth(ret, event);
+
COMMAND_STR = buildCommandString(COMMAND_STR, oldTable);
LOG.debug("<== AlterTableEvent.getInputHObjs(): ret={}", ret);
@@ -122,6 +125,23 @@ private List<HivePrivilegeObject> getOutputHObjs() {
return ret;
}
+ private void addAvroSchemaUrlInputAuth(List<HivePrivilegeObject> ret,
PreAlterTableEvent event) {
+ Table newTable = event.getNewTable();
+ Table oldTable = event.getOldTable();
+ String newSchemaUrl =
AuthorizationUtils.getFilesystemAvroSchemaUrlToAuthorize(
+ new org.apache.hadoop.hive.ql.metadata.Table(newTable));
+ if (newSchemaUrl == null) {
+ return;
+ }
+ String oldSchemaUrl = oldTable == null ? null
+ : AuthorizationUtils.getFilesystemAvroSchemaUrlToAuthorize(
+ new org.apache.hadoop.hive.ql.metadata.Table(oldTable));
+ if (StringUtils.equals(oldSchemaUrl, newSchemaUrl)) {
+ return;
+ }
+ ret.add(getHivePrivilegeObjectDfsUri(newSchemaUrl));
+ }
+
private String buildCommandString(String cmdStr, Table tbl) {
String ret = cmdStr;
if (tbl != null) {
diff --git
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/events/CreateTableEvent.java
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/events/CreateTableEvent.java
index 69017f9d457..41c80304bdf 100644
---
a/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/events/CreateTableEvent.java
+++
b/ql/src/java/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/events/CreateTableEvent.java
@@ -26,6 +26,7 @@
import org.apache.hadoop.hive.metastore.events.PreCreateTableEvent;
import org.apache.hadoop.hive.metastore.events.PreEventContext;
import org.apache.hadoop.hive.metastore.utils.MetaStoreUtils;
+import org.apache.hadoop.hive.ql.security.authorization.AuthorizationUtils;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveOperationType;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject.HivePrivilegeObjectType;
@@ -65,18 +66,27 @@ private List<HivePrivilegeObject> getInputHObjs() {
Database database = event.getDatabase();
String uri = getSdLocation(table.getSd());
- if (StringUtils.isEmpty(uri)) {
- return ret;
+ if (StringUtils.isNotEmpty(uri)) {
+ // Skip DFS_URI only if table location is under default db path
+ if (this.needDFSUriAuth(uri, this.getDefaultTablePath(database, table)))
{
+ ret.add(new HivePrivilegeObject(HivePrivilegeObjectType.DFS_URI, uri));
+ }
}
- // Skip DFS_URI only if table location is under default db path
- if (this.needDFSUriAuth(uri, this.getDefaultTablePath(database, table))) {
- ret.add(new HivePrivilegeObject(HivePrivilegeObjectType.DFS_URI, uri));
- }
+ addAvroSchemaUrlInputAuth(ret, table);
return ret;
}
+ private void addAvroSchemaUrlInputAuth(List<HivePrivilegeObject> ret, Table
table) {
+ String schemaUrl =
AuthorizationUtils.getFilesystemAvroSchemaUrlToAuthorize(
+ new org.apache.hadoop.hive.ql.metadata.Table(table));
+ if (schemaUrl == null) {
+ return;
+ }
+ ret.add(getHivePrivilegeObjectDfsUri(schemaUrl));
+ }
+
private List<HivePrivilegeObject> getOutputHObjs() {
LOG.debug("==> CreateTableEvent.getOutputHObjs()");
diff --git
a/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/TestAvroSchemaUrlAuthorizationUtils.java
b/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/TestAvroSchemaUrlAuthorizationUtils.java
new file mode 100644
index 00000000000..1a86ee507b0
--- /dev/null
+++
b/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/TestAvroSchemaUrlAuthorizationUtils.java
@@ -0,0 +1,139 @@
+/*
+ * 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.hadoop.hive.ql.security.authorization;
+
+import org.apache.hadoop.hive.metastore.api.FieldSchema;
+import org.apache.hadoop.hive.metastore.api.SerDeInfo;
+import org.apache.hadoop.hive.metastore.api.StorageDescriptor;
+import org.apache.hadoop.hive.ql.hooks.Entity;
+import org.apache.hadoop.hive.ql.hooks.ReadEntity;
+import org.apache.hadoop.hive.ql.metadata.Table;
+import org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.AvroTableProperties;
+import org.apache.hadoop.hive.serde2.avro.AvroSerDe;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class TestAvroSchemaUrlAuthorizationUtils {
+
+ @Test
+ public void getFilesystemSchemaUrlToAuthorizeRequiresAvroSerde() {
+ org.apache.hadoop.hive.metastore.api.Table table =
avroTable("hdfs://nn/schema.avsc", null);
+
table.getSd().getSerdeInfo().setSerializationLib("org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe");
+ assertNull(AuthorizationUtils.getFilesystemAvroSchemaUrlToAuthorize(new
Table(table)));
+ }
+
+ @Test
+ public void getFilesystemSchemaUrlToAuthorizeSkipsWhenLiteralPresent() {
+ org.apache.hadoop.hive.metastore.api.Table table =
avroTable("hdfs://nn/schema.avsc",
+ "{\"type\":\"record\",\"name\":\"r\",\"fields\":[]}");
+ assertNull(AuthorizationUtils.getFilesystemAvroSchemaUrlToAuthorize(new
Table(table)));
+ }
+
+ @Test
+ public void getFilesystemSchemaUrlToAuthorizeReturnsHdfsUrl() {
+ String schemaUrl = "hdfs://nn/schema.avsc";
+ org.apache.hadoop.hive.metastore.api.Table table = avroTable(schemaUrl,
null);
+ assertEquals(schemaUrl,
+ AuthorizationUtils.getFilesystemAvroSchemaUrlToAuthorize(new
Table(table)));
+ }
+
+ @Test
+ public void getFilesystemSchemaUrlToAuthorizeRejectsHttpUrl() {
+ org.apache.hadoop.hive.metastore.api.Table table =
avroTable("http://example.com/schema.avsc", null);
+ assertNull(AuthorizationUtils.getFilesystemAvroSchemaUrlToAuthorize(new
Table(table)));
+ }
+
+ @Test
+ public void addSchemaUrlInputForReadEntityAddsDfsReadEntity() {
+ String schemaUrl = "hdfs://nn/schema.avsc";
+ Table table = new Table(avroTable(schemaUrl, null));
+ Set<ReadEntity> inputs = new LinkedHashSet<>();
+ ReadEntity tableInput = new ReadEntity(table);
+ inputs.add(tableInput);
+
+ AuthorizationUtils.addAvroSchemaUrlInputForReadEntity(inputs, tableInput);
+
+ assertEquals(2, inputs.size());
+ assertTrue(inputs.stream().anyMatch(input -> input.getTyp() ==
Entity.Type.DFS_DIR
+ && input.getD().toString().contains(schemaUrl)));
+ }
+
+ @Test
+ public void addSchemaUrlInputForReadEntitySkipsIndirectReads() {
+ Table table = new Table(avroTable("hdfs://nn/schema.avsc", null));
+ Set<ReadEntity> inputs = new LinkedHashSet<>();
+ ReadEntity tableInput = new ReadEntity(table, null, false);
+ inputs.add(tableInput);
+
+ AuthorizationUtils.addAvroSchemaUrlInputForReadEntity(inputs, tableInput);
+
+ assertEquals(1, inputs.size());
+ }
+
+ @Test
+ public void addSchemaUrlInputForReadEntityAddsDfsDirEntity() {
+ String schemaUrl = "hdfs://nn/schema.avsc";
+ Table table = new Table(avroTable(schemaUrl, null));
+ Set<ReadEntity> inputs = new LinkedHashSet<>();
+ ReadEntity tableInput = new ReadEntity(table);
+ inputs.add(tableInput);
+
+ AuthorizationUtils.addAvroSchemaUrlInputForReadEntity(inputs, tableInput);
+
+ boolean foundDfsInput = false;
+ for (ReadEntity input : inputs) {
+ if (input.getTyp() == Entity.Type.DFS_DIR) {
+ foundDfsInput = true;
+ assertTrue(input.getD().toString().contains("hdfs://nn/schema.avsc"));
+ }
+ }
+ assertTrue(foundDfsInput);
+ }
+
+ private static org.apache.hadoop.hive.metastore.api.Table avroTable(String
schemaUrl,
+ String schemaLiteral) {
+ org.apache.hadoop.hive.metastore.api.Table table = new
org.apache.hadoop.hive.metastore.api.Table();
+ table.setDbName("default");
+ table.setTableName("avro_tbl");
+ Map<String, String> params = new HashMap<>();
+ if (schemaUrl != null) {
+ params.put(AvroTableProperties.SCHEMA_URL.getPropName(), schemaUrl);
+ }
+ if (schemaLiteral != null) {
+ params.put(AvroTableProperties.SCHEMA_LITERAL.getPropName(),
schemaLiteral);
+ }
+ table.setParameters(params);
+
+ StorageDescriptor sd = new StorageDescriptor();
+ sd.setCols(new ArrayList<FieldSchema>());
+ SerDeInfo serdeInfo = new SerDeInfo();
+ serdeInfo.setSerializationLib(AvroSerDe.class.getName());
+ sd.setSerdeInfo(serdeInfo);
+ table.setSd(sd);
+ return table;
+ }
+}
diff --git
a/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/TestHiveMetaStoreAuthorizer.java
b/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/TestHiveMetaStoreAuthorizer.java
index ced07a10584..68f8508a3ed 100644
---
a/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/TestHiveMetaStoreAuthorizer.java
+++
b/ql/src/test/org/apache/hadoop/hive/ql/security/authorization/plugin/metastore/TestHiveMetaStoreAuthorizer.java
@@ -46,6 +46,8 @@
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveOperationType;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject;
import
org.apache.hadoop.hive.ql.security.authorization.plugin.metastore.filtercontext.TableFilterContext;
+import org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.AvroTableProperties;
+import org.apache.hadoop.hive.serde2.avro.AvroSerDe;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.thrift.TException;
import org.junit.Before;
@@ -68,6 +70,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify;
@@ -187,11 +190,13 @@ private Pair<List<HivePrivilegeObject>,
List<HivePrivilegeObject>> getHivePrivil
ArgumentCaptor<List<HivePrivilegeObject>> outputsCapturer = ArgumentCaptor
.forClass(class_listPrivObjects);
- verify(mockHiveAuthorizer).checkPrivileges(any(HiveOperationType.class),
+ verify(mockHiveAuthorizer,
atLeastOnce()).checkPrivileges(any(HiveOperationType.class),
inputsCapturer.capture(), outputsCapturer.capture(),
any(HiveAuthzContext.class));
- return new ImmutablePair<>(inputsCapturer.getValue(),
outputsCapturer.getValue());
+ int lastIdx = inputsCapturer.getAllValues().size() - 1;
+ return new ImmutablePair<>(inputsCapturer.getAllValues().get(lastIdx),
+ outputsCapturer.getAllValues().get(lastIdx));
}
@Test
@@ -842,6 +847,74 @@ public void testUnAuthorizedCause() {
}
}
+ @Test
+ public void testW_CreateAvroTable_SchemaUrlDfsUriPrivObject() throws
Exception {
+
UserGroupInformation.setLoginUser(UserGroupInformation.createRemoteUser(authorizedUser));
+ String schemaUrl = "hdfs://namenode:9000/path/to/schema.avsc";
+ try {
+ Table table = new TableBuilder()
+ .setTableName("avro_tbl")
+ .addCol("id", ColumnType.INT_TYPE_NAME)
+ .setSerdeLib(AvroSerDe.class.getName())
+ .addTableParam(AvroTableProperties.SCHEMA_URL.getPropName(),
schemaUrl)
+ .setOwner(authorizedUser)
+ .build(conf);
+ hmsHandler.create_table(table);
+
+ Pair<List<HivePrivilegeObject>, List<HivePrivilegeObject>> io =
getHivePrivilegeObjectsFromLastCall();
+ List<HivePrivilegeObject> inputs = io.getLeft();
+ List<HivePrivilegeObject> dfsUriInputs = inputs.stream()
+ .filter(o -> o.getType() ==
HivePrivilegeObject.HivePrivilegeObjectType.DFS_URI)
+ .collect(Collectors.toList());
+
+ assertTrue("Should authorize read access to avro.schema.url",
+ dfsUriInputs.stream().anyMatch(o ->
schemaUrl.equals(o.getObjectName())));
+ } finally {
+ try {
+ hmsHandler.drop_table("default", "avro_tbl", true);
+ } catch (Exception e) {
+ // Ignore cleanup errors
+ }
+ }
+ }
+
+ @Test
+ public void testX_AlterAvroTable_SchemaUrlDfsUriPrivObject() throws
Exception {
+
UserGroupInformation.setLoginUser(UserGroupInformation.createRemoteUser(authorizedUser));
+ String oldSchemaUrl = "hdfs://namenode:9000/path/to/old_schema.avsc";
+ String newSchemaUrl = "hdfs://namenode:9000/path/to/new_schema.avsc";
+ try {
+ Table table = new TableBuilder()
+ .setTableName("avro_tbl")
+ .addCol("id", ColumnType.INT_TYPE_NAME)
+ .setSerdeLib(AvroSerDe.class.getName())
+ .addTableParam(AvroTableProperties.SCHEMA_URL.getPropName(),
oldSchemaUrl)
+ .setOwner(authorizedUser)
+ .build(conf);
+ hmsHandler.create_table(table);
+ GetTableRequest request = new GetTableRequest("default", "avro_tbl");
+ request.setCatName("hive");
+ Table alteredTable = hmsHandler.get_table_core(request);
+
alteredTable.getParameters().put(AvroTableProperties.SCHEMA_URL.getPropName(),
newSchemaUrl);
+ hmsHandler.alter_table("default", "avro_tbl", alteredTable);
+
+ Pair<List<HivePrivilegeObject>, List<HivePrivilegeObject>> io =
getHivePrivilegeObjectsFromLastCall();
+ List<HivePrivilegeObject> inputs = io.getLeft();
+ List<HivePrivilegeObject> dfsUriInputs = inputs.stream()
+ .filter(o -> o.getType() ==
HivePrivilegeObject.HivePrivilegeObjectType.DFS_URI)
+ .collect(Collectors.toList());
+
+ assertTrue("Should authorize read access to updated avro.schema.url",
+ dfsUriInputs.stream().anyMatch(o ->
newSchemaUrl.equals(o.getObjectName())));
+ } finally {
+ try {
+ hmsHandler.drop_table("default", "avro_tbl", true);
+ } catch (Exception e) {
+ // Ignore cleanup errors
+ }
+ }
+ }
+
@Test
public void testDropTableNoTablePathWritePermissionShouldFail() throws
Exception {
UserGroupInformation.setLoginUser(
diff --git
a/ql/src/test/results/clientpositive/llap/avro_extschema_insert.q.out
b/ql/src/test/results/clientpositive/llap/avro_extschema_insert.q.out
index b083819fddc..aec2bc47adf 100644
--- a/ql/src/test/results/clientpositive/llap/avro_extschema_insert.q.out
+++ b/ql/src/test/results/clientpositive/llap/avro_extschema_insert.q.out
@@ -86,11 +86,13 @@ POSTHOOK: Lineage: avro_extschema_insert1
PARTITION(p1=part1).col2 SCRIPT []
POSTHOOK: Lineage: avro_extschema_insert1 PARTITION(p1=part1).col3 SCRIPT []
PREHOOK: query: insert overwrite table avro_extschema_insert2 partition (p1)
select * from avro_extschema_insert1
PREHOOK: type: QUERY
+#### A masked pattern was here ####
PREHOOK: Input: default@avro_extschema_insert1
PREHOOK: Input: default@avro_extschema_insert1@p1=part1
PREHOOK: Output: default@avro_extschema_insert2
POSTHOOK: query: insert overwrite table avro_extschema_insert2 partition (p1)
select * from avro_extschema_insert1
POSTHOOK: type: QUERY
+#### A masked pattern was here ####
POSTHOOK: Input: default@avro_extschema_insert1
POSTHOOK: Input: default@avro_extschema_insert1@p1=part1
POSTHOOK: Output: default@avro_extschema_insert2
@@ -100,11 +102,13 @@ POSTHOOK: Lineage: avro_extschema_insert2
PARTITION(p1=part1).col2 SIMPLE [(avro
POSTHOOK: Lineage: avro_extschema_insert2 PARTITION(p1=part1).col3 SIMPLE
[(avro_extschema_insert1)avro_extschema_insert1.FieldSchema(name:col3,
type:string, comment:), ]
PREHOOK: query: select * from avro_extschema_insert2
PREHOOK: type: QUERY
+#### A masked pattern was here ####
PREHOOK: Input: default@avro_extschema_insert2
PREHOOK: Input: default@avro_extschema_insert2@p1=part1
#### A masked pattern was here ####
POSTHOOK: query: select * from avro_extschema_insert2
POSTHOOK: type: QUERY
+#### A masked pattern was here ####
POSTHOOK: Input: default@avro_extschema_insert2
POSTHOOK: Input: default@avro_extschema_insert2@p1=part1
#### A masked pattern was here ####
diff --git
a/ql/src/test/results/clientpositive/llap/avro_tableproperty_optimize.q.out
b/ql/src/test/results/clientpositive/llap/avro_tableproperty_optimize.q.out
index 8f6c02f01c6..4283c91daa8 100644
--- a/ql/src/test/results/clientpositive/llap/avro_tableproperty_optimize.q.out
+++ b/ql/src/test/results/clientpositive/llap/avro_tableproperty_optimize.q.out
@@ -93,10 +93,12 @@ col3 string
#### A masked pattern was here ####
PREHOOK: query: SELECT * FROM avro_extschema_url_n0
PREHOOK: type: QUERY
+#### A masked pattern was here ####
PREHOOK: Input: default@avro_extschema_url_n0
#### A masked pattern was here ####
POSTHOOK: query: SELECT * FROM avro_extschema_url_n0
POSTHOOK: type: QUERY
+#### A masked pattern was here ####
POSTHOOK: Input: default@avro_extschema_url_n0
#### A masked pattern was here ####
s1 1 s2
diff --git a/ql/src/test/results/clientpositive/llap/avrotblsjoin.q.out
b/ql/src/test/results/clientpositive/llap/avrotblsjoin.q.out
index 907af5705fd..ea376b0da57 100644
--- a/ql/src/test/results/clientpositive/llap/avrotblsjoin.q.out
+++ b/ql/src/test/results/clientpositive/llap/avrotblsjoin.q.out
@@ -81,11 +81,13 @@ WARNING: Comparing bigint and string may result in loss of
information.
Warning: Shuffle Join MERGEJOIN[13][tables = [$hdt$_0, $hdt$_1]] in Stage
'Reducer 2' is a cross product
PREHOOK: query: select table1_n1.col1, table1_1.* from table1_n1 join table1_1
on table1_n1.col1=table1_1.col1 where table1_1.col1="1"
PREHOOK: type: QUERY
+#### A masked pattern was here ####
PREHOOK: Input: default@table1_1
PREHOOK: Input: default@table1_n1
#### A masked pattern was here ####
POSTHOOK: query: select table1_n1.col1, table1_1.* from table1_n1 join
table1_1 on table1_n1.col1=table1_1.col1 where table1_1.col1="1"
POSTHOOK: type: QUERY
+#### A masked pattern was here ####
POSTHOOK: Input: default@table1_1
POSTHOOK: Input: default@table1_n1
#### A masked pattern was here ####
diff --git a/ql/src/test/results/clientpositive/llap/compustat_avro.q.out
b/ql/src/test/results/clientpositive/llap/compustat_avro.q.out
index 312115e03af..ee171ff1d71 100644
--- a/ql/src/test/results/clientpositive/llap/compustat_avro.q.out
+++ b/ql/src/test/results/clientpositive/llap/compustat_avro.q.out
@@ -47,11 +47,13 @@ comment
COLUMN_STATS_ACCURATE
{\"BASIC_STATS\":\"true\",\"COLUMN_STATS\":{\"col1\":\"true\",\"col2\":\"true\",\"col3\":\"true\",\"col4\":\"true\",\"col5\":\"true\",\"col6\":\"true\"}}
PREHOOK: query: analyze table testAvro compute statistics for columns col1,col3
PREHOOK: type: ANALYZE_TABLE
+#### A masked pattern was here ####
PREHOOK: Input: default@testavro
PREHOOK: Output: default@testavro
#### A masked pattern was here ####
POSTHOOK: query: analyze table testAvro compute statistics for columns
col1,col3
POSTHOOK: type: ANALYZE_TABLE
+#### A masked pattern was here ####
POSTHOOK: Input: default@testavro
POSTHOOK: Output: default@testavro
#### A masked pattern was here ####
diff --git
a/serde/src/java/org/apache/hadoop/hive/serde2/avro/AvroSerdeUtils.java
b/serde/src/java/org/apache/hadoop/hive/serde2/avro/AvroSerdeUtils.java
index e468c58a6d5..2d3dd6236e6 100644
--- a/serde/src/java/org/apache/hadoop/hive/serde2/avro/AvroSerdeUtils.java
+++ b/serde/src/java/org/apache/hadoop/hive/serde2/avro/AvroSerdeUtils.java
@@ -43,10 +43,13 @@
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
+import java.util.Locale;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
/**
* Utilities useful only to the AvroSerde itself. Not mean to be used by
@@ -97,7 +100,8 @@ public String getPropName(){
+ AvroTableProperties.SCHEMA_LITERAL.getPropName() + " nor "
+ AvroTableProperties.SCHEMA_URL.getPropName() + " specified, can't
determine table schema";
-
+ private static final Set<String> HTTP_SCHEMES =
+ new HashSet<>(Arrays.asList("http", "https"));
/**
* Determine the schema to that's been provided for Avro serde work.
@@ -137,11 +141,26 @@ public static Schema
determineSchemaOrThrowException(Configuration conf, Propert
throw new AvroSerdeException(EXCEPTION_MESSAGE);
}
+ validateSchemaUrl(schemaString, conf);
+
try {
- Schema s = getSchemaFromFS(schemaString, conf);
- if (s == null) {
- //in case schema is not a file system
- return AvroSerdeUtils.getSchemaFor(new URL(schemaString));
+ Schema s;
+ if (requiresHttpFetch(schemaString)) {
+ URL url = new URL(schemaString);
+ java.net.URLConnection conn = url.openConnection();
+ if (conn instanceof java.net.HttpURLConnection) {
+ ((java.net.HttpURLConnection)
conn).setInstanceFollowRedirects(false);
+ }
+ try (InputStream in = conn.getInputStream()) {
+ s = getSchemaParser().parse(in);
+ }
+ } else {
+ s = getSchemaFromFS(schemaString, conf);
+ if (s == null) {
+ throw new AvroSerdeException("Unable to read Avro schema from: " +
schemaString
+ + ". avro.schema.url must refer to a Hadoop FileSystem URI (e.g.
hdfs://, s3a://). "
+ + "Consider using avro.schema.literal instead.");
+ }
}
return s;
} catch (IOException ioe) {
@@ -151,6 +170,115 @@ public static Schema
determineSchemaOrThrowException(Configuration conf, Propert
}
}
+ /**
+ * Validate that the avro.schema.url uses a permitted scheme and host.
+ */
+ static void validateSchemaUrl(String schemaUrl, Configuration conf) throws
AvroSerdeException {
+ final URI uri;
+ try {
+ uri = new URI(schemaUrl);
+ } catch (URISyntaxException e) {
+ throw new AvroSerdeException("Invalid avro.schema.url: " + schemaUrl, e);
+ }
+
+ final String scheme = uri.getScheme();
+ if (scheme == null || scheme.isEmpty()) {
+ return;
+ }
+
+ final String schemeLower = scheme.toLowerCase(Locale.ROOT);
+ if ("file".equals(schemeLower)) {
+ throw new AvroSerdeException("avro.schema.url scheme '" + scheme
+ + "' is not permitted. Use a Hadoop FileSystem URI (e.g. hdfs://,
s3a://) or "
+ + "avro.schema.literal instead.");
+ }
+ if (HTTP_SCHEMES.contains(schemeLower)) {
+ if (!HiveConf.getBoolVar(conf,
HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_REMOTE_HTTP_ENABLED)) {
+ throw new AvroSerdeException("avro.schema.url scheme '" + scheme
+ + "' is not permitted. Remote HTTP schema fetch is disabled. Set "
+ +
HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_REMOTE_HTTP_ENABLED.varname
+ + " to true and configure "
+ + HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_HTTP_ALLOWED_HOSTS.varname
+ + ", or use avro.schema.literal instead.");
+ }
+ final String host = uri.getHost();
+ if (host == null || host.isEmpty()) {
+ throw new AvroSerdeException("avro.schema.url must specify a host for
HTTP/HTTPS schemas.");
+ }
+ if (!isHttpHostAllowed(host, conf)) {
+ throw new AvroSerdeException("avro.schema.url host '" + host
+ + "' is not in the permitted host list configured by "
+ +
HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_HTTP_ALLOWED_HOSTS.varname + ".");
+ }
+ return;
+ }
+
+ if (!getAllowedFilesystemSchemes(conf).contains(schemeLower)) {
+ throw new AvroSerdeException("avro.schema.url scheme '" + scheme
+ + "' is not permitted. Use a Hadoop FileSystem URI (e.g. hdfs://,
s3a://) or "
+ + "avro.schema.literal instead.");
+ }
+ }
+
+ /**
+ * Returns true if the schema URL refers to a Hadoop filesystem location
(including scheme-less URIs).
+ */
+ public static boolean isFilesystemSchemaUrl(String schemaUrl) {
+ if (schemaUrl == null || schemaUrl.isEmpty() ||
SCHEMA_NONE.equals(schemaUrl)) {
+ return false;
+ }
+ try {
+ URI uri = new URI(schemaUrl);
+ String scheme = uri.getScheme();
+ if (scheme == null || scheme.isEmpty()) {
+ return true;
+ }
+ String schemeLower = scheme.toLowerCase(Locale.ROOT);
+ return !HTTP_SCHEMES.contains(schemeLower) &&
!"file".equals(schemeLower);
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ private static boolean requiresHttpFetch(String schemaUrl) throws
AvroSerdeException {
+ try {
+ URI uri = new URI(schemaUrl);
+ String scheme = uri.getScheme();
+ return scheme != null &&
HTTP_SCHEMES.contains(scheme.toLowerCase(Locale.ROOT));
+ } catch (URISyntaxException e) {
+ throw new AvroSerdeException("Invalid avro.schema.url: " + schemaUrl, e);
+ }
+ }
+
+ private static Set<String> getAllowedFilesystemSchemes(Configuration conf) {
+ String schemes = conf == null
+ ?
HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_ALLOWED_SCHEMES.getDefaultValue()
+ : HiveConf.getVar(conf,
HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_ALLOWED_SCHEMES);
+ Set<String> allowed = new HashSet<>();
+ for (String scheme : schemes.split(",")) {
+ String trimmed = scheme.trim().toLowerCase(Locale.ROOT);
+ if (!trimmed.isEmpty()) {
+ allowed.add(trimmed);
+ }
+ }
+ return allowed;
+ }
+
+ private static boolean isHttpHostAllowed(String host, Configuration conf) {
+ String allowedHosts = HiveConf.getVar(conf,
HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_HTTP_ALLOWED_HOSTS);
+ if (allowedHosts == null || allowedHosts.trim().isEmpty()) {
+ return false;
+ }
+ String hostLower = host.toLowerCase(Locale.ROOT);
+ for (String allowedHost : allowedHosts.split(",")) {
+ String trimmed = allowedHost.trim().toLowerCase(Locale.ROOT);
+ if (!trimmed.isEmpty() && trimmed.equals(hostLower)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
// Protected for testing and so we can pass in a conf for testing.
protected static Schema getSchemaFromFS(String schemaFSUrl,
Configuration conf) throws IOException,
URISyntaxException {
diff --git
a/serde/src/test/org/apache/hadoop/hive/serde2/avro/TestAvroSerdeUtils.java
b/serde/src/test/org/apache/hadoop/hive/serde2/avro/TestAvroSerdeUtils.java
index 3ef802d605d..509138065e9 100644
--- a/serde/src/test/org/apache/hadoop/hive/serde2/avro/TestAvroSerdeUtils.java
+++ b/serde/src/test/org/apache/hadoop/hive/serde2/avro/TestAvroSerdeUtils.java
@@ -22,6 +22,7 @@
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.MiniDFSCluster;
+import org.apache.hadoop.hive.conf.HiveConf;
import org.junit.Test;
import java.io.IOException;
@@ -38,6 +39,7 @@
import static org.apache.hadoop.hive.serde2.avro.AvroSerdeUtils.isNullableType;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class TestAvroSerdeUtils {
@@ -151,19 +153,103 @@ public void determineSchemaFindsLiterals() throws
Exception {
}
@Test
- public void determineSchemaTriesToOpenUrl() throws AvroSerdeException,
IOException {
+ public void determineSchemaRejectsUnknownSchemeUrl() throws
AvroSerdeException, IOException {
Configuration conf = new Configuration();
Properties props = new Properties();
props.put(AvroTableProperties.SCHEMA_URL.getPropName(),
"not:///a.real.url");
try {
AvroSerdeUtils.determineSchemaOrThrowException(conf, props);
- fail("Should have tried to open that URL");
+ fail("Should have rejected unknown scheme URL");
} catch (AvroSerdeException e) {
- assertEquals("Unable to read schema from given path: not:///a.real.url",
e.getMessage());
+ assertTrue(e.getMessage().contains("not permitted"));
}
}
+ @Test
+ public void determineSchemaRejectsHttpByDefault() throws IOException {
+ Configuration conf = new Configuration();
+ Properties props = new Properties();
+ props.put(AvroTableProperties.SCHEMA_URL.getPropName(),
+ "http://169.254.169.254/latest/meta-data/");
+
+ try {
+ determineSchemaOrThrowException(conf, props);
+ fail("Should have rejected HTTP schema URL");
+ } catch (AvroSerdeException e) {
+ assertTrue(e.getMessage().contains("not permitted"));
+ }
+ }
+
+ @Test
+ public void determineSchemaRejectsHttpsByDefault() throws IOException {
+ Configuration conf = new Configuration();
+ Properties props = new Properties();
+ props.put(AvroTableProperties.SCHEMA_URL.getPropName(),
"https://internal-service/admin");
+
+ try {
+ determineSchemaOrThrowException(conf, props);
+ fail("Should have rejected HTTPS schema URL");
+ } catch (AvroSerdeException e) {
+ assertTrue(e.getMessage().contains("not permitted"));
+ }
+ }
+
+ @Test
+ public void determineSchemaRejectsFileUrl() throws IOException {
+ Configuration conf = new Configuration();
+ Properties props = new Properties();
+ props.put(AvroTableProperties.SCHEMA_URL.getPropName(),
"file:///etc/passwd");
+
+ try {
+ determineSchemaOrThrowException(conf, props);
+ fail("Should have rejected file:// schema URL");
+ } catch (AvroSerdeException e) {
+ assertTrue(e.getMessage().contains("not permitted"));
+ }
+ }
+
+ @Test
+ public void determineSchemaRejectsHttpWhenHostNotAllowlisted() throws
IOException {
+ HiveConf conf = new HiveConf();
+
conf.setBoolVar(HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_REMOTE_HTTP_ENABLED,
true);
+ conf.setVar(HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_HTTP_ALLOWED_HOSTS,
"schema.example.com");
+ Properties props = new Properties();
+ props.put(AvroTableProperties.SCHEMA_URL.getPropName(),
+ "http://169.254.169.254/latest/meta-data/");
+
+ try {
+ determineSchemaOrThrowException(conf, props);
+ fail("Should have rejected HTTP host not in allowlist");
+ } catch (AvroSerdeException e) {
+ assertTrue(e.getMessage().contains("not in the permitted host list"));
+ }
+ }
+
+ @Test
+ public void determineSchemaAllowsHttpWhenHostAllowlisted() throws
IOException {
+ HiveConf conf = new HiveConf();
+
conf.setBoolVar(HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_REMOTE_HTTP_ENABLED,
true);
+ conf.setVar(HiveConf.ConfVars.HIVE_AVRO_SCHEMA_URL_HTTP_ALLOWED_HOSTS,
"127.0.0.1");
+ Properties props = new Properties();
+ props.put(AvroTableProperties.SCHEMA_URL.getPropName(),
"http://127.0.0.1:1/schema.avsc");
+
+ try {
+ determineSchemaOrThrowException(conf, props);
+ fail("Expected fetch failure after HTTP validation passed");
+ } catch (AvroSerdeException e) {
+ assertTrue(e.getMessage().contains("Unable to read schema from given
path"));
+ }
+ }
+
+ @Test
+ public void isFilesystemSchemaUrlIdentifiesFilesystemUrls() {
+ assertTrue(AvroSerdeUtils.isFilesystemSchemaUrl("hdfs://nn/schema.avsc"));
+ assertTrue(AvroSerdeUtils.isFilesystemSchemaUrl("/tmp/schema.avsc"));
+
assertTrue(!AvroSerdeUtils.isFilesystemSchemaUrl("http://example.com/schema.avsc"));
+ assertTrue(!AvroSerdeUtils.isFilesystemSchemaUrl("file:///etc/passwd"));
+ }
+
@Test
public void noneOptionWorksForSpecifyingSchemas() throws IOException,
AvroSerdeException {
Configuration conf = new Configuration();
@@ -195,9 +281,9 @@ public void noneOptionWorksForSpecifyingSchemas() throws
IOException, AvroSerdeE
props.put(AvroTableProperties.SCHEMA_URL.getPropName(),
"not:///a.real.url");
try {
determineSchemaOrThrowException(conf, props);
- fail("Should have tried to open that bogus URL");
+ fail("Should have rejected unknown scheme URL");
} catch (AvroSerdeException e) {
- assertEquals("Unable to read schema from given path: not:///a.real.url",
e.getMessage());
+ assertTrue(e.getMessage().contains("not permitted"));
}
}
@@ -221,6 +307,11 @@ public void determineSchemaCanReadSchemaFromHDFS() throws
IOException, AvroSerde
AvroSerdeUtils.getSchemaFromFS(onHDFS,
miniDfs.getFileSystem().getConf());
Schema expectedSchema = AvroSerdeUtils.getSchemaFor(schemaString);
assertEquals(expectedSchema, schemaFromHDFS);
+
+ Properties props = new Properties();
+ props.put(AvroTableProperties.SCHEMA_URL.getPropName(), onHDFS);
+ Schema resolved =
determineSchemaOrThrowException(miniDfs.getFileSystem().getConf(), props);
+ assertEquals(expectedSchema, resolved);
} finally {
if(miniDfs != null) miniDfs.shutdown();
}