Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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.polaris.service.dropwizard;

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import jakarta.ws.rs.core.SecurityContext;
import java.security.Principal;
import java.time.Clock;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.PolarisDiagnostics;
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
import org.apache.polaris.core.auth.PolarisAuthorizer;
import org.apache.polaris.core.context.CallContext;
import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.core.entity.PolarisEntity;
import org.apache.polaris.core.entity.PrincipalEntity;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.PolarisMetaStoreSession;
import org.apache.polaris.core.persistence.cache.EntityCache;
import org.apache.polaris.service.admin.PolarisServiceImpl;
import org.apache.polaris.service.admin.api.PolarisCatalogsApi;
import org.apache.polaris.service.catalog.IcebergCatalogAdapter;
import org.apache.polaris.service.catalog.api.IcebergRestCatalogApi;
import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService;
import org.apache.polaris.service.catalog.io.FileIOFactory;
import org.apache.polaris.service.config.DefaultConfigurationStore;
import org.apache.polaris.service.config.RealmEntityManagerFactory;
import org.apache.polaris.service.context.CallContextCatalogFactory;
import org.apache.polaris.service.context.PolarisCallContextCatalogFactory;
import org.apache.polaris.service.dropwizard.catalog.io.TestFileIOFactory;
import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl;
import org.apache.polaris.service.task.TaskExecutor;
import org.mockito.Mockito;

public record TestServices(
IcebergRestCatalogApi restApi,
PolarisCatalogsApi catalogsApi,
RealmContext realmContext,
SecurityContext securityContext) {
private static final RealmContext testRealm = () -> "test-realm";

public static TestServices inMemory(Map<String, Object> config) {
return inMemory(new TestFileIOFactory(), config);
}

public static TestServices inMemory(FileIOFactory ioFactory) {
return inMemory(ioFactory, Map.of());
}

public static TestServices inMemory(FileIOFactory ioFactory, Map<String, Object> config) {
InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory =
new InMemoryPolarisMetaStoreManagerFactory();
metaStoreManagerFactory.setStorageIntegrationProvider(
new PolarisStorageIntegrationProviderImpl(
Mockito::mock, () -> GoogleCredentials.create(new AccessToken("abc", new Date()))));

PolarisMetaStoreManager metaStoreManager =
metaStoreManagerFactory.getOrCreateMetaStoreManager(testRealm);

EntityCache cache = new EntityCache(metaStoreManager);
RealmEntityManagerFactory realmEntityManagerFactory =
new RealmEntityManagerFactory(metaStoreManagerFactory, () -> cache) {};
CallContextCatalogFactory callContextFactory =
new PolarisCallContextCatalogFactory(
realmEntityManagerFactory,
metaStoreManagerFactory,
Mockito.mock(TaskExecutor.class),
ioFactory);
PolarisAuthorizer authorizer = Mockito.mock(PolarisAuthorizer.class);
IcebergRestCatalogApiService service =
new IcebergCatalogAdapter(
callContextFactory, realmEntityManagerFactory, metaStoreManagerFactory, authorizer);
IcebergRestCatalogApi restApi = new IcebergRestCatalogApi(service);

PolarisMetaStoreSession session =
metaStoreManagerFactory.getOrCreateSessionSupplier(testRealm).get();
PolarisCallContext context =
new PolarisCallContext(
session,
Mockito.mock(PolarisDiagnostics.class),
new DefaultConfigurationStore(config),
Clock.systemDefaultZone());
PolarisMetaStoreManager.CreatePrincipalResult createdPrincipal =
metaStoreManager.createPrincipal(
context,
new PrincipalEntity.Builder()
.setName("test-principal")
.setCreateTimestamp(Instant.now().toEpochMilli())
.setCredentialRotationRequiredState()
.build());

AuthenticatedPolarisPrincipal principal =
new AuthenticatedPolarisPrincipal(
PolarisEntity.of(createdPrincipal.getPrincipal()), Set.of());

SecurityContext securityContext =
new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return principal;
}

@Override
public boolean isUserInRole(String s) {
return false;
}

@Override
public boolean isSecure() {
return true;
}

@Override
public String getAuthenticationScheme() {
return "";
}
};

PolarisCatalogsApi catalogsApi =
new PolarisCatalogsApi(
new PolarisServiceImpl(realmEntityManagerFactory, metaStoreManagerFactory, authorizer));

CallContext.setCurrentContext(CallContext.of(testRealm, context));
return new TestServices(restApi, catalogsApi, testRealm, securityContext);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,77 +18,36 @@
*/
package org.apache.polaris.service.dropwizard.admin;

