Skip to content

Commit d5b3a1a

Browse files
committed
refactor of allowed locations check
1 parent 4366192 commit d5b3a1a

File tree

7 files changed

+102
-77
lines changed

7 files changed

+102
-77
lines changed

integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisS3RemoteSigningIntegrationTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,20 @@
2020

2121
import static org.apache.polaris.core.storage.StorageAccessProperty.AWS_ENDPOINT;
2222
import static org.apache.polaris.core.storage.StorageAccessProperty.AWS_PATH_STYLE_ACCESS;
23+
import static org.assertj.core.api.Assertions.assertThat;
2324
import static org.assertj.core.api.Assertions.assertThatThrownBy;
25+
import static org.assertj.core.api.InstanceOfAssertFactories.type;
2426

2527
import com.google.common.collect.ImmutableMap;
2628
import java.util.List;
2729
import java.util.Map;
2830
import java.util.Optional;
31+
import org.apache.iceberg.BaseTable;
2932
import org.apache.iceberg.PartitionSpec;
33+
import org.apache.iceberg.Table;
3034
import org.apache.iceberg.TableMetadata;
3135
import org.apache.iceberg.TableMetadataParser;
36+
import org.apache.iceberg.Transaction;
3237
import org.apache.iceberg.catalog.Namespace;
3338
import org.apache.iceberg.catalog.TableIdentifier;
3439
import org.apache.iceberg.exceptions.ForbiddenException;
@@ -87,6 +92,11 @@ protected Optional<String> endpoint() {
8792
return Optional.empty();
8893
}
8994

95+
/**
96+
* A set of allowed locations to include in the {@linkplain #getStorageConfigInfo() storage
97+
* configuration info}. The first allowed location will serve as the base for the catalog default
98+
* location.
99+
*/
90100
protected abstract List<String> allowedLocations();
91101

92102
protected boolean stsUnavailable() {
@@ -136,6 +146,48 @@ public void testExternalCatalogRemoteSigningDisabled() {
136146
}
137147
}
138148

149+
@CatalogConfig(
150+
properties = {
151+
"polaris.config.default-table-location-object-storage-prefix.enabled",
152+
"true",
153+
"polaris.config.allow.overlapping.table.location",
154+
"true"
155+
})
156+
@Test
157+
void testCreateTableWithObjectStoragePrefix() {
158+
@SuppressWarnings("resource")
159+
RESTCatalog restCatalog = catalog();
160+
restCatalog.createNamespace(NS);
161+
// Only direct table creation is supported with object storage prefix
162+
Table tbl1 = restCatalog.buildTable(TABLE, SCHEMA).create();
163+
// Will trigger write sign requests for manifests and snapshots, using object storage prefix
164+
tbl1.newFastAppend().appendFile(FILE_A).commit();
165+
// Will trigger many read sign requests for metadata and manifests
166+
assertFiles(tbl1, FILE_A);
167+
assertThat(tbl1).isNotNull();
168+
}
169+
170+
@CatalogConfig(properties = {"polaris.config.allow.unstructured.table.location", "true"})
171+
@Test
172+
public void testCreateTableWithCustomLocation() {
173+
@SuppressWarnings("resource")
174+
RESTCatalog restCatalog = catalog();
175+
restCatalog.createNamespace(NS);
176+
String customLocation = allowedLocations().getFirst() + "/custom/tbl1";
177+
Transaction create =
178+
restCatalog.buildTable(TABLE, SCHEMA).withLocation(customLocation).createTransaction();
179+
// Will trigger write sign requests for manifests and snapshots, before the table is created
180+
create.newFastAppend().appendFile(FILE_A).commit();
181+
// Will trigger table creation, then many read sign requests for metadata and manifests
182+
create.commitTransaction();
183+
Table tbl1 = restCatalog.loadTable(TABLE);
184+
assertFiles(tbl1, FILE_A);
185+
assertThat(tbl1)
186+
.isNotNull()
187+
.asInstanceOf(type(BaseTable.class))
188+
.returns(customLocation, BaseTable::location);
189+
}
190+
139191
@Test
140192
@Override
141193
@Disabled("It's not possible to request an access delegation mode when registering a table.")

