1818 */
1919package org .apache .polaris .service .storage .s3 .sign ;
2020
21- import java .util .Collection ;
2221import java .util .EnumSet ;
23- import java .util .Optional ;
2422import java .util .Set ;
25- import org .apache .iceberg .BaseTable ;
26- import org .apache .iceberg .Table ;
27- import org .apache .iceberg .catalog .Catalog ;
2823import org .apache .iceberg .catalog .TableIdentifier ;
2924import org .apache .iceberg .exceptions .ForbiddenException ;
3025import org .apache .polaris .core .PolarisDiagnostics ;
3530import org .apache .polaris .core .config .RealmConfig ;
3631import org .apache .polaris .core .context .CallContext ;
3732import 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 ;
4037import org .apache .polaris .core .persistence .PolarisResolvedPathWrapper ;
4138import 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 ;
4540import org .apache .polaris .core .storage .StorageUtil ;
4641import org .apache .polaris .service .catalog .common .CatalogHandler ;
4742import org .apache .polaris .service .catalog .common .CatalogUtils ;
48- import org .apache .polaris .service .catalog .io .FileIOUtil ;
4943import org .apache .polaris .service .context .catalog .CallContextCatalogFactory ;
5044import org .apache .polaris .service .s3 .sign .model .PolarisS3SignRequest ;
5145import 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 ();
0 commit comments