Skip to content

Commit 112f67b

Browse files
authored
Add support for federated principal and role with block for manual role assignment (#1353)
* Add support for federated principal and role with block for manual role assignment * Update spec to distinguish federated and non-federated entities * Changed builder to allow setting federated status twice * Revert spec changes - add 'federated' property back to Principal entity * Fixed builder to remove federated property * Removed unnecessary openapi config flags * Fix compilation issue in test * Remove federated flag from principal entity * Fixed builder oversight * Fix compilation failures and rebase on main
1 parent 197ac76 commit 112f67b

File tree

8 files changed

+242
-16
lines changed

8 files changed

+242
-16
lines changed

api/iceberg-service/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ openApiGenerate {
6666
configOptions.put("useBeanValidation", "false")
6767
configOptions.put("sourceFolder", "src/main/java")
6868
configOptions.put("useJakartaEe", "true")
69-
openapiNormalizer.put("REFACTOR_ALLOF_WITH_PROPERTIES_ONLY", "true")
7069
additionalProperties.put("apiNamePrefix", "IcebergRest")
7170
additionalProperties.put("apiNameSuffix", "")
7271
additionalProperties.put("metricsPrefix", "polaris")

api/polaris-catalog-service/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ openApiGenerate {
9696
configOptions.put("generateBuilders", "true")
9797
configOptions.put("generateConstructorWithAllArgs", "true")
9898
configOptions.put("openApiNullable", "false")
99-
openapiNormalizer.put("REFACTOR_ALLOF_WITH_PROPERTIES_ONLY", "true")
10099
additionalProperties.put("apiNamePrefix", "PolarisCatalog")
101100
additionalProperties.put("apiNameSuffix", "")
102101
additionalProperties.put("metricsPrefix", "polaris")

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

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.polaris.service.it.test;
2020

21+
import static javax.ws.rs.core.Response.Status.CREATED;
2122
import static javax.ws.rs.core.Response.Status.FORBIDDEN;
2223
import static org.apache.polaris.service.it.env.PolarisClient.polarisClient;
2324
import static org.apache.polaris.service.it.test.PolarisApplicationIntegrationTest.PRINCIPAL_ROLE_ALL;
@@ -872,6 +873,27 @@ public void testCreatePrincipalAndRotateCredentials() {
872873
// rotation that makes the old secret fall off retention.
873874
}
874875

876+
@Test
877+
public void testCreateFederatedPrincipalRoleSucceeds() {
878+
// Create a federated Principal Role
879+
PrincipalRole federatedPrincipalRole =
880+
new PrincipalRole(
881+
client.newEntityName("federatedRole"),
882+
true,
883+
Map.of(),
884+
Instant.now().toEpochMilli(),
885+
Instant.now().toEpochMilli(),
886+
1);
887+
888+
// Attempt to create the federated Principal using the managementApi
889+
try (Response createResponse =
890+
managementApi
891+
.request("v1/principal-roles")
892+
.post(Entity.json(new CreatePrincipalRoleRequest(federatedPrincipalRole)))) {
893+
assertThat(createResponse).returns(CREATED.getStatusCode(), Response::getStatus);
894+
}
895+
}
896+
875897
@Test
876898
public void testCreateListUpdateAndDeletePrincipal() {
877899
Principal principal =
@@ -1023,7 +1045,7 @@ public void testGetPrincipalWithInvalidName() {
10231045
public void testCreateListUpdateAndDeletePrincipalRole() {
10241046
PrincipalRole principalRole =
10251047
new PrincipalRole(
1026-
client.newEntityName("myprincipalrole"), Map.of("custom-tag", "foo"), 0L, 0L, 1);
1048+
client.newEntityName("myprincipalrole"), false, Map.of("custom-tag", "foo"), 0L, 0L, 1);
10271049
managementApi.createPrincipalRole(principalRole);
10281050

10291051
// Second attempt to create the same entity should fail with CONFLICT.
@@ -1115,7 +1137,7 @@ public void testCreateListUpdateAndDeletePrincipalRole() {
11151137
public void testCreatePrincipalRoleInvalidName() {
11161138
String goodName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH, true, true);
11171139
PrincipalRole principalRole =
1118-
new PrincipalRole(goodName, Map.of("custom-tag", "good_principal_role"), 0L, 0L, 1);
1140+
new PrincipalRole(goodName, false, Map.of("custom-tag", "good_principal_role"), 0L, 0L, 1);
11191141
managementApi.createPrincipalRole(principalRole);
11201142

11211143
String longInvalidName = RandomStringUtils.random(MAX_IDENTIFIER_LENGTH + 1, true, true);
@@ -1131,7 +1153,12 @@ public void testCreatePrincipalRoleInvalidName() {
11311153
for (String invalidPrincipalRoleName : invalidPrincipalRoleNames) {
11321154
principalRole =
11331155
new PrincipalRole(
1134-
invalidPrincipalRoleName, Map.of("custom-tag", "bad_principal_role"), 0L, 0L, 1);
1156+
invalidPrincipalRoleName,
1157+
false,
1158+
Map.of("custom-tag", "bad_principal_role"),
1159+
0L,
1160+
0L,
1161+
1);
11351162

11361163
try (Response response =
11371164
managementApi
@@ -2154,7 +2181,12 @@ public void testCreateAndUpdatePrincipalRoleWithReservedProperties() {
21542181

21552182
PrincipalRole badPrincipalRole =
21562183
new PrincipalRole(
2157-
client.newEntityName("myprincipalrole"), Map.of("polaris.reserved", "foo"), 0L, 0L, 1);
2184+
client.newEntityName("myprincipalrole"),
2185+
false,
2186+
Map.of("polaris.reserved", "foo"),
2187+
0L,
2188+
0L,
2189+
1);
21582190
try (Response response =
21592191
managementApi
21602192
.request("v1/principal-roles")
@@ -2165,7 +2197,12 @@ public void testCreateAndUpdatePrincipalRoleWithReservedProperties() {
21652197

21662198
PrincipalRole goodPrincipalRole =
21672199
new PrincipalRole(
2168-
client.newEntityName("myprincipalrole"), Map.of("not.reserved", "foo"), 0L, 0L, 1);
2200+
client.newEntityName("myprincipalrole"),
2201+
false,
2202+
Map.of("not.reserved", "foo"),
2203+
0L,
2204+
0L,
2205+
1);
21692206
try (Response response =
21702207
managementApi
21712208
.request("v1/principal-roles")

polaris-core/src/main/java/org/apache/polaris/core/entity/PrincipalRoleEntity.java

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

2121
import org.apache.polaris.core.admin.model.PrincipalRole;
22+
import org.apache.polaris.core.entity.table.federated.FederatedEntities;
2223

2324
/**
2425
* Wrapper for translating between the REST PrincipalRole object and the base PolarisEntity type.
@@ -38,19 +39,19 @@ public static PrincipalRoleEntity of(PolarisBaseEntity sourceEntity) {
3839
public static PrincipalRoleEntity fromPrincipalRole(PrincipalRole principalRole) {
3940
return new Builder()
4041
.setName(principalRole.getName())
42+
.setFederated(principalRole.getFederated())
4143
.setProperties(principalRole.getProperties())
4244
.build();
4345
}
4446

4547
public PrincipalRole asPrincipalRole() {
46-
PrincipalRole principalRole =
47-
new PrincipalRole(
48-
getName(),
49-
getPropertiesAsMap(),
50-
getCreateTimestamp(),
51-
getLastUpdateTimestamp(),
52-
getEntityVersion());
53-
return principalRole;
48+
return new PrincipalRole(
49+
getName(),
50+
FederatedEntities.isFederated(this),
51+
getPropertiesAsMap(),
52+
getCreateTimestamp(),
53+
getLastUpdateTimestamp(),
54+
getEntityVersion());
5455
}
5556

5657
public static class Builder extends PolarisEntity.BaseBuilder<PrincipalRoleEntity, Builder> {
@@ -65,6 +66,15 @@ public Builder(PrincipalRoleEntity original) {
6566
super(original);
6667
}
6768

69+
public Builder setFederated(Boolean isFederated) {
70+
if (isFederated != null && isFederated) {
71+
internalProperties.put(FederatedEntities.FEDERATED_ENTITY, "true");
72+
} else {
73+
internalProperties.remove(FederatedEntities.FEDERATED_ENTITY);
74+
}
75+
return this;
76+
}
77+
6878
@Override
6979
public PrincipalRoleEntity build() {
7080
return new PrincipalRoleEntity(buildBase());
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.core.entity.table.federated;
20+
21+
import java.util.Optional;
22+
import org.apache.polaris.core.entity.PolarisBaseEntity;
23+
24+
public final class FederatedEntities {
25+
26+
public static final String FEDERATED_ENTITY = "federated";
27+
28+
public static boolean isFederated(PolarisBaseEntity entity) {
29+
return Optional.ofNullable(entity.getInternalPropertiesAsMap())
30+
.map(map -> Boolean.parseBoolean(map.get(FEDERATED_ENTITY)))
31+
.orElse(false);
32+
}
33+
34+
private FederatedEntities() {}
35+
}

quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/ManagementServiceTest.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2323

2424
import jakarta.ws.rs.core.Response;
25+
import jakarta.ws.rs.core.SecurityContext;
26+
import java.security.Principal;
2527
import java.time.Clock;
28+
import java.time.Instant;
2629
import java.util.List;
2730
import java.util.Map;
31+
import java.util.Set;
32+
import org.apache.iceberg.exceptions.ValidationException;
2833
import org.apache.polaris.core.PolarisCallContext;
2934
import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
3035
import org.apache.polaris.core.admin.model.Catalog;
@@ -34,8 +39,20 @@
3439
import org.apache.polaris.core.admin.model.PolarisCatalog;
3540
import org.apache.polaris.core.admin.model.StorageConfigInfo;
3641
import org.apache.polaris.core.admin.model.UpdateCatalogRequest;
42+
import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal;
43+
import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
3744
import org.apache.polaris.core.context.CallContext;
45+
import org.apache.polaris.core.context.RealmContext;
46+
import org.apache.polaris.core.entity.PrincipalEntity;
47+
import org.apache.polaris.core.entity.PrincipalRoleEntity;
48+
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
49+
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
50+
import org.apache.polaris.core.persistence.dao.entity.EntityResult;
51+
import org.apache.polaris.core.secrets.UnsafeInMemorySecretsManager;
3852
import org.apache.polaris.service.TestServices;
53+
import org.apache.polaris.service.admin.PolarisAdminService;
54+
import org.apache.polaris.service.config.DefaultConfigurationStore;
55+
import org.apache.polaris.service.config.ReservedProperties;
3956
import org.junit.jupiter.api.BeforeEach;
4057
import org.junit.jupiter.api.Test;
4158
import org.mockito.Mockito;
@@ -158,4 +175,105 @@ public void testUpdateCatalogWithDisallowedStorageConfig() {
158175
.isInstanceOf(IllegalArgumentException.class)
159176
.hasMessage("Unsupported storage type: FILE");
160177
}
178+
179+
private PolarisMetaStoreManager setupMetaStoreManager() {
180+
MetaStoreManagerFactory metaStoreManagerFactory = services.metaStoreManagerFactory();
181+
RealmContext realmContext = services.realmContext();
182+
return metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext);
183+
}
184+
185+
private PolarisCallContext setupCallContext(PolarisMetaStoreManager metaStoreManager) {
186+
MetaStoreManagerFactory metaStoreManagerFactory = services.metaStoreManagerFactory();
187+
RealmContext realmContext = services.realmContext();
188+
return new PolarisCallContext(
189+
metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(),
190+
services.polarisDiagnostics());
191+
}
192+
193+
private PolarisAdminService setupPolarisAdminService(
194+
PolarisMetaStoreManager metaStoreManager, PolarisCallContext callContext) {
195+
RealmContext realmContext = services.realmContext();
196+
return new PolarisAdminService(
197+
CallContext.of(realmContext, callContext),
198+
services.entityManagerFactory().getOrCreateEntityManager(realmContext),
199+
metaStoreManager,
200+
new UnsafeInMemorySecretsManager(),
201+
new SecurityContext() {
202+
@Override
203+
public Principal getUserPrincipal() {
204+
return new AuthenticatedPolarisPrincipal(
205+
new PrincipalEntity.Builder().setName("root").build(), Set.of("service_admin"));
206+
}
207+
208+
@Override
209+
public boolean isUserInRole(String role) {
210+
return true;
211+
}
212+
213+
@Override
214+
public boolean isSecure() {
215+
return false;
216+
}
217+
218+
@Override
219+
public String getAuthenticationScheme() {
220+
return "";
221+
}
222+
},
223+
new PolarisAuthorizerImpl(new DefaultConfigurationStore(Map.of())),
224+
new ReservedProperties() {
225+
@Override
226+
public List<String> prefixes() {
227+
return List.of();
228+
}
229+
230+
@Override
231+
public Set<String> allowlist() {
232+
return Set.of();
233+
}
234+
});
235+
}
236+
237+
private PrincipalEntity createPrincipal(
238+
PolarisMetaStoreManager metaStoreManager, PolarisCallContext callContext, String name) {
239+
return new PrincipalEntity.Builder()
240+
.setName(name)
241+
.setCreateTimestamp(Instant.now().toEpochMilli())
242+
.setId(metaStoreManager.generateNewEntityId(callContext).getId())
243+
.build();
244+
}
245+
246+
private PrincipalRoleEntity createRole(
247+
PolarisMetaStoreManager metaStoreManager,
248+
PolarisCallContext callContext,
249+
String name,
250+
boolean isFederated) {
251+
return new PrincipalRoleEntity.Builder()
252+
.setId(metaStoreManager.generateNewEntityId(callContext).getId())
253+
.setName(name)
254+
.setFederated(isFederated)
255+
.setProperties(Map.of())
256+
.setCreateTimestamp(Instant.now().toEpochMilli())
257+
.setLastUpdateTimestamp(Instant.now().toEpochMilli())
258+
.build();
259+
}
260+
261+
@Test
262+
public void testCannotAssignFederatedEntities() {
263+
PolarisMetaStoreManager metaStoreManager = setupMetaStoreManager();
264+
PolarisCallContext callContext = setupCallContext(metaStoreManager);
265+
PolarisAdminService polarisAdminService =
266+
setupPolarisAdminService(metaStoreManager, callContext);
267+
268+
PrincipalEntity principal = createPrincipal(metaStoreManager, callContext, "principal_id");
269+
metaStoreManager.createPrincipal(callContext, principal);
270+
271+
PrincipalRoleEntity role = createRole(metaStoreManager, callContext, "federated_role_id", true);
272+
EntityResult result = metaStoreManager.createEntityIfNotExists(callContext, null, role);
273+
assertThat(result.isSuccess()).isTrue();
274+
275+
assertThatThrownBy(
276+
() -> polarisAdminService.assignPrincipalRole(principal.getName(), role.getName()))
277+
.isInstanceOf(ValidationException.class);
278+
}
161279
}

0 commit comments

Comments
 (0)