polaris-core/src/main/java/org/apache/polaris/core/storage/LocationRestrictions.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.polaris.core.storage;
2020

2121
import jakarta.annotation.Nonnull;
22+
import java.util.Collection;
2223
import java.util.List;
2324
import java.util.Set;
2425
import org.apache.iceberg.catalog.TableIdentifier;
@@ -41,7 +42,7 @@ public class LocationRestrictions {
4142
* <p>All locations in this list have been validated to conform to the storage type's URI scheme
4243
* requirements during construction.
4344
*/
44-
private final List<String> allowedLocations;
45+
private final Collection<String> allowedLocations;
4546

4647
/**
4748
* The parent location for structured table enforcement.
@@ -54,15 +55,23 @@ public class LocationRestrictions {
5455

5556
public LocationRestrictions(
5657
@Nonnull PolarisStorageConfigurationInfo storageConfigurationInfo, String parentLocation) {
57-
this.allowedLocations = storageConfigurationInfo.getAllowedLocations();
58+
this(storageConfigurationInfo.getAllowedLocations(), parentLocation);
5859
allowedLocations.forEach(storageConfigurationInfo::validatePrefixForStorageType);
59-
this.parentLocation = parentLocation;
6060
}
6161

6262
public LocationRestrictions(@Nonnull PolarisStorageConfigurationInfo storageConfigurationInfo) {
6363
this(storageConfigurationInfo, null);
6464
}
6565

66+
public LocationRestrictions(Collection<String> allowedLocations, String parentLocation) {
67+
this.allowedLocations = allowedLocations;
68+
this.parentLocation = parentLocation;
69+
}
70+
71+
public LocationRestrictions(Collection<String> allowedLocations) {
72+
this(allowedLocations, null);
73+
}
74+
6675
/**
6776
* Validates that the requested storage locations are permitted for the given table identifier.
6877
*
@@ -94,7 +103,7 @@ public void validate(
94103

95104
private void validateLocations(
96105
RealmConfig realmConfig,
97-
List<String> allowedLocations,
106+
Collection<String> allowedLocations,
98107
Set<String> requestLocations,
99108
TableIdentifier identifier) {
100109
var validationResults =

polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public static PolarisStorageConfigurationInfo deserialize(final @Nonnull String
114114
}
115115
}
116116

117-
public static Optional<LocationRestrictions> forEntityPath(
117+
public static Optional<LocationRestrictions> getLocationRestrictionsForEntityPath(
118118
RealmConfig realmConfig, List<PolarisEntity> entityPath) {
119119
return findStorageInfoFromHierarchy(entityPath)
120120
.map(

runtime/service/src/intTest/java/org/apache/polaris/service/it/S3RemoteSigningMinIOIT.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ static void setup(
7171

7272
@Override
7373
protected List<String> allowedLocations() {
74-
return List.of(storageBase.toString());
74+
return List.of(
75+
storageBase.resolve(BUCKET_URI_PREFIX + "/allowed-location1").toString(),
76+
storageBase.resolve(BUCKET_URI_PREFIX + "/allowed-location2").toString());
7577
}
7678

7779
@Override

runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public static void validateLocationsForTableLike(
7171
Set<String> locations,
7272
PolarisResolvedPathWrapper resolvedStorageEntity) {
7373

74-
PolarisStorageConfigurationInfo.forEntityPath(
74+
PolarisStorageConfigurationInfo.getLocationRestrictionsForEntityPath(
7575
realmConfig, resolvedStorageEntity.getRawFullPath())
7676
.ifPresentOrElse(
7777
restrictions -> restrictions.validate(realmConfig, identifier, locations),

runtime/service/src/main/java/org/apache/polaris/service/storage/s3/sign/S3RemoteSigningCatalogHandler.java

Lines changed: 29 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,8 @@
1818
*/
1919
package org.apache.polaris.service.storage.s3.sign;
2020

21-
import java.util.Collection;
2221
import java.util.EnumSet;
23-
import java.util.Optional;
2422
import java.util.Set;
25-
import org.apache.iceberg.BaseTable;
26-
import org.apache.iceberg.Table;
27-
import org.apache.iceberg.catalog.Catalog;
2823
import org.apache.iceberg.catalog.TableIdentifier;
2924
import org.apache.iceberg.exceptions.ForbiddenException;
3025
import org.apache.polaris.core.PolarisDiagnostics;
@@ -35,17 +30,16 @@
3530
import org.apache.polaris.core.config.RealmConfig;
3631
import org.apache.polaris.core.context.CallContext;
3732
import org.apache.polaris.core.entity.CatalogEntity;
38-
import org.apache.polaris.core.entity.PolarisEntity;
39-
import org.apache.polaris.core.entity.PolarisEntityConstants;
33+
import org.apache.polaris.core.entity.PolarisEntitySubType;
34+
import org.apache.polaris.core.entity.PolarisEntityType;
35+
import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
36+
import org.apache.polaris.core.entity.table.TableLikeEntity;
4037
import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
4138
import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
42-
import org.apache.polaris.core.storage.InMemoryStorageIntegration;
43-
import org.apache.polaris.core.storage.PolarisStorageActions;
44-
import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
39+
import org.apache.polaris.core.storage.LocationRestrictions;
4540
import org.apache.polaris.core.storage.StorageUtil;
4641
import org.apache.polaris.service.catalog.common.CatalogHandler;
4742
import org.apache.polaris.service.catalog.common.CatalogUtils;
48-
import org.apache.polaris.service.catalog.io.FileIOUtil;
4943
import org.apache.polaris.service.context.catalog.CallContextCatalogFactory;
5044
import org.apache.polaris.service.s3.sign.model.PolarisS3SignRequest;
5145
import org.apache.polaris.service.s3.sign.model.PolarisS3SignResponse;
@@ -60,7 +54,6 @@ public class S3RemoteSigningCatalogHandler extends CatalogHandler implements Aut
6054
private final S3RequestSigner s3RequestSigner;
6155

6256
private CatalogEntity catalogEntity;
63-
private Catalog baseCatalog;
6457

6558
public S3RemoteSigningCatalogHandler(
6659
PolarisDiagnostics diagnostics,
@@ -92,7 +85,7 @@ protected void initializeCatalog() {
9285
if (catalogEntity.isExternal()) {
9386
throw new ForbiddenException("Cannot use S3 remote signing with federated catalogs.");
9487
}
95-
baseCatalog = catalogFactory.createCallContextCatalog(resolutionManifest);
88+
// no need to materialize the catalog here, as we only need the catalog entity
9689
}
9790

9891
public PolarisS3SignResponse signS3Request(
@@ -107,76 +100,43 @@ public PolarisS3SignResponse signS3Request(
107100

108101
authorizeRemoteSigningOrThrow(EnumSet.of(authzOp), tableIdentifier);
109102

110-
// Must be done after the authorization check, as the auth check creates the catalog entity
103+
// Must be done after the authorization check, as the auth check creates the catalog entity;
104+
// also, materializing the catalog here could hurt performance.
111105
throwIfRemoteSigningNotEnabled(callContext.getRealmConfig(), catalogEntity);
112106

113-
var result =
114-
InMemoryStorageIntegration.validateAllowedLocations(
115-
callContext.getRealmConfig(),
116-
getAllowedLocations(tableIdentifier),
117-
getStorageActions(s3SignRequest),
118-
getTargetLocations(s3SignRequest));
119-
120-
if (result.values().stream().anyMatch(r -> r.values().stream().anyMatch(v -> !v.isSuccess()))) {
121-
throw new ForbiddenException("Requested S3 location is not allowed.");
122-
}
107+
validateLocations(s3SignRequest, tableIdentifier);
123108

124109
PolarisS3SignResponse s3SignResponse = s3RequestSigner.signRequest(s3SignRequest);
125110
LOGGER.debug("S3 signing response: {}", s3SignResponse);
126111

127112
return s3SignResponse;
128113
}
129114

130-
// TODO M2 computing allowed locations is expensive. We should cache it.
131-
private Collection<String> getAllowedLocations(TableIdentifier tableIdentifier) {
132-
133-
if (baseCatalog.tableExists(tableIdentifier)) {
134-
135-
// If the table exists, get allowed locations from the table metadata
136-
Table table = baseCatalog.loadTable(tableIdentifier);
137-
if (table instanceof BaseTable baseTable) {
138-
return StorageUtil.getLocationsUsedByTable(baseTable.operations().current());
139-
}
140-
141-
throw new ForbiddenException("No storage configuration found for table.");
115+
private void validateLocations(
116+
PolarisS3SignRequest s3SignRequest, TableIdentifier tableIdentifier) {
142117

118+
// Will point to the table entity if it exists, otherwise the namespace entity.
119+
PolarisResolvedPathWrapper tableOrNamespace =
120+
CatalogUtils.findResolvedStorageEntity(resolutionManifest, tableIdentifier);
121+
122+
Set<String> targetLocations = getTargetLocations(s3SignRequest);
123+
124+
// If the table exists already, validate the target locations against the table's locations;
125+
// otherwise, validate against the namespace's locations using the entity path hierarchy.
126+
if (tableOrNamespace.getRawLeafEntity().getType() == PolarisEntityType.TABLE_LIKE
127+
&& tableOrNamespace.getRawLeafEntity().getSubType() == PolarisEntitySubType.ICEBERG_TABLE) {
128+
TableLikeEntity tableEntity = new IcebergTableLikeEntity(tableOrNamespace.getRawLeafEntity());
129+
Set<String> allowedLocations =
130+
StorageUtil.getLocationsUsedByTable(
131+
tableEntity.getBaseLocation(), tableEntity.getPropertiesAsMap());
132+
new LocationRestrictions(allowedLocations)
133+
.validate(callContext.getRealmConfig(), tableIdentifier, targetLocations);
143134
} else {
144-
145-
// If the table or view doesn't exist, the engine might be writing the manifests before the
146-
// table creation is committed. In this case, we still need to check allowed locations from
147-
// the parent entities.
148-
149-
PolarisResolvedPathWrapper resolvedPath =
150-
CatalogUtils.findResolvedStorageEntity(resolutionManifest, tableIdentifier);
151-
152-
Optional<PolarisEntity> storageInfo = FileIOUtil.findStorageInfoFromHierarchy(resolvedPath);
153-
154-
var configurationInfo =
155-
storageInfo
156-
.map(PolarisEntity::getInternalPropertiesAsMap)
157-
.map(info -> info.get(PolarisEntityConstants.getStorageConfigInfoPropertyName()))
158-
.map(PolarisStorageConfigurationInfo::deserialize);
159-
160-
if (configurationInfo.isEmpty()) {
161-
throw new ForbiddenException("No storage configuration found for table.");
162-
}
163-
164-
return configurationInfo.get().getAllowedLocations();
135+
CatalogUtils.validateLocationsForTableLike(
136+
callContext.getRealmConfig(), tableIdentifier, targetLocations, tableOrNamespace);
165137
}
166138
}
167139

168-
private Set<PolarisStorageActions> getStorageActions(PolarisS3SignRequest s3SignRequest) {
169-
// TODO M2: better mapping of request URIs to storage actions.
170-
// Disambiguate LIST vs READ or WRITE vs DELETE is not possible based on the HTTP method alone.
171-
// Examples:
172-
// - ListObjects is conceptually a LIST operation, and GetObject is conceptually a READ. But
173-
// both requests use the GET method.
174-
// - DeleteObject uses the DELETE method, but the DeleteObjects operation uses the POST method.
175-
return s3SignRequest.write()
176-
? Set.of(PolarisStorageActions.WRITE)
177-
: Set.of(PolarisStorageActions.READ);
178-
}
179-
180140
private Set<String> getTargetLocations(PolarisS3SignRequest s3SignRequest) {
181141
// TODO M2: map http URI to s3 URI
182142
return Set.of();

runtime/service/src/test/java/org/apache/polaris/service/it/S3RemoteSigningMinIOIntegrationTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ static void setup(
7272

7373
@Override
7474
protected List<String> allowedLocations() {
75-
return List.of(storageBase.toString());
75+
return List.of(
76+
storageBase.resolve(BUCKET_URI_PREFIX + "/allowed-location1").toString(),
77+
storageBase.resolve(BUCKET_URI_PREFIX + "/allowed-location2").toString());
7678
}
7779

7880
@Override

0 commit comments

Comments
 (0)