|
23 | 23 | import static org.assertj.core.api.Assertions.assertThatThrownBy; |
24 | 24 | import static org.mockito.ArgumentMatchers.any; |
25 | 25 | import static org.mockito.ArgumentMatchers.eq; |
| 26 | +import static org.mockito.Mockito.doThrow; |
26 | 27 | import static org.mockito.Mockito.mock; |
| 28 | +import static org.mockito.Mockito.never; |
| 29 | +import static org.mockito.Mockito.verify; |
27 | 30 | import static org.mockito.Mockito.when; |
28 | 31 |
|
29 | 32 | import java.util.List; |
| 33 | +import java.util.Map; |
| 34 | +import java.util.Optional; |
30 | 35 | import org.apache.iceberg.catalog.Namespace; |
31 | 36 | import org.apache.iceberg.catalog.TableIdentifier; |
| 37 | +import org.apache.iceberg.exceptions.AlreadyExistsException; |
32 | 38 | import org.apache.iceberg.exceptions.NoSuchTableException; |
33 | 39 | import org.apache.iceberg.exceptions.NotFoundException; |
34 | 40 | import org.apache.polaris.core.PolarisCallContext; |
| 41 | +import org.apache.polaris.core.admin.model.AuthenticationParameters; |
| 42 | +import org.apache.polaris.core.admin.model.AwsStorageConfigInfo; |
| 43 | +import org.apache.polaris.core.admin.model.Catalog; |
| 44 | +import org.apache.polaris.core.admin.model.CatalogProperties; |
| 45 | +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; |
| 46 | +import org.apache.polaris.core.admin.model.CreateCatalogRequest; |
| 47 | +import org.apache.polaris.core.admin.model.ExternalCatalog; |
| 48 | +import org.apache.polaris.core.admin.model.IcebergRestConnectionConfigInfo; |
| 49 | +import org.apache.polaris.core.admin.model.OAuthClientCredentialsParameters; |
| 50 | +import org.apache.polaris.core.admin.model.StorageConfigInfo; |
35 | 51 | import org.apache.polaris.core.auth.PolarisAuthorizer; |
36 | 52 | import org.apache.polaris.core.auth.PolarisPrincipal; |
37 | 53 | import org.apache.polaris.core.catalog.PolarisCatalogHelpers; |
| 54 | +import org.apache.polaris.core.config.BehaviorChangeConfiguration; |
38 | 55 | import org.apache.polaris.core.config.FeatureConfiguration; |
39 | 56 | import org.apache.polaris.core.config.RealmConfig; |
40 | 57 | import org.apache.polaris.core.context.CallContext; |
41 | 58 | import org.apache.polaris.core.entity.CatalogEntity; |
42 | 59 | import org.apache.polaris.core.entity.NamespaceEntity; |
| 60 | +import org.apache.polaris.core.entity.PolarisBaseEntity; |
43 | 61 | import org.apache.polaris.core.entity.PolarisEntity; |
| 62 | +import org.apache.polaris.core.entity.PolarisEntityConstants; |
44 | 63 | import org.apache.polaris.core.entity.PolarisEntitySubType; |
45 | 64 | import org.apache.polaris.core.entity.PolarisEntityType; |
46 | 65 | import org.apache.polaris.core.entity.PolarisPrivilege; |
|
49 | 68 | import org.apache.polaris.core.persistence.PolarisMetaStoreManager; |
50 | 69 | import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; |
51 | 70 | import org.apache.polaris.core.persistence.dao.entity.BaseResult; |
| 71 | +import org.apache.polaris.core.persistence.dao.entity.CreateCatalogResult; |
52 | 72 | import org.apache.polaris.core.persistence.dao.entity.EntityResult; |
53 | 73 | import org.apache.polaris.core.persistence.dao.entity.GenerateEntityIdResult; |
54 | 74 | import org.apache.polaris.core.persistence.dao.entity.PrivilegeResult; |
55 | 75 | import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; |
56 | 76 | import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; |
57 | 77 | import org.apache.polaris.core.persistence.resolver.ResolvedPathKey; |
58 | 78 | import org.apache.polaris.core.persistence.resolver.ResolverStatus; |
| 79 | +import org.apache.polaris.core.secrets.SecretReference; |
59 | 80 | import org.apache.polaris.core.secrets.UserSecretsManager; |
60 | 81 | import org.apache.polaris.service.config.ReservedProperties; |
61 | 82 | import org.assertj.core.api.Assertions; |
@@ -116,6 +137,62 @@ protected static void assertSuccess(BaseResult result) { |
116 | 137 | Assertions.assertThat(result.isSuccess()).isTrue(); |
117 | 138 | } |
118 | 139 |
|
| 140 | + @Test |
| 141 | + void testCreateCatalogCleansUpInlineSecretsWhenCatalogAlreadyExists() { |
| 142 | + SecretReference secretReference = |
| 143 | + new SecretReference("urn:polaris-secret:test:oauth", Map.of()); |
| 144 | + setupExternalCatalogCreate(secretReference); |
| 145 | + when(metaStoreManager.createCatalog(any(), any(), any())) |
| 146 | + .thenReturn(new CreateCatalogResult(BaseResult.ReturnStatus.ENTITY_ALREADY_EXISTS, null)); |
| 147 | + |
| 148 | + assertThatThrownBy( |
| 149 | + () -> |
| 150 | + adminService.createCatalog(new CreateCatalogRequest(createExternalOauthCatalog()))) |
| 151 | + .isInstanceOf(AlreadyExistsException.class); |
| 152 | + |
| 153 | + verify(userSecretsManager).deleteSecret(secretReference); |
| 154 | + } |
| 155 | + |
| 156 | + @Test |
| 157 | + void testCreateCatalogCleanupFailureDoesNotHideOriginalFailure() { |
| 158 | + SecretReference secretReference = |
| 159 | + new SecretReference("urn:polaris-secret:test:oauth", Map.of()); |
| 160 | + setupExternalCatalogCreate(secretReference); |
| 161 | + CreateCatalogResult resultWithError = |
| 162 | + new CreateCatalogResult( |
| 163 | + BaseResult.ReturnStatus.UNEXPECTED_ERROR_SIGNALED, "Unexpected Error Occurred"); |
| 164 | + when(metaStoreManager.createCatalog(any(), any(), any())).thenReturn(resultWithError); |
| 165 | + doThrow(new RuntimeException("cleanup failed")).when(userSecretsManager).deleteSecret(any()); |
| 166 | + |
| 167 | + assertThatThrownBy( |
| 168 | + () -> |
| 169 | + adminService.createCatalog(new CreateCatalogRequest(createExternalOauthCatalog()))) |
| 170 | + .isInstanceOf(IllegalStateException.class) |
| 171 | + .hasMessage( |
| 172 | + String.format( |
| 173 | + "Cannot create Catalog %s: %s with extraInfo %s", |
| 174 | + "external-catalog", |
| 175 | + resultWithError.getReturnStatus(), |
| 176 | + resultWithError.getExtraInformation())); |
| 177 | + |
| 178 | + verify(userSecretsManager).deleteSecret(secretReference); |
| 179 | + } |
| 180 | + |
| 181 | + @Test |
| 182 | + void testCreateCatalogDoesNotCleanUpInlineSecretsOnSuccess() { |
| 183 | + SecretReference secretReference = |
| 184 | + new SecretReference("urn:polaris-secret:test:oauth", Map.of()); |
| 185 | + setupExternalCatalogCreate(secretReference); |
| 186 | + when(metaStoreManager.createCatalog(any(), any(), any())) |
| 187 | + .thenReturn( |
| 188 | + new CreateCatalogResult( |
| 189 | + createBaseCatalog("external-catalog"), createBaseCatalogRole())); |
| 190 | + |
| 191 | + adminService.createCatalog(new CreateCatalogRequest(createExternalOauthCatalog())); |
| 192 | + |
| 193 | + verify(userSecretsManager, never()).deleteSecret(any()); |
| 194 | + } |
| 195 | + |
119 | 196 | @Test |
120 | 197 | void testGrantPrivilegeOnNamespaceToRole() throws Exception { |
121 | 198 | String catalogName = "test-catalog"; |
@@ -677,6 +754,77 @@ private PolarisEntity createEntity(String name, PolarisEntityType type) { |
677 | 754 | .build(); |
678 | 755 | } |
679 | 756 |
|
| 757 | + private void setupExternalCatalogCreate(SecretReference secretReference) { |
| 758 | + when(realmConfig.getConfig(FeatureConfiguration.ALLOW_OVERLAPPING_CATALOG_URLS)) |
| 759 | + .thenReturn(true); |
| 760 | + when(realmConfig.getConfig(BehaviorChangeConfiguration.STORAGE_CONFIGURATION_MAX_LOCATIONS)) |
| 761 | + .thenReturn(-1); |
| 762 | + when(realmConfig.getConfig(FeatureConfiguration.ENABLE_CATALOG_FEDERATION)).thenReturn(true); |
| 763 | + when(realmConfig.getConfig( |
| 764 | + FeatureConfiguration.SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES)) |
| 765 | + .thenReturn(List.of(AuthenticationParameters.AuthenticationTypeEnum.OAUTH.name())); |
| 766 | + |
| 767 | + GenerateEntityIdResult idResult = mock(GenerateEntityIdResult.class); |
| 768 | + when(idResult.getId()).thenReturn(2L); |
| 769 | + when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); |
| 770 | + when(reservedProperties.removeReservedProperties(Mockito.<Map<String, String>>any())) |
| 771 | + .thenReturn(Map.of()); |
| 772 | + when(userSecretsManager.writeSecret(any(), any())).thenReturn(secretReference); |
| 773 | + when(identityProvider.allocateServiceIdentity(any())).thenReturn(Optional.empty()); |
| 774 | + } |
| 775 | + |
| 776 | + private Catalog createExternalOauthCatalog() { |
| 777 | + AwsStorageConfigInfo awsConfigModel = |
| 778 | + AwsStorageConfigInfo.builder() |
| 779 | + .setRoleArn("arn:aws:iam::123456789012:role/my-role") |
| 780 | + .setExternalId("externalId") |
| 781 | + .setUserArn("userArn") |
| 782 | + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) |
| 783 | + .setAllowedLocations(List.of("s3://bucket/path/to/data")) |
| 784 | + .build(); |
| 785 | + ConnectionConfigInfo connectionConfigInfo = |
| 786 | + IcebergRestConnectionConfigInfo.builder( |
| 787 | + ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) |
| 788 | + .setUri("https://example.com/polaris/api/catalog") |
| 789 | + .setRemoteCatalogName("remote-catalog") |
| 790 | + .setAuthenticationParameters( |
| 791 | + OAuthClientCredentialsParameters.builder( |
| 792 | + AuthenticationParameters.AuthenticationTypeEnum.OAUTH) |
| 793 | + .setClientId("client-id") |
| 794 | + .setClientSecret("client-secret") |
| 795 | + .setScopes(List.of("PRINCIPAL_ROLE:ALL")) |
| 796 | + .build()) |
| 797 | + .build(); |
| 798 | + |
| 799 | + return ExternalCatalog.builder() |
| 800 | + .setType(Catalog.TypeEnum.EXTERNAL) |
| 801 | + .setName("external-catalog") |
| 802 | + .setProperties(CatalogProperties.builder("s3://bucket/path/to/data").build()) |
| 803 | + .setStorageConfigInfo(awsConfigModel) |
| 804 | + .setConnectionConfigInfo(connectionConfigInfo) |
| 805 | + .build(); |
| 806 | + } |
| 807 | + |
| 808 | + private PolarisBaseEntity createBaseCatalog(String name) { |
| 809 | + return new PolarisBaseEntity( |
| 810 | + PolarisEntityConstants.getNullId(), |
| 811 | + 2L, |
| 812 | + PolarisEntityType.CATALOG, |
| 813 | + PolarisEntitySubType.NULL_SUBTYPE, |
| 814 | + PolarisEntityConstants.getRootEntityId(), |
| 815 | + name); |
| 816 | + } |
| 817 | + |
| 818 | + private PolarisBaseEntity createBaseCatalogRole() { |
| 819 | + return new PolarisBaseEntity( |
| 820 | + 2L, |
| 821 | + 3L, |
| 822 | + PolarisEntityType.CATALOG_ROLE, |
| 823 | + PolarisEntitySubType.NULL_SUBTYPE, |
| 824 | + 2L, |
| 825 | + PolarisEntityConstants.getNameOfCatalogAdminRole()); |
| 826 | + } |
| 827 | + |
680 | 828 | private PolarisEntity createEntity(String name, PolarisEntityType type, long id) { |
681 | 829 | return new PolarisEntity.Builder() |
682 | 830 | .setName(name) |
|
0 commit comments