Skip to content

Commit c092060

Browse files
author
Dmitriy Fingerman
committed
HIVE-29359: Support Credential Vending in Hive Iceberg REST Catalog Client
1 parent ba43ea3 commit c092060

8 files changed

Lines changed: 401 additions & 128 deletions

File tree

iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/IcebergCatalogProperties.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@
3232
public class IcebergCatalogProperties {
3333
public static final String CATALOG_NAME = "iceberg.catalog";
3434
public static final String CATALOG_CONFIG_PREFIX = "iceberg.catalog.";
35+
36+
/**
37+
* Optional comma-separated Iceberg REST access delegation modes for catalog HTTP requests (for example
38+
* {@code vended-credentials}). Hive maps this to the {@code X-Iceberg-Access-Delegation} header when initializing the
39+
* REST catalog client, unless {@code iceberg.catalog.<name>.header.X-Iceberg-Access-Delegation} is set explicitly.
40+
*
41+
* @see <a href="https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml">REST catalog spec</a>
42+
*/
43+
public static final String REST_ACCESS_DELEGATION = "rest.access-delegation";
3544
public static final String CATALOG_WAREHOUSE_TEMPLATE = "iceberg.catalog.%s.warehouse";
3645
public static final String CATALOG_IMPL_TEMPLATE = "iceberg.catalog.%s.catalog-impl";
3746
public static final String CATALOG_DEFAULT_CONFIG_PREFIX = "iceberg.catalog-default.";

iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/client/HiveRESTCatalogClient.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ public class HiveRESTCatalogClient extends BaseMetaStoreClient {
6464
public static final String DB_OWNER = "owner";
6565
public static final String DB_OWNER_TYPE = "ownerType";
6666

67+
/**
68+
* Iceberg REST catalog property prefix recognized by {@link org.apache.iceberg.rest.RESTUtil#configHeaders}; values
69+
* are sent as HTTP headers on REST requests.
70+
*/
71+
public static final String ICEBERG_ACCESS_DELEGATION_HEADER_PROPERTY = "header.X-Iceberg-Access-Delegation";
72+
6773
private static final Logger LOG = LoggerFactory.getLogger(HiveRESTCatalogClient.class);
6874

6975
private RESTCatalog restCatalog;
@@ -81,10 +87,44 @@ public HiveRESTCatalogClient(Configuration conf) {
8187
public void reconnect() {
8288
close();
8389
String catName = MetaStoreUtils.getDefaultCatalog(conf);
84-
Map<String, String> properties = IcebergCatalogProperties.getCatalogProperties(conf);
90+
Map<String, String> properties =
91+
applyAccessDelegationHeader(IcebergCatalogProperties.getCatalogProperties(conf));
8592
restCatalog = (RESTCatalog) CatalogUtil.buildIcebergCatalog(catName, properties, null);
8693
}
8794

95+
/**
96+
* Maps Hive catalog property {@link IcebergCatalogProperties#REST_ACCESS_DELEGATION} to the Iceberg REST
97+
* {@code X-Iceberg-Access-Delegation} header so any spec-compliant catalog may attach vended (or other delegated)
98+
* storage credentials to load responses. An explicit {@link #ICEBERG_ACCESS_DELEGATION_HEADER_PROPERTY} entry
99+
* always wins and is left unchanged.
100+
*/
101+
static Map<String, String> applyAccessDelegationHeader(Map<String, String> catalogProps) {
102+
if (catalogProps.containsKey(ICEBERG_ACCESS_DELEGATION_HEADER_PROPERTY)) {
103+
return catalogProps;
104+
}
105+
String delegation = trimToNull(catalogProps.get(IcebergCatalogProperties.REST_ACCESS_DELEGATION));
106+
if (delegation == null) {
107+
return catalogProps;
108+
}
109+
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
110+
catalogProps.forEach(
111+
(key, value) -> {
112+
if (!IcebergCatalogProperties.REST_ACCESS_DELEGATION.equals(key)) {
113+
builder.put(key, value);
114+
}
115+
});
116+
builder.put(ICEBERG_ACCESS_DELEGATION_HEADER_PROPERTY, delegation);
117+
return builder.build();
118+
}
119+
120+
private static String trimToNull(String value) {
121+
if (value == null) {
122+
return null;
123+
}
124+
String trimmed = value.trim();
125+
return trimmed.isEmpty() ? null : trimmed;
126+
}
127+
88128
@Override
89129
public void close() {
90130
try {

iceberg/iceberg-catalog/src/test/java/org/apache/iceberg/hive/client/TestHiveRESTCatalogClient.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.apache.iceberg.catalog.Namespace;
4545
import org.apache.iceberg.catalog.TableIdentifier;
4646
import org.apache.iceberg.hive.HiveSchemaUtil;
47+
import org.apache.iceberg.hive.IcebergCatalogProperties;
4748
import org.apache.iceberg.io.FileIO;
4849
import org.apache.iceberg.io.LocationProvider;
4950
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
@@ -136,6 +137,37 @@ public void after() {
136137

137138
}
138139

140+
@Test
141+
public void applyAccessDelegationHeaderMapsToIcebergRestHeader() {
142+
Map<String, String> in = Maps.newHashMap();
143+
in.put("uri", "http://localhost");
144+
in.put(IcebergCatalogProperties.REST_ACCESS_DELEGATION, "vended-credentials");
145+
Map<String, String> out = HiveRESTCatalogClient.applyAccessDelegationHeader(in);
146+
assertThat(out.get(HiveRESTCatalogClient.ICEBERG_ACCESS_DELEGATION_HEADER_PROPERTY))
147+
.isEqualTo("vended-credentials");
148+
assertThat(out.containsKey(IcebergCatalogProperties.REST_ACCESS_DELEGATION)).isFalse();
149+
assertThat(out.get("uri")).isEqualTo("http://localhost");
150+
}
151+
152+
@Test
153+
public void applyAccessDelegationHeaderExplicitHeaderWins() {
154+
Map<String, String> in = Maps.newHashMap();
155+
in.put(IcebergCatalogProperties.REST_ACCESS_DELEGATION, "vended-credentials");
156+
in.put(HiveRESTCatalogClient.ICEBERG_ACCESS_DELEGATION_HEADER_PROPERTY, "remote-signing");
157+
Map<String, String> out = HiveRESTCatalogClient.applyAccessDelegationHeader(in);
158+
assertThat(out).isSameAs(in);
159+
assertThat(out.get(HiveRESTCatalogClient.ICEBERG_ACCESS_DELEGATION_HEADER_PROPERTY))
160+
.isEqualTo("remote-signing");
161+
}
162+
163+
@Test
164+
public void applyAccessDelegationHeaderNoOpWhenUnset() {
165+
Map<String, String> in = Maps.newHashMap();
166+
in.put("uri", "http://localhost");
167+
Map<String, String> out = HiveRESTCatalogClient.applyAccessDelegationHeader(in);
168+
assertThat(out).isSameAs(in);
169+
}
170+
139171
@Test
140172
public void testGetTable() throws TException {
141173
spyHiveRESTCatalogClient.getTable("default", "tableName");

iceberg/iceberg-handler/src/test/queries/positive/iceberg_rest_catalog_gravitino.q

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
--! qt:replace:/(\s+neededVirtualColumns:\s)(.*)/$1#Masked#/
44
-- Mask random uuid
55
--! qt:replace:/(\s+'uuid'=')\S+('\s*)/$1#Masked#$2/
6+
-- Mask Iceberg metadata file name (sequence id + UUID) in show create table
7+
--! qt:replace:/('metadata_location'=')[^']+(')/$1#Masked#$2/
8+
-- Mask metadata_location path in describe formatted ('|' delimiter: regex contains s3:// so '/' breaks QTestReplaceHandler).
9+
--! qt:replace:|(metadata_location)(\s+)(s3://\S+)|$1$2#Masked#|
610
-- Mask random uuid
711
--! qt:replace:/(\s+uuid\s+)\S+(\s*)/$1#Masked#$2/
812
-- Mask a random snapshot id
@@ -26,9 +30,10 @@ set hive.stats.autogather=false;
2630
set metastore.client.impl=org.apache.iceberg.hive.client.HiveRESTCatalogClient;
2731
set metastore.catalog.default=ice01;
2832
set iceberg.catalog.ice01.type=rest;
33+
set iceberg.catalog.ice01.rest.access-delegation=vended-credentials;
2934

30-
--! This config is set in the driver setup (see TestIcebergRESTCatalogLlapLocalCliDriver.java)
31-
--! conf.set('iceberg.catalog.ice01.uri', <RESTServer URI>);
35+
--! REST URI, OAuth, MinIO + Gravitino S3 warehouse / credential vending, and host S3A are set in
36+
--! TestIcebergRESTCatalogGravitinoLlapLocalCliDriver.
3237

3338
create database ice_rest;
3439
use ice_rest;
@@ -66,11 +71,6 @@ show create table ice_orc2;
6671
insert into ice_orc2 partition (company_id=100)
6772
VALUES ('fn1','ln1', 1, 10), ('fn2','ln2', 2, 20), ('fn3','ln3', 3, 30);
6873

69-
--! In CI, Testcontainers' .withFileSystemBind() is not able to bind the same host path to the same container path,
70-
--! so as a workaround, the .metadata.json files from container are manually synced in a daemon process,
71-
--! since the sync can take some time, need to wait for it to happen after the insert operation.
72-
! sleep 20;
73-
7474
describe formatted ice_orc2;
7575
select * from ice_orc2;
7676

iceberg/iceberg-handler/src/test/results/positive/llap/iceberg_rest_catalog_gravitino.q.out

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@ STORED BY
7979
WITH SERDEPROPERTIES (
8080
'serialization.format'='1')
8181
LOCATION
82-
#### A masked pattern was here ####
82+
's3://iceberg-vend/warehouse/ice_rest/ice_orc2'
8383
TBLPROPERTIES (
8484
'bucketing_version'='2',
8585
'current-schema'='{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"first_name","required":false,"type":"string"},{"id":2,"name":"last_name","required":false,"type":"string"},{"id":3,"name":"dept_id","required":false,"type":"long"},{"id":4,"name":"team_id","required":false,"type":"long"},{"id":5,"name":"company_id","required":false,"type":"long"}]}',
8686
'default-partition-spec'='{"spec-id":0,"fields":[{"name":"company_id","transform":"identity","source-id":5,"field-id":1000}]}',
8787
'format-version'='2',
8888
'iceberg.catalog'='ice01',
8989
'iceberg.orc.files.only'='true',
90-
#### A masked pattern was here ####
90+
'metadata_location'='#Masked#',
9191
'name'='ice_rest.ice_orc2',
9292
'parquet.compression'='zstd',
9393
'serialization.format'='1',
@@ -143,7 +143,7 @@ Table Parameters:
143143
format-version 2
144144
iceberg.catalog ice01
145145
iceberg.orc.files.only true
146-
#### A masked pattern was here ####
146+
metadata_location #Masked#
147147
name ice_rest.ice_orc2
148148
numFiles 1
149149
numRows 3
@@ -219,6 +219,7 @@ PREHOOK: type: SHOWDATABASES
219219
POSTHOOK: query: show databases
220220
POSTHOOK: type: SHOWDATABASES
221221
ice_rest
222+
vend_cred_chk
222223
PREHOOK: query: drop database ice_rest
223224
PREHOOK: type: DROPDATABASE
224225
PREHOOK: Input: database:ice_rest
@@ -231,3 +232,4 @@ PREHOOK: query: show databases
231232
PREHOOK: type: SHOWDATABASES
232233
POSTHOOK: query: show databases
233234
POSTHOOK: type: SHOWDATABASES
235+
vend_cred_chk

itests/qtest-iceberg/pom.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
<artifactId>metrics-core</artifactId>
3838
<version>${dropwizard.version}</version>
3939
</dependency>
40+
<dependency>
41+
<groupId>io.minio</groupId>
42+
<artifactId>minio</artifactId>
43+
<version>8.5.11</version>
44+
<scope>test</scope>
45+
</dependency>
4046
<!-- dependencies are always listed in sorted order by groupId, artifactId -->
4147
<!-- test intra-project -->
4248
<dependency>
@@ -140,6 +146,11 @@
140146
<artifactId>junit</artifactId>
141147
<scope>test</scope>
142148
</dependency>
149+
<dependency>
150+
<groupId>org.assertj</groupId>
151+
<artifactId>assertj-core</artifactId>
152+
<scope>test</scope>
153+
</dependency>
143154
<dependency>
144155
<groupId>com.sun.jersey</groupId>
145156
<artifactId>jersey-servlet</artifactId>
@@ -156,6 +167,11 @@
156167
<artifactId>hadoop-archives</artifactId>
157168
<scope>test</scope>
158169
</dependency>
170+
<dependency>
171+
<groupId>org.apache.hadoop</groupId>
172+
<artifactId>hadoop-aws</artifactId>
173+
<scope>test</scope>
174+
</dependency>
159175
<dependency>
160176
<groupId>org.apache.hadoop</groupId>
161177
<artifactId>hadoop-common</artifactId>
@@ -364,6 +380,12 @@
364380
<artifactId>hbase-mapreduce</artifactId>
365381
<scope>test</scope>
366382
</dependency>
383+
<dependency>
384+
<groupId>org.apache.iceberg</groupId>
385+
<artifactId>iceberg-aws</artifactId>
386+
<version>${iceberg.version}</version>
387+
<scope>test</scope>
388+
</dependency>
367389
<dependency>
368390
<groupId>org.apache.tez</groupId>
369391
<artifactId>tez-tests</artifactId>
@@ -496,6 +518,12 @@
496518
</exclusion>
497519
</exclusions>
498520
</dependency>
521+
<dependency>
522+
<groupId>software.amazon.awssdk</groupId>
523+
<artifactId>bundle</artifactId>
524+
<version>${aws-java-sdk.version}</version>
525+
<scope>test</scope>
526+
</dependency>
499527
<dependency>
500528
<groupId>org.testcontainers</groupId>
501529
<artifactId>testcontainers</artifactId>

0 commit comments

Comments
 (0)