Skip to content
Draft
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
Expand Up @@ -2,32 +2,51 @@

import io.quarkus.security.Authenticated;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.finos.calm.domain.NamespaceRequest;
import org.finos.calm.domain.ValueWrapper;
import org.finos.calm.domain.exception.NamespaceAlreadyExistsException;
import org.finos.calm.domain.namespaces.NamespaceInfo;
import org.finos.calm.security.CalmHubPermissionChecker;
import org.finos.calm.security.CalmHubScopes;
import org.finos.calm.security.UserAccessValidator;
import org.finos.calm.store.NamespaceStore;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Set;

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

private final NamespaceStore namespaceStore;
private final Instance<UserAccessValidator> userAccessValidatorInstance;
private final CalmHubPermissionChecker permissionChecker;

@Inject
public NamespaceResource(NamespaceStore store) {
SecurityIdentity identity;

@Inject
@ConfigProperty(name = "calm.auth.enabled", defaultValue = "false")
boolean authEnabled;

@Inject
public NamespaceResource(NamespaceStore store,
Instance<UserAccessValidator> userAccessValidatorInstance,
CalmHubPermissionChecker permissionChecker) {
this.namespaceStore = store;
this.userAccessValidatorInstance = userAccessValidatorInstance;
this.permissionChecker = permissionChecker;
}

@GET
Expand All @@ -37,7 +56,17 @@ public NamespaceResource(NamespaceStore store) {
)
@Authenticated
public ValueWrapper<NamespaceInfo> namespaces() {
return new ValueWrapper<>(namespaceStore.getNamespaces());
if (!authEnabled || !userAccessValidatorInstance.isResolvable()) {
return new ValueWrapper<>(namespaceStore.getNamespaces());
}
if (permissionChecker.hasGlobalAdmin(identity)) {
return new ValueWrapper<>(namespaceStore.getNamespaces());
}
Set<String> grants = userAccessValidatorInstance.get()
.getReadableNamespaces(identity.getPrincipal().getName());
return new ValueWrapper<>(namespaceStore.getNamespaces().stream()
.filter(ns -> grants.contains(ns.getName()))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic makes sense here, but one question: if line 65 already returns the list of readable namespaces for the user, why do we need to retrieve it again from namespaceStore? Can we not just return the readable namespaces directly, saving a query?

.toList());
}

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

}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
package org.finos.calm.resources;

import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import jakarta.enterprise.inject.Instance;
import org.finos.calm.domain.exception.NamespaceAlreadyExistsException;
import org.finos.calm.domain.namespaces.NamespaceInfo;
import org.finos.calm.security.CalmHubPermissionChecker;
import org.finos.calm.security.UserAccessValidator;
import org.finos.calm.store.NamespaceStore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import static io.restassured.RestAssured.given;
import static org.finos.calm.resources.ResourceValidationConstants.NAMESPACE_MESSAGE;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

@TestSecurity(authorizationEnabled = false)
Expand All @@ -27,6 +37,32 @@ public class TestNamespaceResourceShould {
@InjectMock
NamespaceStore namespaceStore;

// Plain Mockito mocks for direct-instantiation filtering tests
@Mock
private NamespaceStore mockNamespaceStore;
@Mock
private Instance<UserAccessValidator> mockValidatorInstance;
@Mock
private UserAccessValidator mockValidator;
@Mock
private SecurityIdentity mockIdentity;
@Mock
private Principal mockPrincipal;
@Mock
private CalmHubPermissionChecker mockPermissionChecker;

private static final List<NamespaceInfo> ALL_NAMESPACES = List.of(
new NamespaceInfo("finos", "FINOS namespace"),
new NamespaceInfo("custom", "custom namespace")
);

private NamespaceResource resourceWithAuth(boolean authEnabled) {
NamespaceResource resource = new NamespaceResource(mockNamespaceStore, mockValidatorInstance, mockPermissionChecker);
resource.identity = mockIdentity;
resource.authEnabled = authEnabled;
return resource;
}

@Test
void return_empty_wrapper_when_no_namespaces_in_store() {
when(namespaceStore.getNamespaces()).thenReturn(new ArrayList<>());
Expand Down Expand Up @@ -226,4 +262,55 @@ void return_400_when_request_body_is_null() throws NamespaceAlreadyExistsExcepti

verify(namespaceStore, never()).createNamespace(any(), any());
}

@Test
void return_all_namespaces_when_auth_disabled() {
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);

assertEquals(ALL_NAMESPACES, resourceWithAuth(false).namespaces().getValues());
}

@Test
void return_all_namespaces_when_validator_not_resolvable() {
when(mockValidatorInstance.isResolvable()).thenReturn(false);
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);

assertEquals(ALL_NAMESPACES, resourceWithAuth(true).namespaces().getValues());
}

@Test
void return_all_namespaces_for_global_admin() {
when(mockValidatorInstance.isResolvable()).thenReturn(true);
when(mockPermissionChecker.hasGlobalAdmin(mockIdentity)).thenReturn(true);
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);

assertEquals(ALL_NAMESPACES, resourceWithAuth(true).namespaces().getValues());
}

@Test
void return_only_accessible_namespaces_for_authenticated_user() {
when(mockValidatorInstance.isResolvable()).thenReturn(true);
when(mockPermissionChecker.hasGlobalAdmin(mockIdentity)).thenReturn(false);
when(mockIdentity.getPrincipal()).thenReturn(mockPrincipal);
when(mockPrincipal.getName()).thenReturn("thomas");
when(mockValidatorInstance.get()).thenReturn(mockValidator);
when(mockValidator.getReadableNamespaces("thomas")).thenReturn(Set.of("finos"));
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);

assertEquals(List.of(new NamespaceInfo("finos", "FINOS namespace")),
resourceWithAuth(true).namespaces().getValues());
}

@Test
void return_empty_list_when_user_has_no_grants() {
when(mockValidatorInstance.isResolvable()).thenReturn(true);
when(mockPermissionChecker.hasGlobalAdmin(mockIdentity)).thenReturn(false);
when(mockIdentity.getPrincipal()).thenReturn(mockPrincipal);
when(mockPrincipal.getName()).thenReturn("thomas");
when(mockValidatorInstance.get()).thenReturn(mockValidator);
when(mockValidator.getReadableNamespaces("thomas")).thenReturn(Set.of());
when(mockNamespaceStore.getNamespaces()).thenReturn(ALL_NAMESPACES);

assertTrue(resourceWithAuth(true).namespaces().getValues().isEmpty());
}
}
Loading