Skip to content

Commit e30fc1f

Browse files
fix(calm-hub): Adding permission filtering to GET:/namespaces endpoint (#2609)
Adding in memory filtering of namespaces based on the users permissions following the pattern used in SearchResource.java.
1 parent 409d320 commit e30fc1f

2 files changed

Lines changed: 119 additions & 3 deletions

File tree

calm-hub/src/main/java/org/finos/calm/resources/NamespaceResource.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,51 @@
22

33
import io.quarkus.security.Authenticated;
44
import io.quarkus.security.PermissionsAllowed;
5+
import io.quarkus.security.identity.SecurityIdentity;
6+
import jakarta.enterprise.inject.Instance;
57
import jakarta.inject.Inject;
68
import jakarta.validation.Valid;
79
import jakarta.validation.constraints.NotNull;
810
import jakarta.ws.rs.*;
911
import jakarta.ws.rs.core.MediaType;
1012
import jakarta.ws.rs.core.Response;
13+
import org.eclipse.microprofile.config.inject.ConfigProperty;
1114
import org.eclipse.microprofile.openapi.annotations.Operation;
1215
import org.finos.calm.domain.NamespaceRequest;
1316
import org.finos.calm.domain.ValueWrapper;
1417
import org.finos.calm.domain.exception.NamespaceAlreadyExistsException;
1518
import org.finos.calm.domain.namespaces.NamespaceInfo;
1619
import org.finos.calm.security.CalmHubPermissionChecker;
1720
import org.finos.calm.security.CalmHubScopes;
21+
import org.finos.calm.security.UserAccessValidator;
1822
import org.finos.calm.store.NamespaceStore;
1923

2024
import java.net.URI;
2125
import java.net.URISyntaxException;
26+
import java.util.List;
27+
import java.util.Set;
2228

2329
@Path("/calm/namespaces")
2430
public class NamespaceResource {
2531

2632
private final NamespaceStore namespaceStore;
33+
private final Instance<UserAccessValidator> userAccessValidatorInstance;
34+
private final CalmHubPermissionChecker permissionChecker;
2735

2836
@Inject
29-
public NamespaceResource(NamespaceStore store) {
37+
SecurityIdentity identity;
38+
39+
@Inject
40+
@ConfigProperty(name = "calm.auth.enabled", defaultValue = "false")
41+
boolean authEnabled;
42+
43+
@Inject
44+
public NamespaceResource(NamespaceStore store,
45+
Instance<UserAccessValidator> userAccessValidatorInstance,
46+
CalmHubPermissionChecker permissionChecker) {
3047
this.namespaceStore = store;
48+
this.userAccessValidatorInstance = userAccessValidatorInstance;
49+
this.permissionChecker = permissionChecker;
3150
}
3251

3352
@GET
@@ -37,7 +56,17 @@ public NamespaceResource(NamespaceStore store) {
3756
)
3857
@Authenticated
3958
public ValueWrapper<NamespaceInfo> namespaces() {
40-
return new ValueWrapper<>(namespaceStore.getNamespaces());
59+
if (!authEnabled || !userAccessValidatorInstance.isResolvable()) {
60+
return new ValueWrapper<>(namespaceStore.getNamespaces());
61+
}
62+
if (permissionChecker.hasGlobalAdmin(identity)) {
63+
return new ValueWrapper<>(namespaceStore.getNamespaces());
64+
}
65+
Set<String> grants = userAccessValidatorInstance.get()
66+
.getReadableNamespaces(identity.getPrincipal().getName());
67+
return new ValueWrapper<>(namespaceStore.getNamespaces().stream()
68+
.filter(ns -> grants.contains(ns.getName()))
69+
.toList());
4170
}
4271

4372
@POST
@@ -70,4 +99,4 @@ public Response createNamespace(@Valid @NotNull(message = "Request must not be n
7099
return Response.created(new URI("/calm/namespaces/" + name)).build();
71100
}
72101

73-
}
102+
}

calm-hub/src/test/java/org/finos/calm/resources/TestNamespaceResourceShould.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
package org.finos.calm.resources;
22

3+
import io.quarkus.security.identity.SecurityIdentity;
34
import io.quarkus.test.InjectMock;
45
import io.quarkus.test.junit.QuarkusTest;
56
import io.quarkus.test.security.TestSecurity;
7+
import jakarta.enterprise.inject.Instance;
68
import org.finos.calm.domain.exception.NamespaceAlreadyExistsException;
79
import org.finos.calm.domain.namespaces.NamespaceInfo;
10+
import org.finos.calm.security.CalmHubPermissionChecker;
11+
import org.finos.calm.security.UserAccessValidator;
812
import org.finos.calm.store.NamespaceStore;
913
import org.junit.jupiter.api.Test;
1014
import org.junit.jupiter.api.extension.ExtendWith;
15+
import org.mockito.Mock;
1116
import org.mockito.junit.jupiter.MockitoExtension;
1217

18+
import java.security.Principal;
1319
import java.util.ArrayList;
1420
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.Set;
1523

1624
import static io.restassured.RestAssured.given;
1725
import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
1826
import static org.hamcrest.Matchers.containsString;
1927
import static org.hamcrest.Matchers.equalTo;
28+
import static org.junit.jupiter.api.Assertions.assertEquals;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
2030
import static org.mockito.Mockito.*;
2131

2232
@TestSecurity(authorizationEnabled = false)
@@ -27,6 +37,32 @@ public class TestNamespaceResourceShould {
2737
@InjectMock
2838
NamespaceStore namespaceStore;
2939

40+
// Plain Mockito mocks for direct-instantiation filtering tests
41+
@Mock
42+
private NamespaceStore mockNamespaceStore;
43+
@Mock
44+
private Instance<UserAccessValidator> mockValidatorInstance;
45+
@Mock
46+
private UserAccessValidator mockValidator;
47+
@Mock
48+
private SecurityIdentity mockIdentity;
49+
@Mock
50+
private Principal mockPrincipal;
51+
@Mock
52+
private CalmHubPermissionChecker mockPermissionChecker;
53+
54+
private static final List<NamespaceInfo> ALL_NAMESPACES = List.of(
55+
new NamespaceInfo("finos", "FINOS namespace"),
56+
new NamespaceInfo("custom", "custom namespace")
57+
);
58+
59+
private NamespaceResource resourceWithAuth(boolean authEnabled) {
60+
NamespaceResource resource = new NamespaceResource(mockNamespaceStore, mockValidatorInstance, mockPermissionChecker);
61+
resource.identity = mockIdentity;
62+
resource.authEnabled = authEnabled;
63+
return resource;
64+
}
65+
3066
@Test
3167
void return_empty_wrapper_when_no_namespaces_in_store() {
3268
when(namespaceStore.getNamespaces()).thenReturn(new ArrayList<>());
@@ -226,4 +262,55 @@ void return_400_when_request_body_is_null() throws NamespaceAlreadyExistsExcepti
226262

227263
verify(namespaceStore, never()).createNamespace(any(), any());
228264
}
265+
266+
@Test
267+
void return_all_namespaces_when_auth_disabled() {
268+
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);
269+
270+
assertEquals(ALL_NAMESPACES, resourceWithAuth(false).namespaces().getValues());
271+
}
272+
273+
@Test
274+
void return_all_namespaces_when_validator_not_resolvable() {
275+
when(mockValidatorInstance.isResolvable()).thenReturn(false);
276+
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);
277+
278+
assertEquals(ALL_NAMESPACES, resourceWithAuth(true).namespaces().getValues());
279+
}
280+
281+
@Test
282+
void return_all_namespaces_for_global_admin() {
283+
when(mockValidatorInstance.isResolvable()).thenReturn(true);
284+
when(mockPermissionChecker.hasGlobalAdmin(mockIdentity)).thenReturn(true);
285+
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);
286+
287+
assertEquals(ALL_NAMESPACES, resourceWithAuth(true).namespaces().getValues());
288+
}
289+
290+
@Test
291+
void return_only_accessible_namespaces_for_authenticated_user() {
292+
when(mockValidatorInstance.isResolvable()).thenReturn(true);
293+
when(mockPermissionChecker.hasGlobalAdmin(mockIdentity)).thenReturn(false);
294+
when(mockIdentity.getPrincipal()).thenReturn(mockPrincipal);
295+
when(mockPrincipal.getName()).thenReturn("thomas");
296+
when(mockValidatorInstance.get()).thenReturn(mockValidator);
297+
when(mockValidator.getReadableNamespaces("thomas")).thenReturn(Set.of("finos"));
298+
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);
299+
300+
assertEquals(List.of(new NamespaceInfo("finos", "FINOS namespace")),
301+
resourceWithAuth(true).namespaces().getValues());
302+
}
303+
304+
@Test
305+
void return_empty_list_when_user_has_no_grants() {
306+
when(mockValidatorInstance.isResolvable()).thenReturn(true);
307+
when(mockPermissionChecker.hasGlobalAdmin(mockIdentity)).thenReturn(false);
308+
when(mockIdentity.getPrincipal()).thenReturn(mockPrincipal);
309+
when(mockPrincipal.getName()).thenReturn("thomas");
310+
when(mockValidatorInstance.get()).thenReturn(mockValidator);
311+
when(mockValidator.getReadableNamespaces("thomas")).thenReturn(Set.of());
312+
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);
313+
314+
assertTrue(resourceWithAuth(true).namespaces().getValues().isEmpty());
315+
}
229316
}

0 commit comments

Comments
 (0)