import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import io.dropwizard.testing.ConfigOverride;
import io.dropwizard.testing.ResourceHelpers;
import io.dropwizard.testing.junit5.DropwizardAppExtension;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.iceberg.exceptions.ValidationException;
import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
import org.apache.polaris.core.admin.model.Catalog;
import org.apache.polaris.core.admin.model.CatalogProperties;
import org.apache.polaris.core.admin.model.CreateCatalogRequest;
import org.apache.polaris.core.admin.model.StorageConfigInfo;
import org.apache.polaris.service.dropwizard.PolarisApplication;
import org.apache.polaris.service.dropwizard.config.PolarisApplicationConfig;
import org.apache.polaris.service.dropwizard.test.PolarisConnectionExtension;
import org.apache.polaris.service.dropwizard.test.PolarisRealm;
import org.apache.polaris.service.dropwizard.test.TestEnvironmentExtension;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
import org.apache.polaris.service.dropwizard.TestServices;
import org.apache.polaris.service.dropwizard.catalog.io.TestFileIOFactory;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

@ExtendWith({
DropwizardExtensionsSupport.class,
TestEnvironmentExtension.class,
PolarisConnectionExtension.class
})
public class PolarisOverlappingCatalogTest {
private static final DropwizardAppExtension<PolarisApplicationConfig> EXT =
new DropwizardAppExtension<>(
PolarisApplication.class,
ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"),
// Bind to random port to support parallelism
ConfigOverride.config("server.applicationConnectors[0].port", "0"),
ConfigOverride.config("server.adminConnectors[0].port", "0"),
// Block overlapping catalog paths:
ConfigOverride.config("featureConfiguration.ALLOW_OVERLAPPING_CATALOG_URLS", "false"));
private static String userToken;
private static String realm;

@BeforeAll
public static void setup(
PolarisConnectionExtension.PolarisToken adminToken, @PolarisRealm String polarisRealm)
throws IOException {
userToken = adminToken.token();
realm = polarisRealm;

// Set up the database location
PolarisConnectionExtension.createTestDir(realm);
}

static TestServices services =
TestServices.inMemory(
new TestFileIOFactory(), Map.of("ALLOW_OVERLAPPING_CATALOG_URLS", "false"));

private Response createCatalog(String prefix, String defaultBaseLocation, boolean isExternal) {
return createCatalog(prefix, defaultBaseLocation, isExternal, new ArrayList<String>());
}

private static Invocation.Builder request() {
return EXT.client()
.target(String.format("http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort()))
.request("application/json")
.header("Authorization", "Bearer " + userToken)
.header(REALM_PROPERTY_KEY, realm);
}

private Response createCatalog(
String prefix,
String defaultBaseLocation,
Expand Down Expand Up @@ -118,9 +77,10 @@ private Response createCatalog(
System.currentTimeMillis(),
1,
config);
try (Response response = request().post(Entity.json(new CreateCatalogRequest(catalog)))) {
return response;
}
return services
.catalogsApi()
.createCatalog(
new CreateCatalogRequest(catalog), services.realmContext(), services.securityContext());
}

@ParameterizedTest
Expand All @@ -144,12 +104,14 @@ public void testBasicOverlappingCatalogs(boolean initiallyExternal, boolean late
.returns(Response.Status.CREATED.getStatusCode(), Response::getStatus);

// inside `root`
assertThat(createCatalog(prefix, "root/child", laterExternal))
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
assertThatThrownBy(() -> createCatalog(prefix, "root/child", laterExternal))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");

// `root` is inside this
assertThat(createCatalog(prefix, "", laterExternal))
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
assertThatThrownBy(() -> createCatalog(prefix, "", laterExternal))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");
}

@ParameterizedTest
Expand All @@ -166,17 +128,24 @@ public void testAllowedLocationOverlappingCatalogs(
.returns(Response.Status.CREATED.getStatusCode(), Response::getStatus);

// This DBL overlaps with initial AL
assertThat(createCatalog(prefix, "dogs", initiallyExternal, Arrays.asList("huskies", "labs")))
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
assertThatThrownBy(
() ->
createCatalog(prefix, "dogs", initiallyExternal, Arrays.asList("huskies", "labs")))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");

// This AL overlaps with initial DBL
assertThat(
createCatalog(
prefix, "kingdoms", initiallyExternal, Arrays.asList("plants", "animals")))
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
assertThatThrownBy(
() ->
createCatalog(
prefix, "kingdoms", initiallyExternal, Arrays.asList("plants", "animals")))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");

// This AL overlaps with an initial AL
assertThat(createCatalog(prefix, "plays", initiallyExternal, Arrays.asList("rent", "cats")))
.returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus);
assertThatThrownBy(
() -> createCatalog(prefix, "plays", initiallyExternal, Arrays.asList("rent", "cats")))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("One or more of its locations overlaps with an existing catalog");
}
}
Loading
Loading