Skip to content

Commit

Permalink
Merge pull request #417 from folio-org/MODFQMMGR-462-no-mo-mod-consorsio
Browse files Browse the repository at this point in the history
Revert "Revert "MODFQMMGR-462 Get tenant data from user-tenants""
  • Loading branch information
mweaver-ebsco authored Sep 9, 2024
2 parents 166ddb2 + ada98ea commit b9d5768
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 183 deletions.
33 changes: 14 additions & 19 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
{
"methods": ["POST"],
"pathPattern": "/_/tenant",
"permissionsRequired": []
"permissionsRequired": [],
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["GET", "DELETE"],
Expand All @@ -27,20 +28,19 @@
"methods": ["GET"],
"pathPattern": "/entity-types/{entity-type-id}",
"permissionsRequired": ["fqm.entityTypes.item.get"],
"modulePermissions": ["perms.users.get"]
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["GET"],
"pathPattern": "/entity-types",
"permissionsRequired": ["fqm.entityTypes.collection.get"],
"modulePermissions": ["perms.users.get"]
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["GET"],
"pathPattern": "/entity-types/{entity-type-id}/columns/{column-name}/values",
"permissionsRequired": ["fqm.entityTypes.item.columnValues.get"],
"permissionsDesired": ["consortia.user-tenants.collection.get"],
"modulePermissions": ["perms.users.get"]
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["POST"],
Expand All @@ -57,21 +57,19 @@
"methods": ["GET"],
"pathPattern": "/query",
"permissionsRequired": ["fqm.query.sync.get"],
"permissionsDesired": ["consortia.user-tenants.collection.get"],
"modulePermissions": ["perms.users.get"]
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["GET"],
"pathPattern": "/query/{query-id}",
"permissionsRequired": ["fqm.query.async.results.get"],
"permissionsDesired": ["consortia.user-tenants.collection.get"],
"modulePermissions": ["perms.users.get"]
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["GET"],
"pathPattern": "/query/{query-id}/sortedIds",
"permissionsRequired": ["fqm.query.async.results.get"],
"modulePermissions": ["perms.users.get"]
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["GET"],
Expand All @@ -83,14 +81,13 @@
"methods": ["POST"],
"pathPattern": "/query/contents",
"permissionsRequired": ["fqm.query.async.results.get"],
"modulePermissions": ["perms.users.get"]
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["POST"],
"pathPattern": "/query",
"permissionsRequired": ["fqm.query.async.post"],
"permissionsDesired": ["consortia.user-tenants.collection.get"],
"modulePermissions": ["perms.users.get"]
"modulePermissions": ["perms.users.get", "user-tenants.collection.get"]
},
{
"methods": ["POST"],
Expand Down Expand Up @@ -266,6 +263,10 @@
"id": "users",
"version": "16.0"
},
{
"id": "user-tenants",
"version": "1.0"
},
{
"id": "loan-policy-storage",
"version": "2.3"
Expand Down Expand Up @@ -351,12 +352,6 @@
"version": "1.1"
}
],
"optional": [
{
"id": "consortia",
"version": "1.0"
}
],
"launchDescriptor": {
"dockerImage": "@artifactId@:@version@",
"dockerPull": false,
Expand Down
92 changes: 37 additions & 55 deletions src/main/java/org/folio/fqm/service/CrossTenantQueryService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.folio.fqm.service;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
Expand All @@ -13,7 +12,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand All @@ -24,49 +23,23 @@ public class CrossTenantQueryService {
private final FolioExecutionContext executionContext;
private final PermissionsService permissionsService;

private static final String CROSS_TENANT_QUERY_ERROR = "Error retrieving tenants for cross-tenant query. Tenant may not be in an ECS environment.";
private static final String CONSORTIA_CONFIGURATION_PATH = "consortia-configuration";
private static final String CENTRAL_TENANT_ID = "centralTenantId";
private static final String COMPOSITE_INSTANCES_ID = "6b08439b-4f8e-4468-8046-ea620f5cfb74";

public List<String> getTenantsToQuery(EntityType entityType, boolean forceCrossTenantQuery) {
if (!forceCrossTenantQuery && !Boolean.TRUE.equals(entityType.getCrossTenantQueriesEnabled())) {
return List.of(executionContext.getTenantId());
}
// List of shadow users associated with this user and the ECS tenants that those users exist in
List<Map<String, String>> userTenantMaps;
String centralTenantId;
String consortiumId;
try {
String configurationJson = ecsClient.get(CONSORTIA_CONFIGURATION_PATH, Map.of());
centralTenantId = JsonPath
.parse(configurationJson)
.read(CENTRAL_TENANT_ID);
if (centralTenantId.equals(executionContext.getTenantId())) {
String consortiumIdJson = ecsClient.get("consortia", Map.of());
consortiumId = JsonPath
.parse(consortiumIdJson)
.read("consortia[0].id");
} else {
log.debug("Tenant {} is not central tenant. Running intra-tenant query.", executionContext.getTenantId());
// The Instances entity type is required to retrieve shared instances from the central tenant when
// running queries from member tenants. This means that if we are running a query for Instances, we need to
// query the current tenant (for local records) as well as the central tenant (for shared records).
if (COMPOSITE_INSTANCES_ID.equals(entityType.getId())) {
return List.of(executionContext.getTenantId(), centralTenantId);
}
return List.of(executionContext.getTenantId());
List<Map<String, String>> userTenantMaps = getUserTenants();
String centralTenantId = getCentralTenantId(userTenantMaps); // null if non-ECS environment; non-null otherwise

if (!executionContext.getTenantId().equals(centralTenantId)) {
log.debug("Tenant {} is not central tenant. Running intra-tenant query.", executionContext.getTenantId());
// The Instances entity type is required to retrieve shared instances from the central tenant when
// running queries from member tenants. This means that if we are running a query for Instances, we need to
// query the current tenant (for local records) as well as the central tenant (for shared records).
if (COMPOSITE_INSTANCES_ID.equals(entityType.getId())) {
return List.of(executionContext.getTenantId(), centralTenantId);
}
UUID userId = executionContext.getUserId();
String userTenantResponse = ecsClient.get(
"consortia/" + consortiumId + "/user-tenants",
Map.of("userId", userId.toString())
);
userTenantMaps = JsonPath
.parse(userTenantResponse)
.read("$.userTenants", List.class);
} catch (Exception e) {
log.debug("Error retrieving tenants for cross-tenant query. Running intra-tenant query.");
return List.of(executionContext.getTenantId());
}

Expand All @@ -88,26 +61,35 @@ public List<String> getTenantsToQuery(EntityType entityType, boolean forceCrossT
return tenantsToQuery;
}

@SuppressWarnings("unchecked")
// JsonPath.parse is returning a plain List without a type parameter, and the TypeRef (vs Class) parameter to JsonPath.read is not supported by the JSON parser
private List<Map<String, String>> getUserTenants() {
String userTenantsResponse = ecsClient.get("user-tenants", Map.of("limit", "1000"));
List<Map<String, String>> userTenants = JsonPath
.parse(userTenantsResponse)
.read("$.userTenants", List.class);

// Get the first entry for each tenant
return userTenants.stream().collect(Collectors.groupingBy(m -> m.get("tenantId")))
.values()
.stream()
.filter(l -> !l.isEmpty()) // Just to be safe...
.map(m -> m.get(0))
.toList();
}

private String getCentralTenantId(List<Map<String, String>> userTenants) {
return userTenants.stream()
.map(map -> map.get("centralTenantId"))
.findFirst()
.orElse(null);
}

public String getCentralTenantId() {
try {
String rawJson = ecsClient.get(CONSORTIA_CONFIGURATION_PATH, Map.of());
DocumentContext parsedJson = JsonPath.parse(rawJson);
return parsedJson.read(CENTRAL_TENANT_ID);
} catch (Exception e) {
log.debug(CROSS_TENANT_QUERY_ERROR);
return null;
}
return getCentralTenantId(getUserTenants());
}

public boolean ecsEnabled() {
try {
String rawJson = ecsClient.get(CONSORTIA_CONFIGURATION_PATH, Map.of());
DocumentContext parsedJson = JsonPath.parse(rawJson);
parsedJson.read(CENTRAL_TENANT_ID);
return true;
} catch (Exception e) {
log.debug(CROSS_TENANT_QUERY_ERROR);
return false;
}
return !getUserTenants().isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,10 @@ private Stream<EntityTypeColumn> getFilteredColumns(Stream<EntityTypeColumn> unf
}

private boolean ecsEnabled() {
try {
String rawJson = ecsClient.get("consortia-configuration", Map.of("limit", String.valueOf(100)));
DocumentContext parsedJson = JsonPath.parse(rawJson);
// The value isn't needed here, this just provides an easy way to tell if ECS is enabled
parsedJson.read("centralTenantId");
return true;
} catch (Exception e) {
return false;
}
String rawJson = ecsClient.get("user-tenants", Map.of("limit", String.valueOf(1)));
DocumentContext parsedJson = JsonPath.parse(rawJson);
// The value isn't needed here, this just provides an easy way to tell if ECS is enabled
int totalRecords = parsedJson.read("totalRecords", Integer.class);
return totalRecords > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import feign.FeignException;
import lombok.extern.log4j.Log4j2;
import org.folio.fqm.client.SimpleHttpClient;
import org.folio.fqm.repository.EntityTypeRepository;
import org.folio.querytool.domain.dto.EntityType;
import org.folio.spring.FolioExecutionContext;
Expand All @@ -34,19 +29,19 @@ public class EntityTypeInitializationService {

private final ObjectMapper objectMapper;
private final ResourcePatternResolver resourceResolver;
private final SimpleHttpClient ecsClient;
private final CrossTenantQueryService crossTenantQueryService;

@Autowired
public EntityTypeInitializationService(
EntityTypeRepository entityTypeRepository,
FolioExecutionContext folioExecutionContext,
ResourcePatternResolver resourceResolver,
SimpleHttpClient ecsClient
CrossTenantQueryService crossTenantQueryService
) {
this.entityTypeRepository = entityTypeRepository;
this.folioExecutionContext = folioExecutionContext;
this.resourceResolver = resourceResolver;
this.ecsClient = ecsClient;
this.crossTenantQueryService = crossTenantQueryService;

// this enables all JSON5 features, except for numeric ones (hex, starting/trailing
// decimal points, use of NaN, etc), as those are not relevant for our use
Expand All @@ -71,16 +66,15 @@ public EntityTypeInitializationService(
// called as part of tenant install/upgrade (see FqmTenantService)
public void initializeEntityTypes() throws IOException {
log.info("Initializing entity types");
String centralTenantId = "${central_tenant_id}";
try {
String rawJson = ecsClient.get("consortia-configuration", Map.of("limit", String.valueOf(100)));
DocumentContext parsedJson = JsonPath.parse(rawJson);
centralTenantId = parsedJson.read("centralTenantId");
String centralTenantId = crossTenantQueryService.getCentralTenantId();
if (centralTenantId != null) {
log.info("ECS central tenant ID: {}", centralTenantId);
} catch (FeignException.NotFound | IllegalArgumentException e) {
}
else {
log.info("ECS is not enabled for tenant {}", folioExecutionContext.getTenantId());
centralTenantId = "${central_tenant_id}";
}
String finalCentralTenantId = centralTenantId;
String finalCentralTenantId = centralTenantId; // Make centralTenantId effectively final, for the lambda below
List<EntityType> desiredEntityTypes = Stream
.concat(
Arrays.stream(resourceResolver.getResources("classpath:/entity-types/**/*.json")),
Expand Down
7 changes: 4 additions & 3 deletions src/test/java/org/folio/fqm/IntegrationTestBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ protected static DataSource getDataSource() {

private static void postTenant(String body) {
given()
.header(XOkapiHeaders.TENANT, TENANT_ID)
// .header(XOkapiHeaders.TENANT, TENANT_ID)
.headers(getOkapiHeaders())
.contentType("application/json")
.body(body)
.when()
Expand Down Expand Up @@ -173,8 +174,8 @@ public MockResponse dispatch(@NotNull RecordedRequest recordedRequest) {
}
""").setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}
if (recordedRequest.getPath().matches("/consortia-configuration.*")) {
return new MockResponse().setBody("").setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
if (recordedRequest.getPath().matches("/user-tenants.*")) {
return new MockResponse().setBody("{\"userTenants\": [], \"totalRecords\": 0}").setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}
throw new RuntimeException("Unexpected request: " + recordedRequest.getPath());
}
Expand Down
29 changes: 26 additions & 3 deletions src/test/java/org/folio/fqm/context/TestDbSetupConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@

import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.util.Map;
import javax.sql.DataSource;
import liquibase.integration.spring.SpringLiquibase;
import org.folio.fqm.IntegrationTestBase;
import org.folio.fqm.client.SimpleHttpClient;
import org.folio.fqm.repository.EntityTypeRepository;
import org.folio.fqm.service.CrossTenantQueryService;
import org.folio.fqm.service.EntityTypeInitializationService;
import org.folio.fqm.service.PermissionsService;
import org.folio.spring.FolioExecutionContext;
import org.mockito.Mock;
import org.mockito.Spy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.support.ResourcePatternResolver;

import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -50,10 +55,28 @@ static class EntityTypeInitializer {
@Autowired
private ResourcePatternResolver resourceResolver;

@Autowired
private FolioExecutionContext executionContext;

@Autowired
private PermissionsService permissionsService;

@PostConstruct
public void populateEntityTypes() throws IOException {
SimpleHttpClient ecsClient = mock(SimpleHttpClient.class);
when(ecsClient.get("consortia-configuration", Map.of("limit", String.valueOf(100)))).thenReturn("{\"centralTenantId\": \"tenant_01\"}");
when(ecsClient.get(eq("user-tenants"), anyMap())).thenReturn("""
{
"userTenants": [
{
"id": "06192681-0df7-4f33-a38f-48e017648d69",
"userId": "a5e7895f-503c-4335-8828-f507bc8d1c45",
"tenantId": "tenant_01",
"centralTenantId": "tenant_01"
}
],
"totalRecords": 1
}
""");
new EntityTypeInitializationService(
entityTypeRepository,
new FolioExecutionContext() {
Expand All @@ -63,7 +86,7 @@ public String getTenantId() {
}
},
resourceResolver,
ecsClient
new CrossTenantQueryService(ecsClient, executionContext, permissionsService)
)
.initializeEntityTypes();
}
Expand Down
Loading

0 comments on commit b9d5768

Please sign in to comment.