diff --git a/pom.xml b/pom.xml index 0bf363b5630407611fdd47070d23aa7fcde57d83..463794ff03568206e6348128bf108e965315e728 100644 --- a/pom.xml +++ b/pom.xml @@ -130,6 +130,7 @@ <module>provider/indexer-azure</module> <module>provider/indexer-gcp</module> <module>provider/indexer-ibm</module> + <module>provider/indexer-reference</module> </modules> <repositories> diff --git a/provider/indexer-reference/Dockerfile b/provider/indexer-reference/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1e6901f365e2e9c7049c5019b4037ade931b65f0 --- /dev/null +++ b/provider/indexer-reference/Dockerfile @@ -0,0 +1,5 @@ +FROM openjdk:8-slim +WORKDIR /app +COPY target/indexer-reference-0.0.1-SNAPSHOT-spring-boot.jar indexer-reference.jar +# Run the web service on container startup. +CMD java -Djava.security.egd=file:/dev/./urandom -Dserver.port=8080 -jar /app/indexer-reference.jar diff --git a/provider/indexer-reference/docker-compose.yml b/provider/indexer-reference/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..a6838447de241e658325e275434293b2d4e2be09 --- /dev/null +++ b/provider/indexer-reference/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" +services: + os-storage-app: + build: + args: + JAR_FILE: target/indexer-reference-0.0.1-SNAPSHOT-spring-boot.jar + context: "" + dockerfile: ../Dockerfile + image: us.gcr.io/osdu-anthos-02/os-indexer/anthos-indexer-reference + ports: + - "8080:8080" + diff --git a/provider/indexer-reference/kubernetes/deployments/deployment-os-indexer-service.yml b/provider/indexer-reference/kubernetes/deployments/deployment-os-indexer-service.yml new file mode 100644 index 0000000000000000000000000000000000000000..0157d9875389754f1c640f0d186e02b2ea0020d8 --- /dev/null +++ b/provider/indexer-reference/kubernetes/deployments/deployment-os-indexer-service.yml @@ -0,0 +1,179 @@ +apiVersion: v1 +data: + AUTHORIZE_API: ${AUTHORIZE_API} + MONGO_DB_URL: ${MONGO_DB_URL} + MONGO_DB_USER: ${MONGO_DB_USER} + MONGO_DB_NAME: ${MONGO_DB_NAME} + REGION: ${REGION} + LEGALTAG_API: ${LEGALTAG_API} + INDEXER_HOST: ${INDEXER_HOST} + GOOGLE_CLOUD_PROJECT: ${GOOGLE_CLOUD_PROJECT} + STORAGE_HOSTNAME: ${STORAGE_HOSTNAME} + STORAGE_SCHEMA_HOST: ${STORAGE_SCHEMA_HOST} + STORAGE_QUERY_RECORD_HOST: ${STORAGE_QUERY_RECORD_HOST} + STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST: ${STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST} + INDEXER_QUEUE_HOST: ${INDEXER_QUEUE_HOST} + CRS_API: ${CRS_API} + REDIS_GROUP_HOST: ${REDIS_GROUP_HOST} + REDIS_SEARCH_HOST: ${REDIS_SEARCH_HOST} + GOOGLE_AUDIENCES: ${GOOGLE_AUDIENCES} + KEY_RING: ${KEY_RING} + KMS_KEY: ${KMS_KEY} +kind: ConfigMap +metadata: + labels: + app: indexer-reference + name: indexer-config + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + generateName: indexer-reference-anthos + labels: + app: indexer-reference + name: indexer-reference + namespace: default +spec: + selector: + matchLabels: + app: indexer-reference + replicas: 1 + template: + metadata: + labels: + app: indexer-reference + spec: + containers: + - env: + - name: AUTHORIZE_API + valueFrom: + configMapKeyRef: + key: AUTHORIZE_API + name: indexer-config + - name: MONGO_DB_URL + valueFrom: + configMapKeyRef: + key: MONGO_DB_URL + name: indexer-config + - name: MONGO_DB_USER + valueFrom: + configMapKeyRef: + key: MONGO_DB_USER + name: indexer-config + - name: MONGO_DB_PASSWORD + valueFrom: + secretKeyRef: + name: indexer-secret + key: mongo.db.password + - name: MONGO_DB_NAME + valueFrom: + configMapKeyRef: + key: MONGO_DB_NAME + name: indexer-config + - name: MB_RABBITMQ_URI + valueFrom: + secretKeyRef: + name: indexer-secret + key: mb.rabbitmq.uri + - name: REGION + valueFrom: + configMapKeyRef: + key: REGION + name: indexer-config + - name: LEGALTAG_API + valueFrom: + configMapKeyRef: + key: LEGALTAG_API + name: indexer-config + - name: INDEXER_HOST + valueFrom: + configMapKeyRef: + key: INDEXER_HOST + name: indexer-config + - name: GOOGLE_CLOUD_PROJECT + valueFrom: + configMapKeyRef: + key: GOOGLE_CLOUD_PROJECT + name: indexer-config + - name: STORAGE_HOSTNAME + valueFrom: + configMapKeyRef: + key: STORAGE_HOSTNAME + name: indexer-config + - name: STORAGE_SCHEMA_HOST + valueFrom: + configMapKeyRef: + key: STORAGE_SCHEMA_HOST + name: indexer-config + - name: STORAGE_QUERY_RECORD_HOST + valueFrom: + configMapKeyRef: + key: STORAGE_QUERY_RECORD_HOST + name: indexer-config + - name: STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST + valueFrom: + configMapKeyRef: + key: STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST + name: indexer-config + - name: INDEXER_QUEUE_HOST + valueFrom: + configMapKeyRef: + key: INDEXER_QUEUE_HOST + name: indexer-config + - name: CRS_API + valueFrom: + configMapKeyRef: + key: CRS_API + name: indexer-config + - name: REDIS_GROUP_HOST + valueFrom: + configMapKeyRef: + key: REDIS_GROUP_HOST + name: indexer-config + - name: REDIS_SEARCH_HOST + valueFrom: + configMapKeyRef: + key: REDIS_SEARCH_HOST + name: indexer-config + - name: GOOGLE_AUDIENCES + valueFrom: + configMapKeyRef: + key: GOOGLE_AUDIENCES + name: indexer-config + - name: KEY_RING + valueFrom: + configMapKeyRef: + key: KEY_RING + name: indexer-config + - name: KMS_KEY + valueFrom: + configMapKeyRef: + key: KMS_KEY + name: indexer-config + image: us.gcr.io/osdu-anthos-02/os-indexer/anthos-indexer-reference:9966597-dirty + name: indexer-reference +--- +apiVersion: v1 +kind: Service +metadata: + name: indexer-reference + namespace: default +spec: + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + selector: + app: indexer-reference + type: LoadBalancer +--- +apiVersion: v1 +data: + mongo.db.password: ${mongo.db.password} + mb.rabbitmq.uri: ${mb.rabbitmq.uri} +kind: Secret +metadata: + name: indexer-secret + namespace: default +type: Opaque \ No newline at end of file diff --git a/provider/indexer-reference/pom.xml b/provider/indexer-reference/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..99d01082c723b55da8f969a693d301daf9378570 --- /dev/null +++ b/provider/indexer-reference/pom.xml @@ -0,0 +1,197 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2020 Google LLC + ~ Copyright 2020 EPAM Systems, Inc + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ https://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.opengroup.osdu.indexer</groupId> + <artifactId>indexer-service</artifactId> + <version>1.0.4-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <artifactId>indexer-reference</artifactId> + <version>0.0.1-SNAPSHOT</version> + <name>indexer-reference</name> + <description>Indexer Service GCP Anthos</description> + <packaging>jar</packaging> + + <dependencies> + <dependency> + <groupId>org.opengroup.osdu.indexer</groupId> + <artifactId>indexer-core</artifactId> + <version>1.0.6-SNAPSHOT</version> + </dependency> + + <dependency> + <groupId>org.opengroup.osdu</groupId> + <artifactId>core-lib-gcp</artifactId> + <version>0.1.17</version> + </dependency> + + <dependency> + <groupId>org.opengroup.osdu</groupId> + <artifactId>os-core-common</artifactId> + <version>0.0.18</version> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-mongodb</artifactId> + </dependency> + + <dependency> + <groupId>com.google.cloud</groupId> + <artifactId>google-cloud-logging</artifactId> + <version>1.72.0</version> + </dependency> + <dependency> + <groupId>com.google.apis</groupId> + <artifactId>google-api-services-storage</artifactId> + <version>v1-rev150-1.25.0</version> + <exclusions> + <exclusion> + <groupId>com.google.guava</groupId> + <artifactId>guava-jdk5</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>com.google.apis</groupId> + <artifactId>google-api-services-cloudkms</artifactId> + <version>v1-rev81-1.25.0</version> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-amqp</artifactId> + </dependency> + <dependency> + <groupId>org.elasticsearch</groupId> + <artifactId>elasticsearch</artifactId> + <version>6.6.2</version> + </dependency> + <dependency> + <groupId>org.elasticsearch.client</groupId> + <artifactId>elasticsearch-rest-client</artifactId> + <version>6.6.2</version> + </dependency> + <dependency> + <groupId>org.elasticsearch.client</groupId> + <artifactId>elasticsearch-rest-high-level-client</artifactId> + <version>6.6.2</version> + </dependency> + + <!-- Test Dependencies --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-test</artifactId> + <scope>test</scope> + </dependency> + + <!-- Test Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.12</version> + <scope>test</scope> + </dependency> + <!-- https://mvnrepository.com/artifact/org.powermock/powermock-api-mockito2 --> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito2</artifactId> + <version>2.0.2</version> + <scope>test</scope> + </dependency> + <!-- https://mvnrepository.com/artifact/org.powermock/powermock-module-junit4 --> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-module-junit4</artifactId> + <version>2.0.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>2.26.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.codehaus.mojo</groupId> + <artifactId>cobertura-maven-plugin</artifactId> + <version>2.7</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>3.8.1</version> + </dependency> + + <!-- https://mvnrepository.com/artifact/com.github.stefanbirkner/system-rules --> + <dependency> + <groupId>com.github.stefanbirkner</groupId> + <artifactId>system-rules</artifactId> + <version>1.2.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.google.api.grpc</groupId> + <artifactId>proto-google-iam-v1</artifactId> + <version>0.12.0</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-actuator-autoconfigure</artifactId> + </dependency> + + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>repackage</goal> + </goals> + <configuration> + <classifier>spring-boot</classifier> + <mainClass> + org.opengroup.osdu.indexer.IndexerAnthosApplication + </mainClass> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + + +</project> diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/IndexerAnthosApplication.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/IndexerAnthosApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..a87c9d4577a573359f5d34682be962078b11bc67 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/IndexerAnthosApplication.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer; + +import org.opengroup.osdu.core.gcp.multitenancy.TenantFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; + +@SpringBootApplication(exclude = {MongoAutoConfiguration.class, SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class}) +@Configuration +@ComponentScan(value = {"org.opengroup.osdu"}, excludeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = TenantFactory.class)}) +public class IndexerAnthosApplication { + + public static void main(String[] args) { + SpringApplication.run(IndexerAnthosApplication.class, args); + } + +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/ServletInitializer.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/ServletInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..ccc931148ed7a9826fb8cc7854c85be5f898d796 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/ServletInitializer.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ServletInitializer extends SpringBootServletInitializer { + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(IndexerAnthosApplication.class); + } +} + diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/AttributesCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/AttributesCache.java new file mode 100644 index 0000000000000000000000000000000000000000..bdfa51cd624d4afedfa08547fdda56a06c1f7051 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/AttributesCache.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.provider.interfaces.IAttributesCache; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import java.util.Set; + +@Component +public class AttributesCache implements IAttributesCache<String,Set>, AutoCloseable { + + private RedisCache<String, Set> cache; + + public AttributesCache(@Value("${REDIS_SEARCH_HOST}") final String REDIS_SEARCH_HOST, + @Value("${REDIS_SEARCH_PORT}") final String REDIS_SEARCH_PORT, + @Value("${INDEX_CACHE_EXPIRATION}") final String INDEX_CACHE_EXPIRATION) { + + cache = new RedisCache(REDIS_SEARCH_HOST, Integer.parseInt(REDIS_SEARCH_PORT), + Integer.parseInt(INDEX_CACHE_EXPIRATION) * 60, String.class, Boolean.class); + } + + @Override + public void put(String key, Set value) { + this.cache.put(key, value); + } + + @Override + public Set get(String key) { + return this.cache.get(key); + } + + @Override + public void delete(String key) { + this.cache.delete(key); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } + + @Override + public void close() { + this.cache.close(); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/DatastoreCredentialCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/DatastoreCredentialCache.java new file mode 100644 index 0000000000000000000000000000000000000000..d17e26f7b832658226f9cd32b11426b4e54fe1fc --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/DatastoreCredentialCache.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import com.google.auth.oauth2.AccessToken; +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class DatastoreCredentialCache extends RedisCache<String, AccessToken> { + + // Datastore credentials are only valid for 1hr, release the key 2 minutes before the expiration + public DatastoreCredentialCache(@Value("${REDIS_SEARCH_HOST}") final String REDIS_SEARCH_HOST, @Value("${REDIS_SEARCH_PORT}") final String REDIS_SEARCH_PORT) { + super(REDIS_SEARCH_HOST, Integer.parseInt(REDIS_SEARCH_PORT), 58 * 60, String.class, AccessToken.class); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/ElasticCredentialsCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/ElasticCredentialsCache.java new file mode 100644 index 0000000000000000000000000000000000000000..7a5d5f6f32d8a76eb6172b30047d893a718277d4 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/ElasticCredentialsCache.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.model.search.ClusterSettings; +import org.opengroup.osdu.core.common.provider.interfaces.IElasticCredentialsCache; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class ElasticCredentialsCache implements IElasticCredentialsCache<String, ClusterSettings>, AutoCloseable { + + private RedisCache<String, ClusterSettings> cache; + + public ElasticCredentialsCache(@Value("${REDIS_SEARCH_HOST}") final String REDIS_SEARCH_HOST, + @Value("${REDIS_SEARCH_PORT}") final String REDIS_SEARCH_PORT, + @Value("${ELASTIC_CACHE_EXPIRATION}") final String ELASTIC_CACHE_EXPIRATION) { + cache = new RedisCache<>(REDIS_SEARCH_HOST, Integer.parseInt(REDIS_SEARCH_PORT), + Integer.parseInt(ELASTIC_CACHE_EXPIRATION) * 60, String.class, ClusterSettings.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, ClusterSettings o) { + this.cache.put(s,o); + } + + @Override + public ClusterSettings get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/IndexCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/IndexCache.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b7197fb040234eac6627e926e6e2b88a69fcc3 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/IndexCache.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.provider.interfaces.IIndexCache; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class IndexCache implements IIndexCache<String, Boolean>, AutoCloseable { + private RedisCache<String, Boolean> cache; + + public IndexCache(@Value("${REDIS_SEARCH_HOST}") final String REDIS_SEARCH_HOST, + @Value("${REDIS_SEARCH_PORT}") final String REDIS_SEARCH_PORT, + @Value("${INDEX_CACHE_EXPIRATION}") final String INDEX_CACHE_EXPIRATION) { + cache = new RedisCache<>(REDIS_SEARCH_HOST, Integer.parseInt(REDIS_SEARCH_PORT), + Integer.parseInt(INDEX_CACHE_EXPIRATION) * 60, String.class, Boolean.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, Boolean o) { + this.cache.put(s, o); + } + + @Override + public Boolean get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/JwtCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/JwtCache.java new file mode 100644 index 0000000000000000000000000000000000000000..2b442b622e4e3dca4266d93dbe5ea7dea8def433 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/JwtCache.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.model.search.IdToken; +import org.opengroup.osdu.core.common.provider.interfaces.IJwtCache; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtCache implements IJwtCache<String, IdToken>, AutoCloseable { + RedisCache<String, IdToken> cache; + + // google service account id_token can be requested only for 1 hr + private final static int EXPIRED_AFTER = 59; + + public JwtCache(@Value("${REDIS_SEARCH_HOST}") final String REDIS_SEARCH_HOST, @Value("${REDIS_SEARCH_PORT}") final String REDIS_SEARCH_PORT) { + cache = new RedisCache<>(REDIS_SEARCH_HOST, Integer.parseInt(REDIS_SEARCH_PORT), + EXPIRED_AFTER * 60, String.class, IdToken.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, IdToken o) { + this.cache.put(s, o); + } + + @Override + public IdToken get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/KindsCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/KindsCache.java new file mode 100644 index 0000000000000000000000000000000000000000..d70210a0c570e66dfa97c0e5ac3629f02c8c488c --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/KindsCache.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.core.common.provider.interfaces.IKindsCache; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Set; + +@Component +public class KindsCache implements IKindsCache<String, Set>, AutoCloseable { + private RedisCache<String, Set> cache; + + public KindsCache(@Value("${REDIS_SEARCH_HOST}") final String REDIS_SEARCH_HOST, + @Value("${REDIS_SEARCH_PORT}") final String REDIS_SEARCH_PORT, + @Value("${KINDS_CACHE_EXPIRATION}") final String KINDS_CACHE_EXPIRATION, + @Value("${KINDS_REDIS_DATABASE}") final String KINDS_REDIS_DATABASE) { + cache = new RedisCache<>(REDIS_SEARCH_HOST, Integer.parseInt(REDIS_SEARCH_PORT), + Integer.parseInt(KINDS_CACHE_EXPIRATION) * 60, + Integer.parseInt(KINDS_REDIS_DATABASE), String.class, Set.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, Set o) { + this.cache.put(s, o); + } + + @Override + public Set get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/SchemaCache.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/SchemaCache.java new file mode 100644 index 0000000000000000000000000000000000000000..1fc8b411471a71cd670e9f22d3981c2855a85dcc --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/cache/SchemaCache.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.cache; + +import org.opengroup.osdu.core.common.cache.RedisCache; +import org.opengroup.osdu.indexer.provider.interfaces.ISchemaCache; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class SchemaCache implements ISchemaCache<String, String>, AutoCloseable { + private RedisCache<String, String> cache; + + public SchemaCache(@Value("${REDIS_SEARCH_HOST}") final String REDIS_SEARCH_HOST, + @Value("${REDIS_SEARCH_PORT}") final String REDIS_SEARCH_PORT, + @Value("${SCHEMA_CACHE_EXPIRATION}") final String SCHEMA_CACHE_EXPIRATION) { + cache = new RedisCache<>(REDIS_SEARCH_HOST, Integer.parseInt(REDIS_SEARCH_PORT), + Integer.parseInt(SCHEMA_CACHE_EXPIRATION) * 60, String.class, String.class); + } + + @Override + public void close() throws Exception { + this.cache.close(); + } + + @Override + public void put(String s, String o) { + this.cache.put(s, o); + } + + @Override + public String get(String s) { + return this.cache.get(s); + } + + @Override + public void delete(String s) { + this.cache.delete(s); + } + + @Override + public void clearAll() { + this.cache.clearAll(); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/EntitlementsClientFactory.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/EntitlementsClientFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..8460aa5f0da3f175cffb3d54e1b0bdf9cdc2b33d --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/EntitlementsClientFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.di; + +import org.opengroup.osdu.core.common.entitlements.EntitlementsAPIConfig; +import org.opengroup.osdu.core.common.entitlements.EntitlementsFactory; +import org.opengroup.osdu.core.common.entitlements.IEntitlementsFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +@Lazy +public class EntitlementsClientFactory extends AbstractFactoryBean<IEntitlementsFactory> { + + @Value("${AUTHORIZE_API}") + private String AUTHORIZE_API; + + @Value("${AUTHORIZE_API_KEY:}") + private String AUTHORIZE_API_KEY; + + @Override + protected IEntitlementsFactory createInstance() throws Exception { + + return new EntitlementsFactory(EntitlementsAPIConfig + .builder() + .rootUrl(AUTHORIZE_API) + .apiKey(AUTHORIZE_API_KEY) + .build()); + } + + @Override + public Class<?> getObjectType() { + return IEntitlementsFactory.class; + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/RabbitMQFactoryImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/RabbitMQFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..20236aaf68467a6441e23ba129331eb3f8907550 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/RabbitMQFactoryImpl.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.di; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.concurrent.TimeoutException; +import javax.annotation.PostConstruct; +import org.opengroup.osdu.indexer.messagebus.IMessageFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class RabbitMQFactoryImpl implements IMessageFactory { + + private static final Logger LOG = LoggerFactory.getLogger(RabbitMQFactoryImpl.class); + + @Value("${mb.rabbitmq.uri}") + private String uri; + + private Channel channel; + + @PostConstruct + private void init() { + ConnectionFactory factory = new ConnectionFactory(); + try { + LOG.debug("RabbitMQ Channel " + uri); + factory.setUri(uri); + factory.setAutomaticRecoveryEnabled(true); + Connection conn = factory.newConnection(); + this.channel = conn.createChannel(); + LOG.debug("RabbitMQ Channel was created."); + for (String queue : Arrays.asList(DEFAULT_QUEUE_NAME, INDEXER_QUEUE_NAME, LEGAL_QUEUE_NAME)) { + channel.queueDeclare(queue, true, false, false, null); + LOG.debug("Queue [" + queue + "] was declared."); + } + } catch (KeyManagementException | NoSuchAlgorithmException | URISyntaxException | IOException | TimeoutException e) { + LOG.error(e.getMessage(), e); + } + + } + + @Override + public void sendMessage(String msg) { + this.sendMessage("records", msg); + } + + @Override + public void sendMessage(String queueName, String msg) { + String queueNameWithPrefix = queueName; + try { + channel.basicPublish("", queueNameWithPrefix, null, msg.getBytes()); + LOG.info(" [x] Sent '" + msg + "' to queue [" + queueNameWithPrefix + "]"); + } catch (IOException e) { + LOG.error("Unable to publish message to [" + queueNameWithPrefix + "]"); + LOG.error(e.getMessage(), e); + } + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/TenantFactoryImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/TenantFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..8aad1a1a1edadc6cb355859a4e21aaea0520748e --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/di/TenantFactoryImpl.java @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.di; + + +import com.google.gson.Gson; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.bson.Document; +import org.opengroup.osdu.core.common.cache.ICache; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.provider.interfaces.ITenantFactory; +import org.opengroup.osdu.indexer.persistence.MongoDdmsClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@Primary +@Component +public class TenantFactoryImpl implements ITenantFactory { + + private static final Logger LOG = LoggerFactory.getLogger(TenantFactoryImpl.class); + + public static final String MAIN_DATABASE = "main"; + public static final String TENANT_INFO = "tenantinfo"; + + @Autowired + private MongoDdmsClient mongoClient; + + private Map<String, TenantInfo> tenants; + + public boolean exists(String tenantName) { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.containsKey(tenantName); + } + + public TenantInfo getTenantInfo(String tenantName) { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.get(tenantName); + } + + public Collection<TenantInfo> listTenantInfo() { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.values(); + } + + public <V> ICache<String, V> createCache(String tenantName, String host, int port, + int expireTimeSeconds, Class<V> classOfV) { + return null; + } + + public void flushCache() { + } + + private void initTenants() { + this.tenants = new HashMap<>(); + MongoCollection<Document> mongoCollection = mongoClient + .getMongoCollection(MAIN_DATABASE, TENANT_INFO); + FindIterable<Document> results = mongoCollection.find(); + if (Objects.isNull(results) && Objects.isNull(results.first())) { + LOG.error(String.format("Collection \'%s\' is empty.", results)); + } + for (Document document : results) { + TenantInfo tenantInfo = new Gson().fromJson(document.toJson(),TenantInfo.class); + this.tenants.put(tenantInfo.getName(), tenantInfo); + } + } + +} + diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/kms/KmsClient.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/kms/KmsClient.java new file mode 100644 index 0000000000000000000000000000000000000000..2adc6336b182750c215b84de565f37e7ebb8da74 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/kms/KmsClient.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.kms; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.cloudkms.v1.CloudKMS; +import com.google.api.services.cloudkms.v1.CloudKMSScopes; +import com.google.api.services.cloudkms.v1.model.DecryptRequest; +import com.google.api.services.cloudkms.v1.model.DecryptResponse; +import com.google.api.services.cloudkms.v1.model.EncryptRequest; +import com.google.api.services.cloudkms.v1.model.EncryptResponse; +import org.opengroup.osdu.core.common.provider.interfaces.IKmsClient; +import org.opengroup.osdu.core.common.search.Preconditions; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Component +@RequestScope +public class KmsClient implements IKmsClient { + + @Value("${GOOGLE_CLOUD_PROJECT}") + private String GOOGLE_CLOUD_PROJECT; + + @Value("${KMS_KEY}") + private String KMS_KEY; + + @Value("${KEY_RING}") + private String KEY_RING; + + private static final String KEY_NAME = "projects/%s/locations/global/keyRings/%s/cryptoKeys/%s"; + + /** + * Encrypts the given plaintext using the specified crypto key. + * Google KMS automatically uses the new primary key version to encrypt data, so this could be directly used for key rotation + */ + public String encryptString(String textToBeEncrypted) throws IOException { + Preconditions.checkNotNullOrEmpty(textToBeEncrypted, "textToBeEncrypted cannot be null"); + + byte[] plaintext = textToBeEncrypted.getBytes(StandardCharsets.UTF_8); + String resourceName = String.format(KEY_NAME, GOOGLE_CLOUD_PROJECT, KEY_RING, KMS_KEY); + CloudKMS kms = createAuthorizedClient(); + EncryptRequest request = new EncryptRequest().encodePlaintext(plaintext); + EncryptResponse response = kms.projects().locations().keyRings().cryptoKeys() + .encrypt(resourceName, request) + .execute(); + return response.getCiphertext(); + } + + /** + * Decrypts the provided ciphertext with the specified crypto key. + * Google KMS automatically uses the correct key version to decrypt data, as long as the key version is not disabled + */ + public String decryptString(String textToBeDecrypted) throws IOException { + Preconditions.checkNotNullOrEmpty(textToBeDecrypted, "textToBeDecrypted cannot be null"); + + CloudKMS kms = createAuthorizedClient(); + String cryptoKeyName = String.format(KEY_NAME, GOOGLE_CLOUD_PROJECT, KEY_RING, KMS_KEY); + DecryptRequest request = new DecryptRequest().setCiphertext(textToBeDecrypted); + DecryptResponse response = kms.projects().locations().keyRings().cryptoKeys() + .decrypt(cryptoKeyName, request) + .execute(); + return new String(response.decodePlaintext(), StandardCharsets.UTF_8).trim(); + } + + /** + * Creates an authorized CloudKMS client service using Application Default Credentials. + * + * @return an authorized CloudKMS client + * @throws IOException if there's an error getting the default credentials. + */ + private CloudKMS createAuthorizedClient() throws IOException { + HttpTransport transport = new NetHttpTransport(); + JsonFactory jsonFactory = new JacksonFactory(); + GoogleCredential credential = GoogleCredential.getApplicationDefault(); + if (credential.createScopedRequired()) { + credential = credential.createScoped(CloudKMSScopes.all()); + } + return new CloudKMS.Builder(transport, jsonFactory, credential) + .setApplicationName("CloudKMS snippets") + .build(); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/messagebus/IMessageFactory.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/messagebus/IMessageFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..1679a6d63d3b005eb5a807f881299aa11cd377c3 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/messagebus/IMessageFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.messagebus; + +public interface IMessageFactory { + String DEFAULT_QUEUE_NAME = "records"; + String LEGAL_QUEUE_NAME = "legal"; + String INDEXER_QUEUE_NAME = "indexer"; + + void sendMessage(String msg); + + void sendMessage(String queueName, String msg); +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/middleware/IndexFilter.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/middleware/IndexFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..95bbd9be85eb877f5b5025ca2056c80eb14111da --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/middleware/IndexFilter.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.middleware; + +import com.google.common.base.Strings; +import lombok.extern.java.Log; +import org.apache.http.HttpStatus; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.http.ResponseHeaders; +import org.opengroup.osdu.core.common.model.search.DeploymentEnvironment; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Log +@Component +public class IndexFilter implements Filter { + + @Inject + private DpsHeaders dpsHeaders; + + @Inject + private IRequestInfo requestInfo; + + @Value("${DEPLOYMENT_ENVIRONMENT}") + private String DEPLOYMENT_ENVIRONMENT; + + private FilterConfig filterConfig; + + private static final String PATH_SWAGGER = "/swagger.json"; + private static final String PATH_TASK_HANDLERS = "task-handlers"; + private static final String PATH_CRON_HANDLERS = "cron-handlers"; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + String uri = httpRequest.getRequestURI().toLowerCase(); + + if (httpRequest.getMethod().equalsIgnoreCase(HttpMethod.POST.name()) && uri.contains(PATH_TASK_HANDLERS)) { + if (DeploymentEnvironment.valueOf(DEPLOYMENT_ENVIRONMENT) != DeploymentEnvironment.LOCAL) { + checkWorkerApiAccess(requestInfo); + } + } + + if (httpRequest.getMethod().equalsIgnoreCase(HttpMethod.GET.name()) && uri.contains(PATH_CRON_HANDLERS)) { + checkWorkerApiAccess(requestInfo); + } + +// if (!httpRequest.isSecure()) { +// throw new AppException(302, "Redirect", "HTTP is not supported. Use HTTPS."); +// } + + filterChain.doFilter(servletRequest, servletResponse); + + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + Map<String, List<Object>> standardHeaders = ResponseHeaders.STANDARD_RESPONSE_HEADERS; + for (Map.Entry<String, List<Object>> header : standardHeaders.entrySet()) { + httpResponse.addHeader(header.getKey(), header.getValue().toString()); + } + if (httpResponse.getHeader(DpsHeaders.CORRELATION_ID) == null) { + httpResponse.addHeader(DpsHeaders.CORRELATION_ID, dpsHeaders.getCorrelationId()); + } + + } + + @Override + public void destroy() { + } + + private void checkWorkerApiAccess(IRequestInfo requestInfo) { +// if (requestInfo.isTaskQueueRequest()) return; +// throw AppException.createForbidden("invalid user agent, AppEngine Task Queue only"); + } + + private List<String> validateAccountId(DpsHeaders requestHeaders) { + String accountHeader = requestHeaders.getPartitionIdWithFallbackToAccountId(); + String debuggingInfo = String.format("%s:%s", DpsHeaders.DATA_PARTITION_ID, accountHeader); + + if (Strings.isNullOrEmpty(accountHeader)) { + throw new AppException(HttpStatus.SC_BAD_REQUEST, "Bad request", "invalid or empty data partition", debuggingInfo); + } + + List<String> dataPartitions = Arrays.asList(accountHeader.trim().split("\\s*,\\s*")); + if (dataPartitions.isEmpty() || dataPartitions.size() > 1) { + throw new AppException(HttpStatus.SC_BAD_REQUEST, "Bad request", "invalid or empty data partition", debuggingInfo); + } + return dataPartitions; + } + +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreCredential.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreCredential.java new file mode 100644 index 0000000000000000000000000000000000000000..c3285bd78e05d172f3923a667fa5593be86fdb5c --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreCredential.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.persistence; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.iam.v1.Iam; +import com.google.api.services.iam.v1.Iam.Projects.ServiceAccounts.SignJwt; +import com.google.api.services.iam.v1.IamScopes; +import com.google.api.services.iam.v1.model.SignJwtRequest; +import com.google.api.services.iam.v1.model.SignJwtResponse; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.gson.JsonObject; +import java.util.Collections; +import java.util.Date; +import org.apache.commons.lang3.time.DateUtils; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.util.Crc32c; +import org.opengroup.osdu.indexer.cache.DatastoreCredentialCache; + +public class DatastoreCredential extends GoogleCredentials { + + private static final long serialVersionUID = 8344377091688956815L; + private static final JsonFactory JSON_FACTORY = new JacksonFactory(); + private Iam iam; + + private final TenantInfo tenant; + private final DatastoreCredentialCache cache; + + protected DatastoreCredential(TenantInfo tenant, DatastoreCredentialCache cache) { + this.tenant = tenant; + this.cache = cache; + } + + @Override + public AccessToken refreshAccessToken() { + + String cacheKey = this.getCacheKey(); + + AccessToken accessToken = this.cache.get(cacheKey); + + if (accessToken != null) { + return accessToken; + } + + try { + SignJwtRequest signJwtRequest = new SignJwtRequest(); + signJwtRequest.setPayload(this.getPayload()); + + String serviceAccountName = String.format("projects/-/serviceAccounts/%s", this.tenant.getServiceAccount()); + + SignJwt signJwt = this.getIam().projects().serviceAccounts().signJwt(serviceAccountName, signJwtRequest); + + SignJwtResponse signJwtResponse = signJwt.execute(); + String signedJwt = signJwtResponse.getSignedJwt(); + + accessToken = new AccessToken(signedJwt, DateUtils.addSeconds(new Date(), 3600)); + + this.cache.put(cacheKey, accessToken); + + return accessToken; + } catch (Exception e) { + throw new RuntimeException("Error creating datastore credential", e); + } + } + + private String getPayload() { + JsonObject payload = new JsonObject(); + payload.addProperty("iss", this.tenant.getServiceAccount()); + payload.addProperty("sub", this.tenant.getServiceAccount()); + payload.addProperty("aud", "https://datastore.googleapis.com/google.datastore.v1.Datastore"); + payload.addProperty("iat", System.currentTimeMillis() / 1000); + + return payload.toString(); + } + + protected void setIam(Iam iam) { + this.iam = iam; + } + + private Iam getIam() throws Exception { + if (this.iam == null) { + + GoogleCredential credential = GoogleCredential.getApplicationDefault(); + if (credential.createScopedRequired()) { + credential = credential.createScoped(Collections.singletonList(IamScopes.CLOUD_PLATFORM)); + } + + Iam.Builder builder = new Iam.Builder(GoogleNetHttpTransport.newTrustedTransport(), JSON_FACTORY, + credential).setApplicationName("Search Service"); + + this.iam = builder.build(); + } + return this.iam; + } + + + private String getCacheKey() { + return Crc32c.hashToBase64EncodedString(String.format("datastoreCredential:%s", this.tenant.getName())); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreFactory.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..db90ee7efb2dfee6fb3995d09b0fe158a03ad9e1 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/DatastoreFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.persistence; + +import com.google.api.gax.retrying.RetrySettings; +import com.google.cloud.TransportOptions; +import com.google.cloud.datastore.Datastore; +import com.google.cloud.datastore.DatastoreOptions; +import com.google.cloud.http.HttpTransportOptions; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.indexer.cache.DatastoreCredentialCache; +import org.springframework.stereotype.Component; +import org.threeten.bp.Duration; + +import javax.inject.Inject; +import java.util.HashMap; +import java.util.Map; + +@Component +public class DatastoreFactory { + + @Inject + private DatastoreCredentialCache cache; + + private static Map<String, Datastore> DATASTORE_CLIENTS = new HashMap<>(); + + private static final RetrySettings RETRY_SETTINGS = RetrySettings.newBuilder() + .setMaxAttempts(6) + .setInitialRetryDelay(Duration.ofSeconds(10)) + .setMaxRetryDelay(Duration.ofSeconds(32)) + .setRetryDelayMultiplier(2.0) + .setTotalTimeout(Duration.ofSeconds(50)) + .setInitialRpcTimeout(Duration.ofSeconds(50)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofSeconds(50)) + .build(); + + private static final TransportOptions TRANSPORT_OPTIONS = HttpTransportOptions.newBuilder() + .setReadTimeout(30000) + .build(); + + public Datastore getDatastoreInstance(TenantInfo tenantInfo) { + if (DATASTORE_CLIENTS.get(tenantInfo.getName()) == null) { + Datastore googleDatastore = DatastoreOptions.newBuilder() + .setCredentials(new DatastoreCredential(tenantInfo, this.cache)) + .setRetrySettings(RETRY_SETTINGS) + .setTransportOptions(TRANSPORT_OPTIONS) + .setNamespace(tenantInfo.getName()) + .setProjectId(tenantInfo.getProjectId()) + .build().getService(); + DATASTORE_CLIENTS.put(tenantInfo.getName(), googleDatastore); + } + return DATASTORE_CLIENTS.get(tenantInfo.getName()); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/ElasticRepositoryMongoDB.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/ElasticRepositoryMongoDB.java new file mode 100644 index 0000000000000000000000000000000000000000..a3ab89c5d9dc1824af87ad823e547befbc6c4fa8 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/ElasticRepositoryMongoDB.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.persistence; + +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import java.util.Objects; +import org.apache.http.HttpStatus; +import org.bson.Document; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.search.ClusterSettings; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.provider.interfaces.IElasticRepository; +import org.opengroup.osdu.core.common.provider.interfaces.IKmsClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class ElasticRepositoryMongoDB implements IElasticRepository { + + private static final Logger LOG = LoggerFactory.getLogger(ElasticRepositoryMongoDB.class); + + private static final String SCHEMA_DATABASE = "local"; + private static final String SEARCH_SETTINGS = "SearchSettings"; + private static final String HOST = "host"; + private static final String PORT = "port"; + private static final String XPACK_RESTCLIENT_CONFIGURATION = "configuration"; + + @Autowired + private MongoDdmsClient mongoClient; + + @Autowired + private IKmsClient kmsClient; + + @Override + public ClusterSettings getElasticClusterSettings(TenantInfo tenantInfo) { + MongoCollection<Document> mongoCollection = mongoClient + .getMongoCollection(SCHEMA_DATABASE, SEARCH_SETTINGS); + + FindIterable<Document> results = mongoCollection.find(); + + if (Objects.isNull(results) && Objects.isNull(results.first())) { + LOG.error(String.format("Collection \'%s\' is empty.", results)); + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Cluster setting fetch error", + "An error has occurred fetching cluster settings from the database."); + } + + Document document = results.first(); + + String encryptedConfiguration = document.get(XPACK_RESTCLIENT_CONFIGURATION).toString(); + String encryptedHost = document.get(HOST).toString(); + String encryptedPort = document.get(PORT).toString(); + + try { + + String host = encryptedHost;//this.kmsClient.decryptString(encryptedHost); + String portString = encryptedPort;//this.kmsClient.decryptString(encryptedPort); + String usernameAndPassword = encryptedConfiguration;//this.kmsClient.decryptString(encryptedConfiguration); + + int port = Integer.parseInt(portString); + ClusterSettings clusterSettings = new ClusterSettings(host, port, usernameAndPassword); + clusterSettings.setHttps(false); + return clusterSettings; + + } catch (Exception e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Cluster setting fetch error", + "An error has occurred fetching cluster settings from the database.", e); + } + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/MongoDdmsClient.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/MongoDdmsClient.java new file mode 100644 index 0000000000000000000000000000000000000000..43b3fd5b1b519006aa781af227f594c81f2d459a --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/persistence/MongoDdmsClient.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.persistence; + +import com.mongodb.client.MongoCollection; +import org.bson.Document; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.indexer.util.MongoClientHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MongoDdmsClient { + + @Autowired + private MongoClientHandler mongoClientHandler; + + @Autowired + private TenantInfo tenantInfo; + + public MongoCollection<Document> getMongoCollection(String dbName, String collectionName) { + return mongoClientHandler.getMongoClient().getDatabase(dbName) + .getCollection(collectionName); + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/publish/PublisherImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/publish/PublisherImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d2a4d74cd3ee5969714eb84317bcef27813bcb75 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/publish/PublisherImpl.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.publish; + + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.elasticsearch.common.Strings; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.indexer.JobStatus; +import org.opengroup.osdu.core.common.model.indexer.RecordStatus; +import org.opengroup.osdu.core.common.model.search.RecordChangedMessages; +import org.opengroup.osdu.indexer.messagebus.IMessageFactory; +import org.opengroup.osdu.indexer.provider.interfaces.IPublisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +public class PublisherImpl implements IPublisher { + + private static final Logger LOG = LoggerFactory.getLogger(PublisherImpl.class); + + @Autowired + IMessageFactory mq; + + + @Override + public void publishStatusChangedTagsToTopic(DpsHeaders headers, JobStatus indexerBatchStatus) throws Exception { + + String tenant = headers.getPartitionId(); + if (Strings.isNullOrEmpty(tenant)) { + tenant = headers.getAccountId(); + } + + Map<String, String> message = new HashMap<>(); + message.put(tenant, headers.getPartitionIdWithFallbackToAccountId()); + headers.addCorrelationIdIfMissing(); + message.put(DpsHeaders.CORRELATION_ID, headers.getCorrelationId()); + + RecordChangedMessages recordChangedMessages = getRecordChangedMessage(headers, indexerBatchStatus); + message.put("data", recordChangedMessages.toString()); + + try { + LOG.info("Indexer publishes message " + headers.getCorrelationId()); + mq.sendMessage(IMessageFactory.INDEXER_QUEUE_NAME, message.toString()); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + + private RecordChangedMessages getRecordChangedMessage(DpsHeaders headers, JobStatus indexerBatchStatus) { + + Gson gson = new GsonBuilder().create(); + Map<String, String> attributesMap = new HashMap<>(); + Type listType = new TypeToken<List<RecordStatus>>() { + }.getType(); + + JsonElement statusChangedTagsJson = gson.toJsonTree(indexerBatchStatus.getStatusesList(), listType); + String statusChangedTagsData = (statusChangedTagsJson.toString()); + + String tenant = headers.getPartitionId(); + // This code it to provide backward compatibility to slb-account-id + if (!Strings.isNullOrEmpty(tenant)) { + attributesMap.put(DpsHeaders.DATA_PARTITION_ID, headers.getPartitionIdWithFallbackToAccountId()); + } else { + attributesMap.put(DpsHeaders.ACCOUNT_ID, headers.getPartitionIdWithFallbackToAccountId()); + } + headers.addCorrelationIdIfMissing(); + attributesMap.put(DpsHeaders.CORRELATION_ID, headers.getCorrelationId()); + + RecordChangedMessages recordChangedMessages = new RecordChangedMessages(); + // statusChangedTagsData is not ByteString but String + recordChangedMessages.setData(statusChangedTagsData); + recordChangedMessages.setAttributes(attributesMap); + + return recordChangedMessages; + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/security/GSuiteSecurityConfig.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/security/GSuiteSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..7927d6f3b55656168683b497d67597d446f0246b --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/security/GSuiteSecurityConfig.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.security; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class GSuiteSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http.httpBasic().disable() + .csrf().disable(); //disable default authN. AuthN handled by endpoints proxy + + } + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/api-docs") + .antMatchers("/swagger"); + } + +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..0b558b08ccb00f8666d4e98ecf4961bef37d69c9 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java @@ -0,0 +1,525 @@ +// Copyright 2017-2019, Schlumberger +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.opengroup.osdu.indexer.service; + +import com.google.gson.Gson; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import javax.inject.Inject; +import org.apache.http.HttpStatus; +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.rest.RestStatus; +import org.opengroup.osdu.core.common.Constants; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.entitlements.Acl; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.http.RequestStatus; +import org.opengroup.osdu.core.common.model.indexer.IndexSchema; +import org.opengroup.osdu.core.common.model.indexer.IndexingStatus; +import org.opengroup.osdu.core.common.model.indexer.JobStatus; +import org.opengroup.osdu.core.common.model.indexer.OperationType; +import org.opengroup.osdu.core.common.model.indexer.RecordIndexerPayload; +import org.opengroup.osdu.core.common.model.indexer.RecordInfo; +import org.opengroup.osdu.core.common.model.indexer.RecordStatus; +import org.opengroup.osdu.core.common.model.indexer.Records; +import org.opengroup.osdu.core.common.model.search.RecordChangedMessages; +import org.opengroup.osdu.core.common.model.search.RecordMetaAttribute; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.search.ElasticIndexNameResolver; +import org.opengroup.osdu.core.common.search.IndicesService; +import org.opengroup.osdu.indexer.logging.AuditLogger; +import org.opengroup.osdu.indexer.provider.interfaces.IPublisher; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.opengroup.osdu.indexer.util.IndexerQueueTaskBuilder; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; + +@Service +@Primary +public class IndexerServiceImpl implements IndexerService { + + private static final TimeValue BULK_REQUEST_TIMEOUT = TimeValue.timeValueMinutes(1); + + private static final List<RestStatus> RETRY_ELASTIC_EXCEPTION = new ArrayList<>(Arrays.asList(RestStatus.TOO_MANY_REQUESTS, RestStatus.BAD_GATEWAY, RestStatus.SERVICE_UNAVAILABLE)); + + private final Gson gson = new Gson(); + + @Inject + private JaxRsDpsLog jaxRsDpsLog; + @Inject + private AuditLogger auditLogger; + @Inject + private StorageService storageService; + @Inject + private IndexSchemaService schemaService; + @Inject + private IndicesService indicesService; + @Inject + private IndexerMappingService mappingService; + @Inject + private IPublisher progressPublisher; + @Inject + private ElasticClientHandler elasticClientHandler; + @Inject + private IndexerQueueTaskBuilder indexerQueueTaskBuilder; + @Inject + private ElasticIndexNameResolver elasticIndexNameResolver; + @Inject + private StorageIndexerPayloadMapper storageIndexerPayloadMapper; + @Inject + private IRequestInfo requestInfo; + @Inject + private JobStatus jobStatus; + + private DpsHeaders headers; + + @Override + public JobStatus processRecordChangedMessages(RecordChangedMessages message, List<RecordInfo> recordInfos) throws Exception { + + // this should not happen + if (recordInfos.size() == 0) return null; + + String errorMessage = ""; + List<String> retryRecordIds = new LinkedList<>(); + + // get auth header with service account Authorization + this.headers = this.requestInfo.getHeaders(); + String authorization = message.getAttributes().get("authorization"); + headers.put("authorization", authorization); + // initialize status for all messages. + this.jobStatus.initialize(recordInfos); + + try { + // get upsert records + Map<String, Map<String, OperationType>> upsertRecordMap = RecordInfo.getUpsertRecordIds(recordInfos); + if (upsertRecordMap != null && !upsertRecordMap.isEmpty()) { + List<String> upsertFailureRecordIds = processUpsertRecords(upsertRecordMap); + retryRecordIds.addAll(upsertFailureRecordIds); + } + + // get delete records + Map<String, List<String>> deleteRecordMap = RecordInfo.getDeleteRecordIds(recordInfos); + if (deleteRecordMap != null && !deleteRecordMap.isEmpty()) { + List<String> deleteFailureRecordIds = processDeleteRecords(deleteRecordMap); + retryRecordIds.addAll(deleteFailureRecordIds); + } + + // process schema change messages + Map<String, OperationType> schemaMsgs = RecordInfo.getSchemaMsgs(recordInfos); + if (schemaMsgs != null && !schemaMsgs.isEmpty()) { + this.schemaService.processSchemaMessages(schemaMsgs); + } + + // process failed records + if (retryRecordIds.size() > 0) { + retryAndEnqueueFailedRecords(recordInfos, retryRecordIds, message); + } + } catch (IOException e) { + errorMessage = e.getMessage(); + throw new AppException(HttpStatus.SC_GATEWAY_TIMEOUT, "Internal communication failure", errorMessage, e); + } catch (AppException e) { + errorMessage = e.getMessage(); + throw e; + } catch (Exception e) { + errorMessage = "error indexing records"; + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Unknown error", "An unknown error has occurred.", e); + } finally { + this.jobStatus.finalizeRecordStatus(errorMessage); + this.updateAuditLog(); + this.progressPublisher.publishStatusChangedTagsToTopic(this.headers, this.jobStatus); + } + + return jobStatus; + } + + @Override + public void processSchemaMessages(List<RecordInfo> recordInfos) throws IOException { + Map<String, OperationType> schemaMsgs = RecordInfo.getSchemaMsgs(recordInfos); + if (schemaMsgs != null && !schemaMsgs.isEmpty()) { + try (RestHighLevelClient restClient = elasticClientHandler.createRestClient()) { + schemaMsgs.entrySet().forEach(msg -> { + try { + processSchemaEvents(restClient, msg); + } catch (IOException | ElasticsearchStatusException e) { + throw new AppException(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR.value(), "unable to process schema delete", e.getMessage()); + } + }); + } + } + } + + private void processSchemaEvents(RestHighLevelClient restClient, + Map.Entry<String, OperationType> msg) throws IOException, ElasticsearchStatusException { + String kind = msg.getKey(); + String index = elasticIndexNameResolver.getIndexNameFromKind(kind); + + boolean indexExist = indicesService.isIndexExist(restClient, index); + if (indexExist && msg.getValue() == OperationType.purge_schema) { + indicesService.deleteIndex(restClient, index); + } + } + + private List<String> processUpsertRecords(Map<String, Map<String, OperationType>> upsertRecordMap) throws Exception { + // get schema for kind + Map<String, IndexSchema> schemas = this.getSchema(upsertRecordMap); + + if (schemas.isEmpty()) return new LinkedList<>(); + + // get recordIds with valid upsert index-status + List<String> recordIds = this.jobStatus.getIdsByValidUpsertIndexingStatus(); + + if (recordIds.isEmpty()) return new LinkedList<>(); + + // get records via storage api + Records storageRecords = this.storageService.getStorageRecords(recordIds); + List<String> failedOrRetryRecordIds = new LinkedList<>(storageRecords.getMissingRetryRecords()); + + // map storage records to indexer payload + RecordIndexerPayload recordIndexerPayload = this.getIndexerPayload(upsertRecordMap, schemas, storageRecords); + + jaxRsDpsLog.info(String.format("records change messages received : %s | valid storage bulk records: %s | valid index payload: %s", recordIds.size(), storageRecords.getRecords().size(), recordIndexerPayload.getRecords().size())); + + // index records + failedOrRetryRecordIds.addAll(processElasticMappingAndUpsertRecords(recordIndexerPayload)); + + return failedOrRetryRecordIds; + } + + private Map<String, IndexSchema> getSchema(Map<String, Map<String, OperationType>> upsertRecordMap) { + + Map<String, IndexSchema> schemas = new HashMap<>(); + + try { + for (Map.Entry<String, Map<String, OperationType>> entry : upsertRecordMap.entrySet()) { + + String kind = entry.getKey(); + IndexSchema schemaObj = this.schemaService.getIndexerInputSchema(kind, false); + if (schemaObj.isDataSchemaMissing()) { + this.jobStatus.addOrUpdateRecordStatus(entry.getValue().keySet(), IndexingStatus.WARN, HttpStatus.SC_NOT_FOUND, "schema not found", String.format("schema not found | kind: %s", kind)); + } + + schemas.put(kind, schemaObj); + } + } catch (AppException e) { + throw e; + } catch (Exception e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Get schema error", "An error has occurred while getting schema", e); + } + + return schemas; + } + + private RecordIndexerPayload getIndexerPayload(Map<String, Map<String, OperationType>> upsertRecordMap, Map<String, IndexSchema> kindSchemaMap, Records records) { + List<Records.Entity> storageValidRecords = records.getRecords(); + List<RecordIndexerPayload.Record> indexerPayload = new ArrayList<>(); + List<IndexSchema> schemas = new ArrayList<>(); + + for (Records.Entity storageRecord : storageValidRecords) { + + Map<String, OperationType> idOperationMap = upsertRecordMap.get(storageRecord.getKind()); + + // skip if storage returned record with same id but different kind + if (idOperationMap == null) { + String message = String.format("storage service returned incorrect record | requested kind: %s | received kind: %s", this.jobStatus.getRecordKindById(storageRecord.getId()), storageRecord.getKind()); + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.SKIP, RequestStatus.STORAGE_CONFLICT, message, String.format("%s | record-id: %s", message, storageRecord.getId())); + continue; + } + + IndexSchema schema = kindSchemaMap.get(storageRecord.getKind()); + schemas.add(schema); + + // skip indexing of records if data block is empty + RecordIndexerPayload.Record document = prepareIndexerPayload(schema, storageRecord, idOperationMap); + if (document != null) { + indexerPayload.add(document); + } + } + + // this should only happen if storage service returned WRONG records with kind for all the records in the messages + if (indexerPayload.isEmpty()) { + throw new AppException(RequestStatus.STORAGE_CONFLICT, "Indexer error", "upsert record failed, storage service returned incorrect records"); + } + + return RecordIndexerPayload.builder().records(indexerPayload).schemas(schemas).build(); + } + + private RecordIndexerPayload.Record prepareIndexerPayload(IndexSchema schemaObj, Records.Entity storageRecord, Map<String, OperationType> idToOperationMap) { + + RecordIndexerPayload.Record document = null; + + try { + Map<String, Object> storageRecordData = storageRecord.getData(); + document = new RecordIndexerPayload.Record(); + if (storageRecordData == null || storageRecordData.isEmpty()) { + String message = "empty or null data block found in the storage record"; + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.WARN, HttpStatus.SC_NOT_FOUND, message, String.format("record-id: %s | %s", storageRecord.getId(), message)); + } else if (schemaObj.isDataSchemaMissing()) { + document.setSchemaMissing(true); + } else { + Map<String, Object> dataMap = this.storageIndexerPayloadMapper.mapDataPayload(schemaObj, storageRecordData, storageRecord.getId()); + if (dataMap.isEmpty()) { + document.setMappingMismatch(true); + String message = String.format("complete schema mismatch: none of the data attribute can be mapped | data: %s", storageRecordData); + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.WARN, HttpStatus.SC_NOT_FOUND, message, String.format("record-id: %s | %s", storageRecord.getId(), message)); + } + document.setData(dataMap); + } + } catch (AppException e) { + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.FAIL, HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + jaxRsDpsLog.warning(String.format("record-id: %s | %s", storageRecord.getId(), e.getMessage()), e); + } catch (Exception e) { + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.FAIL, HttpStatus.SC_INTERNAL_SERVER_ERROR, String.format("error parsing records against schema, error-message: %s", e.getMessage())); + jaxRsDpsLog.error(String.format("record-id: %s | error parsing records against schema, error-message: %s", storageRecord.getId(), e.getMessage()), e); + } + + try { + // index individual parts of kind + String[] kindParts = storageRecord.getKind().split(":"); + + document.setKind(storageRecord.getKind()); + document.setNamespace(kindParts[0] + ":" + kindParts[1]); + document.setType(kindParts[2]); + document.setId(storageRecord.getId()); + document.setVersion(storageRecord.getVersion()); + document.setAcl(storageRecord.getAcl()); + document.setLegal(storageRecord.getLegal()); + RecordStatus recordStatus = this.jobStatus.getJobStatusByRecordId(storageRecord.getId()); + if (recordStatus.getIndexProgress().getStatusCode() == 0) { + recordStatus.getIndexProgress().setStatusCode(HttpStatus.SC_OK); + } + document.setIndexProgress(recordStatus.getIndexProgress()); + if (storageRecord.getAncestry() != null) document.setAncestry(storageRecord.getAncestry()); + document.setOperationType(idToOperationMap.get(storageRecord.getId())); + } catch (Exception e) { + this.jobStatus.addOrUpdateRecordStatus(storageRecord.getId(), IndexingStatus.FAIL, HttpStatus.SC_INTERNAL_SERVER_ERROR, String.format("error parsing meta data, error-message: %s", e.getMessage())); + jaxRsDpsLog.error(String.format("record-id: %s | error parsing meta data, error-message: %s", storageRecord.getId(), e.getMessage()), e); + } + return document; + } + + private List<String> processElasticMappingAndUpsertRecords(RecordIndexerPayload recordIndexerPayload) throws Exception { + + try (RestHighLevelClient restClient = this.elasticClientHandler.createRestClient()) { + List<IndexSchema> schemas = recordIndexerPayload.getSchemas(); + if (schemas == null || schemas.isEmpty()) { + return new LinkedList<>(); + } + + // process the schema + this.cacheOrCreateElasticMapping(schemas, restClient); + + // process the records + return this.upsertRecords(recordIndexerPayload.getRecords(), restClient); + } + } + + private void cacheOrCreateElasticMapping(List<IndexSchema> schemas, RestHighLevelClient restClient) throws Exception { + + for (IndexSchema schema : schemas) { + String index = this.elasticIndexNameResolver.getIndexNameFromKind(schema.getKind()); + + // check if index exist + if (this.indicesService.isIndexExist(restClient, index)) continue; + + // create index + Map<String, Object> mapping = this.mappingService.getIndexMappingFromRecordSchema(schema); + if (!this.indicesService.createIndex(restClient, index, null, schema.getType(), mapping)) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Elastic error", "Error creating index.", String.format("Failed to get confirmation from elastic server for index: %s", index)); + } + } + } + + private List<String> upsertRecords(List<RecordIndexerPayload.Record> records, RestHighLevelClient restClient) throws AppException { + if (records == null || records.isEmpty()) return new LinkedList<>(); + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.timeout(BULK_REQUEST_TIMEOUT); + + for (RecordIndexerPayload.Record record : records) { + if ((record.getData() == null || record.getData().isEmpty()) && !record.skippedDataIndexing()) { + // it will come here when schema is missing + // TODO: rollback once we know what is causing the problem + jaxRsDpsLog.warning(String.format("data not found for record: %s", record)); + } + + OperationType operation = record.getOperationType(); + Map<String, Object> sourceMap = getSourceMap(record); + String index = this.elasticIndexNameResolver.getIndexNameFromKind(record.getKind()); + + if (operation == OperationType.create) { + IndexRequest indexRequest = new IndexRequest(index, record.getType(), record.getId()).source(this.gson.toJson(sourceMap), XContentType.JSON); + bulkRequest.add(indexRequest); + } else if (operation == OperationType.update) { + UpdateRequest updateRequest = new UpdateRequest(index, record.getType(), record.getId()).upsert(this.gson.toJson(sourceMap), XContentType.JSON); + bulkRequest.add(updateRequest); + } + } + + return processBulkRequest(restClient, bulkRequest); + } + + private List<String> processDeleteRecords(Map<String, List<String>> deleteRecordMap) throws Exception { + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.timeout(BULK_REQUEST_TIMEOUT); + + for (Map.Entry<String, List<String>> record : deleteRecordMap.entrySet()) { + + String[] kindParts = record.getKey().split(":"); + String type = kindParts[2]; + + String index = this.elasticIndexNameResolver.getIndexNameFromKind(record.getKey()); + + for (String id : record.getValue()) { + DeleteRequest deleteRequest = new DeleteRequest(index, type, id); + bulkRequest.add(deleteRequest); + } + } + + try (RestHighLevelClient restClient = this.elasticClientHandler.createRestClient()) { + return processBulkRequest(restClient, bulkRequest); + } + } + + private List<String> processBulkRequest(RestHighLevelClient restClient, BulkRequest bulkRequest) throws AppException { + + List<String> failureRecordIds = new LinkedList<>(); + if (bulkRequest.numberOfActions() == 0) return failureRecordIds; + + + + try { + BulkResponse bulkResponse = restClient.bulk(bulkRequest, RequestOptions.DEFAULT); + + // log failed bulk requests + ArrayList<String> bulkFailures = new ArrayList<>(); + int succeededResponses = 0; + int failedResponses = 0; + + for (BulkItemResponse bulkItemResponse : bulkResponse.getItems()) { + if (bulkItemResponse.isFailed()) { + BulkItemResponse.Failure failure = bulkItemResponse.getFailure(); + bulkFailures.add(String.format("elasticsearch bulk service status: %s id: %s message: %s", failure.getStatus(), failure.getId(), failure.getMessage())); + this.jobStatus.addOrUpdateRecordStatus(bulkItemResponse.getId(), IndexingStatus.FAIL, failure.getStatus().getStatus(), bulkItemResponse.getFailureMessage()); + if (RETRY_ELASTIC_EXCEPTION.contains(bulkItemResponse.status())) { + failureRecordIds.add(bulkItemResponse.getId()); + } + failedResponses++; + } else { + succeededResponses++; + this.jobStatus.addOrUpdateRecordStatus(bulkItemResponse.getId(), IndexingStatus.SUCCESS, HttpStatus.SC_OK, "Indexed Successfully"); + } + } + if (!bulkFailures.isEmpty()) this.jaxRsDpsLog.warning(bulkFailures); + + jaxRsDpsLog.info(String.format("records in elasticsearch service bulk request: %s | successful: %s | failed: %s", bulkRequest.numberOfActions(), succeededResponses, failedResponses)); + } catch (IOException e) { + // throw explicit 504 for IOException + throw new AppException(HttpStatus.SC_GATEWAY_TIMEOUT, "Elastic error", "Request cannot be completed in specified time.", e); + } catch (ElasticsearchStatusException e) { + throw new AppException(e.status().getStatus(), "Elastic error", e.getMessage(), e); + } catch (Exception e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Elastic error", "Error indexing records.", e); + } + return failureRecordIds; + } + + private Map<String, Object> getSourceMap(RecordIndexerPayload.Record record) { + + Map<String, Object> indexerPayload = new HashMap<>(); + + // get the key and get the corresponding object from the individualRecord object + if (record.getData() != null) { + Map<String, Object> data = new HashMap<>(); + for (Map.Entry<String, Object> entry : record.getData().entrySet()) { + data.put(entry.getKey(), entry.getValue()); + } + indexerPayload.put(Constants.DATA, data); + } + + indexerPayload.put(RecordMetaAttribute.ID.getValue(), record.getId()); + indexerPayload.put(RecordMetaAttribute.KIND.getValue(), record.getKind()); + indexerPayload.put(RecordMetaAttribute.NAMESPACE.getValue(), record.getNamespace()); + indexerPayload.put(RecordMetaAttribute.TYPE.getValue(), record.getType()); + indexerPayload.put(RecordMetaAttribute.VERSION.getValue(), record.getVersion()); + indexerPayload.put(RecordMetaAttribute.ACL.getValue(), record.getAcl()); + indexerPayload.put(RecordMetaAttribute.X_ACL.getValue(), Acl.flattenAcl(record.getAcl())); + indexerPayload.put(RecordMetaAttribute.LEGAL.getValue(), record.getLegal()); + indexerPayload.put(RecordMetaAttribute.INDEX_STATUS.getValue(), record.getIndexProgress()); + if (record.getAncestry() != null) { + indexerPayload.put(RecordMetaAttribute.ANCESTRY.getValue(), record.getAncestry()); + } + return indexerPayload; + } + + private void retryAndEnqueueFailedRecords(List<RecordInfo> recordInfos, List<String> failuresRecordIds, RecordChangedMessages message) throws IOException { + + jaxRsDpsLog.info(String.format("queuing bulk failed records back to task-queue for retry | count: %s | records: %s", failuresRecordIds.size(), failuresRecordIds)); + List<RecordInfo> retryRecordInfos = new LinkedList<>(); + for (String recordId : failuresRecordIds) { + for (RecordInfo origMessage : recordInfos) { + if (origMessage.getId().equalsIgnoreCase(recordId)) { + retryRecordInfos.add(origMessage); + } + } + } + + RecordChangedMessages newMessage = RecordChangedMessages.builder() + .messageId(message.getMessageId()) + .publishTime(message.getPublishTime()) + .data(this.gson.toJson(retryRecordInfos)) + .attributes(message.getAttributes()).build(); + + String payLoad = this.gson.toJson(newMessage); + this.indexerQueueTaskBuilder.createWorkerTask(payLoad, this.headers); + } + + private void updateAuditLog() { + logAuditEvents(OperationType.create, this.auditLogger::indexCreateRecordSuccess, this.auditLogger::indexCreateRecordFail); + logAuditEvents(OperationType.update, this.auditLogger::indexUpdateRecordSuccess, this.auditLogger::indexUpdateRecordFail); + logAuditEvents(OperationType.purge, this.auditLogger::indexPurgeRecordSuccess, this.auditLogger::indexPurgeRecordFail); + logAuditEvents(OperationType.delete, this.auditLogger::indexDeleteRecordSuccess, this.auditLogger::indexDeleteRecordFail); + } + + private void logAuditEvents(OperationType operationType, Consumer<List<String>> successEvent, Consumer<List<String>> failedEvent) { + List<RecordStatus> succeededRecords = this.jobStatus.getRecordStatuses(IndexingStatus.SUCCESS, operationType); + if(!succeededRecords.isEmpty()) { + successEvent.accept(succeededRecords.stream().map(RecordStatus::succeededAuditLogMessage).collect(Collectors.toList())); + } + List<RecordStatus> skippedRecords = this.jobStatus.getRecordStatuses(IndexingStatus.SKIP, operationType); + List<RecordStatus> failedRecords = this.jobStatus.getRecordStatuses(IndexingStatus.FAIL, operationType); + failedRecords.addAll(skippedRecords); + if(!failedRecords.isEmpty()) { + failedEvent.accept(failedRecords.stream().map(RecordStatus::failedAuditLogMessage).collect(Collectors.toList())); + } + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/DpsHeaderFactoryGcp.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/DpsHeaderFactoryGcp.java new file mode 100644 index 0000000000000000000000000000000000000000..0e3c201656b9c61bdcb537c552d16fd50bef9188 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/DpsHeaderFactoryGcp.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.inject.Inject; + +import com.google.common.base.Strings; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; + +import org.opengroup.osdu.core.gcp.model.AppEngineHeaders; +import org.opengroup.osdu.core.gcp.util.TraceIdExtractor; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +@Primary +public class DpsHeaderFactoryGcp extends DpsHeaders { + + @Inject + public DpsHeaderFactoryGcp(HttpServletRequest request) { + + Map<String, String> headers = Collections + .list(request.getHeaderNames()) + .stream() + .collect(Collectors.toMap(h -> h, request::getHeader)); + + String traceContext = headers.get(AppEngineHeaders.CLOUD_TRACE_CONTEXT); + + if(!Strings.isNullOrEmpty(traceContext)){ + headers.put(AppEngineHeaders.TRACE_ID, TraceIdExtractor.getTraceId(traceContext)); + } + + this.addFromMap(headers); + + // Add Correlation ID if missing + this.addCorrelationIdIfMissing(); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/MongoClientHandler.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/MongoClientHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b60ef1da6a11f4f21c4206448743ecb05e81c59e --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/MongoClientHandler.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import org.apache.http.HttpStatus; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class MongoClientHandler { + + private static final Logger LOG = LoggerFactory.getLogger( + org.opengroup.osdu.indexer.util.MongoClientHandler.class); + private static final String MONGO_PREFIX = "mongodb://"; + private static final String MONGO_OPTIONS = "retryWrites=true&w=majority&maxIdleTimeMS=10000"; + + private MongoClient mongoClient = null; + + @Value("${mongo.db.url:#{null}}") + private String dbUrl; + + @Value("${mongo.db.user:#{null}}") + private String dbUser; + + @Value("${mongo.db.password:#{null}}") + private String dbPassword; + + private MongoClient getOrInitMongoClient() throws RuntimeException { + if (mongoClient != null) { + return mongoClient; + } + + final String connectionString = String.format("%s%s:%s@%s/?%s", + MONGO_PREFIX, + dbUser, + dbPassword, + dbUrl, + MONGO_OPTIONS); + ConnectionString connString = new ConnectionString(connectionString); + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(connString) + .retryWrites(true) + .build(); + try { + mongoClient = MongoClients.create(settings); + } catch (Exception ex) { + LOG.error("Error connecting MongoDB", ex); + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Error connecting MongoDB", + ex.getMessage(), ex); + } + return mongoClient; + } + + public MongoClient getMongoClient() { + if (mongoClient == null) { + getOrInitMongoClient(); + } + return mongoClient; + } + +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/RequestInfoImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/RequestInfoImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..2396dd1e55b99ee7853d4e9b94e752be5a559261 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/RequestInfoImpl.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import com.google.common.base.Strings; +import lombok.extern.java.Log; +import org.apache.http.HttpStatus; +import org.opengroup.osdu.core.common.Constants; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.search.DeploymentEnvironment; +import org.opengroup.osdu.core.common.util.IServiceAccountJwtClient; +import org.opengroup.osdu.core.gcp.model.AppEngineHeaders; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +import javax.inject.Inject; +import java.util.Map; + +import static org.opengroup.osdu.core.common.model.http.DpsHeaders.AUTHORIZATION; + + +@Log +@Component +@RequestScope +public class RequestInfoImpl implements IRequestInfo { + + @Inject + private DpsHeaders dpsHeaders; + + @Inject + private IServiceAccountJwtClient serviceAccountJwtClient; + + @Inject + private TenantInfo tenantInfo; + + @Value("${DEPLOYMENT_ENVIRONMENT}") + private String DEPLOYMENT_ENVIRONMENT; + + private static final String expectedCronHeaderValue = "true"; + + @Override + public DpsHeaders getHeaders() { + + return this.dpsHeaders; + } + + @Override + public String getPartitionId() { + return this.dpsHeaders.getPartitionId(); + } + + @Override + public Map<String, String> getHeadersMap() { + return this.dpsHeaders.getHeaders(); + } + + @Override + public Map<String, String> getHeadersMapWithDwdAuthZ() { + return getHeadersWithDwdAuthZ().getHeaders(); + } + + @Override + public DpsHeaders getHeadersWithDwdAuthZ() { + // Update DpsHeaders so that service account creds are passed down + this.dpsHeaders.put(AUTHORIZATION, this.checkOrGetAuthorizationHeader()); + return this.dpsHeaders; + } + + @Override + public boolean isCronRequest() { + String appEngineCronHeader = this.dpsHeaders.getHeaders().getOrDefault(AppEngineHeaders.CRON_SERVICE, null); + return expectedCronHeaderValue.equalsIgnoreCase(appEngineCronHeader); + } + + @Override + public boolean isTaskQueueRequest() { + if (!this.dpsHeaders.getHeaders().containsKey(AppEngineHeaders.TASK_QUEUE_NAME)) return false; + + String queueId = this.dpsHeaders.getHeaders().get(AppEngineHeaders.TASK_QUEUE_NAME); + return queueId.endsWith(Constants.INDEXER_QUEUE_IDENTIFIER); + } + + public String checkOrGetAuthorizationHeader() { + if (DeploymentEnvironment.valueOf(DEPLOYMENT_ENVIRONMENT) == DeploymentEnvironment.LOCAL) { + String authHeader = this.dpsHeaders.getAuthorization(); + if (Strings.isNullOrEmpty(authHeader)) { + throw new AppException(HttpStatus.SC_UNAUTHORIZED, "Invalid authorization header", "Authorization token cannot be empty"); + } + String user = this.dpsHeaders.getUserEmail(); + if (Strings.isNullOrEmpty(user)) { + throw new AppException(HttpStatus.SC_UNAUTHORIZED, "Invalid user header", "User header cannot be empty"); + } + return authHeader; + } else { + return "Bearer " + this.serviceAccountJwtClient.getIdToken(tenantInfo.getName()); + } + } +} diff --git a/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImpl.java b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..6da39c9ed5a45a7f792936e2c7b88bdb8ee8e236 --- /dev/null +++ b/provider/indexer-reference/src/main/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImpl.java @@ -0,0 +1,196 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.iam.v1.Iam; +import com.google.api.services.iam.v1.IamScopes; +import com.google.api.services.iam.v1.model.SignJwtRequest; +import com.google.api.services.iam.v1.model.SignJwtResponse; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.search.IdToken; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.provider.interfaces.IJwtCache; +import org.opengroup.osdu.core.common.provider.interfaces.ITenantFactory; +import org.opengroup.osdu.core.common.util.IServiceAccountJwtClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; + +@Component +@RequestScope +public class ServiceAccountJwtGcpClientImpl implements IServiceAccountJwtClient { + + private static final String JWT_AUDIENCE = "https://www.googleapis.com/oauth2/v4/token"; + private static final String SERVICE_ACCOUNT_NAME_FORMAT = "projects/%s/serviceAccounts/%s"; + + private final JsonFactory JSON_FACTORY = new JacksonFactory(); + + private Iam iam; + + @Inject + private ITenantFactory tenantInfoServiceProvider; + @Inject + private IJwtCache cacheService; + @Inject + private JaxRsDpsLog log; + @Inject + private DpsHeaders dpsHeaders; + + @Value("${GOOGLE_AUDIENCES}") + public String GOOGLE_AUDIENCES; + + @Value("${INDEXER_HOST}") + public String INDEXER_HOST; + + public String getIdToken(String tenantName) { + this.log.info("Tenant name received for auth token is: " + tenantName); + TenantInfo tenant = this.tenantInfoServiceProvider.getTenantInfo(tenantName); + if (tenant == null) { + this.log.error("Invalid tenant name receiving from pubsub"); + throw new AppException(HttpStatus.SC_BAD_REQUEST, "Invalid tenant Name", "Invalid tenant Name from pubsub"); + } + try { + + IdToken cachedToken = (IdToken) this.cacheService.get(tenant.getServiceAccount()); + // Add the user to DpsHeaders directly + this.dpsHeaders.put(DpsHeaders.USER_EMAIL, tenant.getServiceAccount()); + + if (!IdToken.refreshToken(cachedToken)) { + return cachedToken.getTokenValue(); + } + + // Getting signed JWT + Map<String, Object> signJwtPayload = this.getJWTCreationPayload(tenant); + + SignJwtRequest signJwtRequest = new SignJwtRequest(); + signJwtRequest.setPayload(JSON_FACTORY.toString(signJwtPayload)); + + String serviceAccountName = String + .format(SERVICE_ACCOUNT_NAME_FORMAT, tenant.getProjectId(), tenant.getServiceAccount()); + + Iam.Projects.ServiceAccounts.SignJwt signJwt = this.getIam().projects().serviceAccounts() + .signJwt(serviceAccountName, signJwtRequest); + SignJwtResponse signJwtResponse = signJwt.execute(); + String signedJwt = signJwtResponse.getSignedJwt(); + + // Getting id token + List<NameValuePair> postParameters = new ArrayList<>(); + postParameters.add(new BasicNameValuePair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")); + postParameters.add(new BasicNameValuePair("assertion", signedJwt)); + + HttpPost post = new HttpPost(JWT_AUDIENCE); + post.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()); + post.setEntity(new UrlEncodedFormEntity(postParameters, "UTF-8")); + + try (CloseableHttpClient httpclient = HttpClients.createDefault(); + CloseableHttpResponse httpResponse = httpclient.execute(post)) { + JsonObject jsonContent = new JsonParser().parse(EntityUtils.toString(httpResponse.getEntity())) + .getAsJsonObject(); + + if (!jsonContent.has("id_token")) { + log.error(String.format("Google IAM response: %s", jsonContent.toString())); + throw new AppException(HttpStatus.SC_FORBIDDEN, "Access denied", + "The user is not authorized to perform this action"); + } + + String token = jsonContent.get("id_token").getAsString(); + IdToken idToken = IdToken.builder().tokenValue(token) + .expirationTimeMillis(JWT.decode(token).getExpiresAt().getTime()).build(); + + this.cacheService.put(tenant.getServiceAccount(), idToken); + + return token; + } + } catch (JWTDecodeException e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Persistence error", + "Invalid token, error decoding", e); + } catch (AppException e) { + throw e; + } catch (Exception e) { + throw new AppException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Persistence error", "Error generating token", + e); + } + } + + public Iam getIam() throws Exception { + + if (this.iam == null) { + HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + + // Authenticate using Google Application Default Credentials. + GoogleCredential credential = GoogleCredential.getApplicationDefault(); + if (credential.createScopedRequired()) { + List<String> scopes = new ArrayList<>(); + // Enable full Cloud Platform scope. + scopes.add(IamScopes.CLOUD_PLATFORM); + credential = credential.createScoped(scopes); + } + + // Create IAM API object associated with the authenticated transport. + this.iam = new Iam.Builder(httpTransport, JSON_FACTORY, credential) + .setApplicationName(INDEXER_HOST) + .build(); + } + + return this.iam; + } + + private Map<String, Object> getJWTCreationPayload(TenantInfo tenantInfo) { + + Map<String, Object> payload = new HashMap<>(); + String googleAudience = GOOGLE_AUDIENCES; + if (googleAudience.contains(",")) { + googleAudience = googleAudience.split(",")[0]; + } + payload.put("target_audience", googleAudience); + payload.put("exp", System.currentTimeMillis() / 1000 + 3600); + payload.put("iat", System.currentTimeMillis() / 1000); + payload.put("iss", tenantInfo.getServiceAccount()); + payload.put("aud", JWT_AUDIENCE); + + return payload; + } +} diff --git a/provider/indexer-reference/src/main/resources/application-dev.properties b/provider/indexer-reference/src/main/resources/application-dev.properties new file mode 100644 index 0000000000000000000000000000000000000000..09ea204df6cd8cb54bceb57f3b3b3e2d44613b34 --- /dev/null +++ b/provider/indexer-reference/src/main/resources/application-dev.properties @@ -0,0 +1,40 @@ +# +# Copyright 2020 Google LLC +# Copyright 2020 EPAM Systems, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +GOOGLE_CLOUD_PROJECT=nice-etching-277309 + +INDEXER_HOST=os-indexer-dot-nice-etching-277309.uc.r.appspot.com +STORAGE_HOSTNAME=os-storage-dot-nice-etching-277309.uc.r.appspot.com + +STORAGE_SCHEMA_HOST=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/schemas +STORAGE_QUERY_RECORD_HOST=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/query/records +STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/query/records:batch +STORAGE_RECORDS_BATCH_SIZE=20 + +INDEXER_QUEUE_HOST=https://os-indexer-queue-dot-nice-etching-277309.uc.r.appspot.com/_dps/task-handlers/enqueue + +AUTHORIZE_API=https://entitlements-dot-nice-etching-277309.uc.r.appspot.com/entitlements/v1 +LEGALTAG_API=https://os-legal-dot-nice-etching-277309.uc.r.appspot.com/api/legal/v1 +CRS_API=example.com + +## use below values for gcp: opendes +REDIS_GROUP_HOST=127.0.0.1 +REDIS_SEARCH_HOST=127.0.0.1 + +GOOGLE_AUDIENCES=689762842995-pv217jo3k8j803kk6gqf52qb5amos3a9.apps.googleusercontent.com + +mb.rabbitmq.uri=amqp://guest:guest@127.0.0.1:5672 diff --git a/provider/indexer-reference/src/main/resources/application.properties b/provider/indexer-reference/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..4d62d4abed0705ab986b75574fbbb0ec3e22f418 --- /dev/null +++ b/provider/indexer-reference/src/main/resources/application.properties @@ -0,0 +1,78 @@ +# +# Copyright 2020 Google LLC +# Copyright 2020 EPAM Systems, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LOG_PREFIX=indexer + +server.servlet.contextPath=/api/indexer/v2 +logging.level.org.springframework.web=DEBUG +server.port=8080 +JAVA_OPTS=-Xms3072m -Xmx3072m +JAVA_GC_OPTS=-XX:+UseG1GC -XX:+UseStringDeduplication -XX:InitiatingHeapOccupancyPercent=45 + +DEPLOYMENT_ENVIRONMENT=CLOUD + +REDIS_GROUP_PORT=6379 +REDIS_SEARCH_PORT=6379 +DEFAULT_DATA_COUNTRY=US + +#Default Cache Settings +SCHEMA_CACHE_EXPIRATION=60 +INDEX_CACHE_EXPIRATION=60 +ELASTIC_CACHE_EXPIRATION=1440 +CURSOR_CACHE_EXPIRATION=60 +# Kinds Cache expiration 2*24*60 +KINDS_CACHE_EXPIRATION=2880 +# Attributes Cache expiration 2*24*60 +ATTRIBUTES_CACHE_EXPIRATION=2880 + +KINDS_REDIS_DATABASE=1 +CRON_INDEX_CLEANUP_THRESHOLD_DAYS=3 +CRON_EMPTY_INDEX_CLEANUP_THRESHOLD_DAYS=7 + +GAE_SERVICE=indexer +KEY_RING=csqp +KMS_KEY=searchService + +ELASTIC_DATASTORE_KIND=SearchSettings +ELASTIC_DATASTORE_ID=indexer-service + +mongo.db.url=localhost:27017 +mongo.db.user= +mongo.db.password= + +INDEXER_HOST=os-indexer-dot-nice-etching-277309.uc.r.appspot.com +STORAGE_HOSTNAME=os-storage-dot-nice-etching-277309.uc.r.appspot.com + +STORAGE_SCHEMA_HOST=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/schemas +STORAGE_QUERY_RECORD_HOST=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/query/records +STORAGE_QUERY_RECORD_FOR_CONVERSION_HOST=https://os-storage-dot-nice-etching-277309.uc.r.appspot.com/api/storage/v2/query/records:batch +STORAGE_RECORDS_BATCH_SIZE=20 + +INDEXER_QUEUE_HOST=https://os-indexer-queue-dot-nice-etching-277309.uc.r.appspot.com/_dps/task-handlers/enqueue + +AUTHORIZE_API=https://entitlements-dot-nice-etching-277309.uc.r.appspot.com/entitlements/v1 +LEGALTAG_API=https://os-legal-dot-nice-etching-277309.uc.r.appspot.com/api/legal/v1 +CRS_API=example.com + +## use below values for gcp: opendes +REDIS_GROUP_HOST=127.0.0.1 +REDIS_SEARCH_HOST=127.0.0.1 + +GOOGLE_AUDIENCES=689762842995-pv217jo3k8j803kk6gqf52qb5amos3a9.apps.googleusercontent.com + +mb.rabbitmq.uri=amqp://guest:guest@127.0.0.1:5672 + diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/middleware/IndexFilterTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/middleware/IndexFilterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7ebfab8dba438872ec892048d02cea3dd0d9e1a7 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/middleware/IndexFilterTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.middleware; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Collections; + +@RunWith(MockitoJUnitRunner.class) +public class IndexFilterTest { + + @InjectMocks + private IndexFilter indexFilter; + + @Mock + private DpsHeaders dpsHeaders; + + @Test + public void shouldSetCorrectResponseHeaders() throws IOException, ServletException { + HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); + HttpServletResponse httpServletResponse = Mockito.mock(HttpServletResponse.class); + FilterChain filterChain = Mockito.mock(FilterChain.class); + Mockito.when(httpServletRequest.getRequestURI()).thenReturn("https://test.com"); + Mockito.when(httpServletRequest.getMethod()).thenReturn("POST"); + Mockito.when(dpsHeaders.getCorrelationId()).thenReturn("correlation-id-value"); + + indexFilter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + Mockito.verify(httpServletResponse).addHeader("Access-Control-Allow-Origin", Collections.singletonList("*").toString()); + Mockito.verify(httpServletResponse).addHeader("Access-Control-Allow-Headers", Collections.singletonList("origin, content-type, accept, authorization, data-partition-id, correlation-id, appkey").toString()); + Mockito.verify(httpServletResponse).addHeader("Access-Control-Allow-Methods", Collections.singletonList("GET, POST, PUT, DELETE, OPTIONS, HEAD").toString()); + Mockito.verify(httpServletResponse).addHeader("Access-Control-Allow-Credentials", Collections.singletonList("true").toString()); + Mockito.verify(httpServletResponse).addHeader("X-Frame-Options", Collections.singletonList("DENY").toString()); + Mockito.verify(httpServletResponse).addHeader("X-XSS-Protection", Collections.singletonList("1; mode=block").toString()); + Mockito.verify(httpServletResponse).addHeader("X-Content-Type-Options", Collections.singletonList("nosniff").toString()); + Mockito.verify(httpServletResponse).addHeader("Cache-Control", Collections.singletonList("no-cache, no-store, must-revalidate").toString()); + Mockito.verify(httpServletResponse).addHeader("Content-Security-Policy", Collections.singletonList("default-src 'self'").toString()); + Mockito.verify(httpServletResponse).addHeader("Strict-Transport-Security", Collections.singletonList("max-age=31536000; includeSubDomains").toString()); + Mockito.verify(httpServletResponse).addHeader("Expires", Collections.singletonList("0").toString()); + Mockito.verify(httpServletResponse).addHeader("correlation-id", "correlation-id-value"); + Mockito.verify(filterChain).doFilter(httpServletRequest, httpServletResponse); + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/CronServiceImplTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/CronServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..80e25bbb400b88632fec13016fdbfc6bd622b4c4 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/CronServiceImplTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import com.google.common.collect.Lists; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.search.IndexInfo; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.search.IndicesService; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static org.mockito.Mockito.*; + + +@RunWith(SpringRunner.class) +@PrepareForTest({RestHighLevelClient.class}) +public class CronServiceImplTest { + + @Mock + private RestHighLevelClient restHighLevelClient; + @Mock + private IndicesService indicesService; + @Mock + private ElasticClientHandler elasticClientHandler; + @Mock + private IRequestInfo requestInfo; + @Mock + private JaxRsDpsLog log; + @InjectMocks + private CronServiceImpl sut; + + @InjectMocks + private DpsHeaders dpsHeaders; + + @Before + public void setup() { + + when(this.requestInfo.getHeaders()).thenReturn(dpsHeaders); + + ReflectionTestUtils.setField(this.sut, "CRON_INDEX_CLEANUP_THRESHOLD_DAYS", "3"); + ReflectionTestUtils.setField(this.sut, "CRON_EMPTY_INDEX_CLEANUP_THRESHOLD_DAYS", "7"); + } + + @Test + public void run_cleanup_when_cron_job_runs_with_correct_pattern() throws Exception { + final String indexPattern = "tenant1-index-*"; + + IndexInfo info = IndexInfo.builder().name("tenant1-index-1.0.0").documentCount("10").creationDate(Long.toString(Instant.now().minus(4, ChronoUnit.DAYS).toEpochMilli())).build(); + + when(this.requestInfo.getPartitionId()).thenReturn("tenant1"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, indexPattern)).thenReturn(Lists.newArrayList(info)); + + this.sut.cleanupIndices(indexPattern); + + verify(this.indicesService, times(1)).deleteIndex(restHighLevelClient, "tenant1-index-1.0.0"); + verify(this.indicesService, times(1)).getIndexInfo(restHighLevelClient, indexPattern); + } + + @Test(expected = IOException.class) + public void run_cleanup_when_cron_job_runs_with_wrong_pattern() throws Exception { + IOException exception = new IOException("blah"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, "tenant1-test-*")).thenThrow(exception); + + this.sut.cleanupIndices("tenant1-test-*"); + + verify(this.indicesService, times(0)).deleteIndex(any(), any()); + } + + @Test + public void run_cleanup_when_backend_does_not_have_empty_stale_indices() throws Exception { + IndexInfo info = IndexInfo.builder().name("tenant1-index-1.0.0").documentCount("10").creationDate(Long.toString(Instant.now().minus(8, ChronoUnit.DAYS).toEpochMilli())).build(); + + when(this.requestInfo.getPartitionId()).thenReturn("tenant1"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, null)).thenReturn(Lists.newArrayList(info)); + + this.sut.cleanupEmptyStaleIndices(); + + verify(this.indicesService, times(0)).deleteIndex(restHighLevelClient, null); + verify(this.indicesService, times(1)).getIndexInfo(restHighLevelClient, null); + } + + @Test + public void run_cleanup_when_backend_have_empty_stale_indices() throws Exception { + IndexInfo info = IndexInfo.builder().name("tenant1-index-1.0.0").documentCount("0").creationDate(Long.toString(Instant.now().minus(8, ChronoUnit.DAYS).toEpochMilli())).build(); + + when(this.requestInfo.getPartitionId()).thenReturn("tenant1"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, null)).thenReturn(Lists.newArrayList(info)); + + this.sut.cleanupEmptyStaleIndices(); + + verify(this.indicesService, times(1)).deleteIndex(restHighLevelClient, "tenant1-index-1.0.0"); + verify(this.indicesService, times(1)).getIndexInfo(restHighLevelClient, null); + } + + @Test(expected = IOException.class) + public void run_cleanup_when_backend_throws_exception() throws Exception { + IOException exception = new IOException("blah"); + when(this.elasticClientHandler.createRestClient()).thenReturn(this.restHighLevelClient); + when(this.indicesService.getIndexInfo(this.restHighLevelClient, null)).thenThrow(exception); + + this.sut.cleanupEmptyStaleIndices(); + + verify(this.indicesService, times(0)).deleteIndex(any(), any()); + } +} \ No newline at end of file diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ElasticSettingServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ElasticSettingServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e83cb64880b35e56be406b186c81185ddff353cf --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ElasticSettingServiceTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.model.search.ClusterSettings; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.provider.interfaces.IElasticCredentialsCache; +import org.opengroup.osdu.core.common.provider.interfaces.IElasticRepository; +import org.opengroup.osdu.core.common.multitenancy.ITenantInfoService; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +@RunWith(SpringRunner.class) +public class ElasticSettingServiceTest { + + @Mock + private ITenantInfoService tenantInfoService; + @Mock + private IElasticRepository elasticRepository; + @Mock + private IElasticCredentialsCache elasticCredentialCache; + @Mock + private TenantInfo tenantInfo; + @InjectMocks + private ElasticSettingServiceImpl sut; + @Mock + private ClusterSettings clusterSettings; + @Mock + private DpsHeaders headersInfo; + + @Mock + private JaxRsDpsLog log; + + + public String GAE_SERVICE = "indexer"; + + private final String host = "db5c51c1.us-central1.gcp.cloud.es.io"; + private final int port = 9243; + private final String credentials = "name:password"; + + String cacheKey = ""; + + + @Before + public void setup() { + when(tenantInfo.getName()).thenReturn("tenant1"); + when(this.headersInfo.getPartitionId()).thenReturn("tenant1"); + when(this.tenantInfoService.getTenantInfo()).thenReturn(tenantInfo); + sut.GAE_SERVICE = "indexer"; + clusterSettings = ClusterSettings.builder().host(host).port(port).userNameAndPassword(credentials).build(); + cacheKey = String.format("%s-%s", GAE_SERVICE, tenantInfo.getName()); + } + + @Test + public void should_getValid_clusterSettings_fromCache() { + + when(this.elasticCredentialCache.get(cacheKey)).thenReturn(clusterSettings); + + ClusterSettings response = this.sut.getElasticClusterInformation(); + assertNotNull(response); + assertEquals(response.getHost(), host); + assertEquals(response.getPort(), port); + assertEquals(response.getUserNameAndPassword(), credentials); + } + + @Test + public void should_getValid_clusterSettings_fromCosmosDB() { + + when(this.elasticCredentialCache.get(cacheKey)).thenReturn(clusterSettings); + + when(this.elasticRepository.getElasticClusterSettings(tenantInfo)).thenReturn(clusterSettings); + + ClusterSettings response = this.sut.getElasticClusterInformation(); + assertNotNull(response); + assertEquals(response.getHost(), host); + assertEquals(response.getPort(), port); + assertEquals(response.getUserNameAndPassword(), credentials); + } + + @Test(expected = AppException.class) + public void should_throwAppException_when_tenantClusterInfo_not_found() throws AppException { + + when(this.elasticRepository.getElasticClusterSettings(tenantInfo)).thenReturn(null); + + this.sut.getElasticClusterInformation(); + + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexCopyServiceImplTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexCopyServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9ef4a6adbe21c802885ee6b4caf93840b2728c1e --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexCopyServiceImplTest.java @@ -0,0 +1,193 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import org.apache.http.HttpEntity; +import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.model.search.ClusterSettings; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.indexer.logging.AuditLogger; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.model.indexer.IElasticSettingService; +import org.opengroup.osdu.core.common.search.IndicesService; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.opengroup.osdu.core.common.search.ElasticIndexNameResolver; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(SpringRunner.class) +@PrepareForTest({RestHighLevelClient.class, Response.class, RestClient.class, HttpEntity.class, EntityUtils.class}) +public class IndexCopyServiceImplTest { + private final String correlationId = UUID.randomUUID().toString(); + + @Mock + private HttpEntity httpEntity; + @Mock + private HttpEntity httpEntityRequest; + @Mock + private IRequestInfo requestInfo; + @Mock + private DpsHeaders headersInfo; + @Mock + private RestClient restClient; + @Mock + private RestHighLevelClient restHighLevelClient; + @Mock + private IndicesService indicesService; + @Mock + private IndexerMappingService mappingService; + @Mock + private ElasticClientHandler elasticClientHandler; + @Mock + private ElasticIndexNameResolver elasticIndexNameResolver; + @Mock + private Response response; + @Mock + private IElasticSettingService elasticSettingService; + @Mock + private AuditLogger auditLogger; + @Mock + private Map<String, String> httpHeaders; + @InjectMocks + private IndexCopyServiceImpl sut; + + private ClusterSettings commonCluster; + + private Map<String, Object> correctMap; + + @Before + public void setup() { + + commonCluster = ClusterSettings.builder().host("commonhost").port(8080).userNameAndPassword("username:pwd").build(); + + httpHeaders = new HashMap<>(); + httpHeaders.put(DpsHeaders.AUTHORIZATION, "testAuth"); + httpHeaders.put(DpsHeaders.CORRELATION_ID, correlationId); + when(requestInfo.getHeadersMapWithDwdAuthZ()).thenReturn(httpHeaders); + when(response.getEntity()).thenReturn(httpEntity); + + Type mapType = new TypeToken<Map<String, Object>>() {}.getType(); + String afterFormat = "{\"properties\":{\"id\":{\"type\":\"keyword\"}}}"; + correctMap = new Gson().fromJson(afterFormat, mapType); + + restHighLevelClient = mock(RestHighLevelClient.class); + + } + + @Test(expected = IOException.class) + public void should_throwIOException_when_indexMappingNotFound() throws Exception { + IOException exception = new IOException("Fail to get mapping for the given index from common cluster."); + + when(this.mappingService.getIndexMapping(ArgumentMatchers.any(), ArgumentMatchers.any())).thenThrow(exception); + + this.sut.copyIndex("common:metadata:entity:1.0.0"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_throwIllegalArgExceptionCopyIndexRequest_copyIndexTest() { + try { + this.sut.copyIndex(null); + } catch (IOException e) { + fail("Should not throw IOException but illegalArgumentException."); + } + } + + @Test + public void should_returnIndexMapping_getIndexMappingFromCommonClustertest() { + String mappingJson = "{\"common-metadata-entity-1.0.0\":{\"mappings\":{\"entity\":{\"properties\":{\"id\":{\"type\":\"keyword\"}}}}}}"; + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + try { + when(this.mappingService.getIndexMapping(ArgumentMatchers.any(), ArgumentMatchers.any())).thenReturn(mappingJson); + Map<String, Object> resultMap = this.sut.getIndexMappingsFromCommonCluster("test", "test"); + Assert.assertEquals(resultMap, correctMap); + } catch (Exception ignored) { + } + } + + @Test + public void should_returnClusterInfo_getCommonClusterInformationtest() { + try { + String[] correctCommonCluster = {"https://commonhost:8080", "username", "pwd"}; + + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + + when(elasticSettingService.getElasticClusterInformation()).thenReturn(commonCluster); + + String[] resultCommonCluster = this.sut.getCommonClusterInformation(); + Assert.assertEquals(correctCommonCluster[0], resultCommonCluster[0]); + Assert.assertEquals(correctCommonCluster[1], resultCommonCluster[1]); + Assert.assertEquals(correctCommonCluster[2], resultCommonCluster[2]); + } catch (IOException ignored) { + fail("Should not throw this exception " + ignored.getMessage()); + } + } + + @Test(expected = AppException.class) + public void should_throwException_failToCreateIndexInTenantCluster_createIndexInTenantClustertest() { + try { + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + when(indicesService.createIndex(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())).thenReturn(false); + this.sut.createIndexInTenantCluster("test", "test", "test", correctMap); + } catch (IOException ignored) { + fail("Should not throw this exception " + ignored.getMessage()); + } + } + + @Ignore + public void should_returnTaskIdResponse_reindexRequestSucceed_reindexInTenantClustertest() { + //TODO: fix the null Response from restHighLevelClient.getLowLevelClient().performRequest(). + try { + String[] correctCommonCluster = {"https://commonhost:8080", "username", "pwd"}; + Request request = new Request("POST", "/_reindex?wait_for_completion=false"); + request.setEntity(httpEntityRequest); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + when(indicesService.createIndex(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())).thenReturn(false); + when(restHighLevelClient.getLowLevelClient()).thenReturn(restClient); + when(restClient.performRequest(request)).thenReturn(response); + when(response.getEntity()).thenReturn(httpEntity); + Assert.assertEquals(httpEntity, this.sut.reindexInTenantCluster("test", "test", correctCommonCluster)); + } catch (IOException ignored) { + } + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerMappingServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerMappingServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d091cf83b22ce4fc1b6bd3c3d78042fc88ec85e6 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerMappingServiceTest.java @@ -0,0 +1,341 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import com.google.gson.Gson; +import org.apache.http.StatusLine; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse; +import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse.FieldMappingMetaData; +import org.elasticsearch.action.bulk.BulkItemResponse.Failure; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.*; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.reindex.BulkByScrollResponse; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.indexer.IndexSchema; +import org.opengroup.osdu.core.common.model.search.RecordMetaAttribute; +import org.opengroup.osdu.core.common.search.Config; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.opengroup.osdu.indexer.util.TypeMapper; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.util.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +@Ignore +@RunWith(SpringRunner.class) +@PrepareForTest({ RestHighLevelClient.class, IndicesClient.class, Config.class }) +public class IndexerMappingServiceTest { + + private final String kind = "tenant:test:test:1.0.0"; + private final String index = "tenant-test-test-1.0.0"; + private final String type = "test"; + private final String mappingValid = "{\"dynamic\":false,\"properties\":{\"data\":{\"properties\":{\"Msg\":{\"type\":\"text\",\"analyzer\":\"de_indexer_analyzer\",\"search_analyzer\":\"de_search_analyzer\"},\"Location\":{\"type\":\"geo_point\"}}},\"id\":{\"type\":\"keyword\"},\"acl\":{\"properties\":{\"viewers\":{\"type\":\"keyword\"},\"owners\":{\"type\":\"keyword\"}}}}}"; + + @Mock + private RestClient restClient; + @Mock + private Response response; + @Mock + private StatusLine statusLine; + + @InjectMocks + private IndexerMappingServiceImpl sut; + + @Mock + private ElasticClientHandler elasticClientHandler; + + @InjectMocks + private RestHighLevelClient restHighLevelClient; + + @InjectMocks + private IndexSchema indexSchema; + @InjectMocks + private IndicesClient indicesClient; + + @InjectMocks + private AcknowledgedResponse mappingResponse; + + @Before + public void setup() throws IOException { + initMocks(this); + mockStatic(Config.class); + when(Config.isPreDemo()).thenReturn(true); + Map<String, String> dataMapping = new HashMap<>(); + dataMapping.put("Location", "geo_point"); + dataMapping.put("Msg", "text"); + Map<String, Object> metaMapping = new HashMap<>(); + metaMapping.put(RecordMetaAttribute.ID.getValue(), "keyword"); + metaMapping.put(RecordMetaAttribute.ACL.getValue(), TypeMapper.getIndexerType(RecordMetaAttribute.ACL)); + this.indexSchema = IndexSchema.builder().kind(kind).type(type).dataSchema(dataMapping).metaSchema(metaMapping) + .build(); + + this.indicesClient = PowerMockito.mock(IndicesClient.class); + this.restHighLevelClient = PowerMockito.mock(RestHighLevelClient.class); + + when(this.restHighLevelClient.getLowLevelClient()).thenReturn(restClient); + when(this.restClient.performRequest(any())).thenReturn(response); + when(this.response.getStatusLine()).thenReturn(statusLine); + when(this.statusLine.getStatusCode()).thenReturn(200); + } + + @Test + public void should_returnValidMapping_givenFalseMerge_createMappingTest() { + try { + String mapping = this.sut.createMapping(restHighLevelClient, indexSchema, index, false); + assertEquals(mappingValid, mapping); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + @Test + public void should_returnValidMapping_givenTrueMerge_createMappingTest() { + try { + doReturn(this.indicesClient).when(this.restHighLevelClient).indices(); + doReturn(mappingResponse).when(this.indicesClient).putMapping(any(), any(RequestOptions.class)); + + String mapping = this.sut.createMapping(this.restHighLevelClient, this.indexSchema, this.index, true); + assertEquals(this.mappingValid, mapping); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + @Test + public void should_returnValidMapping_givenExistType_createMappingTest() { + try { + doReturn(this.indicesClient).when(this.restHighLevelClient).indices(); + doReturn(mappingResponse).when(this.indicesClient).putMapping(any(), any(RequestOptions.class)); + + IndexerMappingServiceImpl indexerMappingServiceLocal = PowerMockito.spy(new IndexerMappingServiceImpl()); + doReturn(false).when(indexerMappingServiceLocal).isTypeExist(any(), any(), any()); + String mapping = this.sut.createMapping(this.restHighLevelClient, this.indexSchema, this.index, true); + assertEquals(this.mappingValid, mapping); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + @Test + public void should_update_indices_field_with_keyword_when_valid_indices() throws Exception { + try { + Set<String> indices = new HashSet<String>(); + indices.add("indices 1"); + GetFieldMappingsResponse getFieldMappingsResponse = mock(GetFieldMappingsResponse.class); + doReturn(this.indicesClient).when(this.restHighLevelClient).indices(); + when(this.indicesClient.getFieldMapping(any(), any())).thenReturn(getFieldMappingsResponse); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.field("any field", new HashMap()); + builder.endObject(); + BytesReference bytesReference = BytesReference.bytes(builder); + FieldMappingMetaData mappingMetaData = new FieldMappingMetaData(index, bytesReference); + Map<String, FieldMappingMetaData> mapBuilder = new HashMap<>(); + mapBuilder.put("data.any field", mappingMetaData); + Map<String, Map<String, FieldMappingMetaData>> mappingBuilder = new HashMap<>(); + mappingBuilder.put("any index 1", mapBuilder); + mappingBuilder.put("any index 2", mapBuilder); + Map<String, Map<String, Map<String, FieldMappingMetaData>>> mapping = new HashMap<>(); + mapping.put("indices 1", mappingBuilder); + when(getFieldMappingsResponse.mappings()).thenReturn(mapping); + doReturn(mappingResponse).when(this.indicesClient).putMapping(any(), any(RequestOptions.class)); + BulkByScrollResponse response = mock(BulkByScrollResponse.class); + doReturn(response).when(this.restHighLevelClient).updateByQuery(any(), any(RequestOptions.class)); + when(response.getBulkFailures()).thenReturn(new ArrayList<Failure>()); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + + this.sut.updateIndexMappingForIndicesOfSameType( indices,"any field"); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + @Test(expected = AppException.class) + public void should_throw_exception_if_someIndex_is_invalid_andWeIndexfield_with_keyword() throws Exception { + try { + Set<String> indices = new HashSet<String>(); + indices.add("invalid 1"); + GetFieldMappingsResponse getFieldMappingsResponse = mock(GetFieldMappingsResponse.class); + doReturn(this.indicesClient).when(this.restHighLevelClient).indices(); + when(this.indicesClient.getFieldMapping(any(), any())).thenReturn(getFieldMappingsResponse); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.field("any field", new HashMap()); + builder.endObject(); + BytesReference bytesReference = BytesReference.bytes(builder); + FieldMappingMetaData mappingMetaData = new FieldMappingMetaData(index, bytesReference); + Map<String, FieldMappingMetaData> mapBuilder = new HashMap<>(); + mapBuilder.put("data.any field", mappingMetaData); + Map<String, Map<String, FieldMappingMetaData>> mappingBuilder = new HashMap<>(); + mappingBuilder.put("any index 1", mapBuilder); + mappingBuilder.put("any index 2", mapBuilder); + Map<String, Map<String, Map<String, FieldMappingMetaData>>> mapping = new HashMap<>(); + mapping.put("indices 1", mappingBuilder); + when(getFieldMappingsResponse.mappings()).thenReturn(mapping); + doReturn(mappingResponse).when(this.indicesClient).putMapping(any(), any(RequestOptions.class)); + BulkByScrollResponse response = mock(BulkByScrollResponse.class); + doReturn(response).when(this.restHighLevelClient).updateByQuery(any(), any(RequestOptions.class)); + when(response.getBulkFailures()).thenReturn(new ArrayList<Failure>()); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + + this.sut.updateIndexMappingForIndicesOfSameType(indices,"any field"); + } catch (Exception e) { + throw e; + } + } + + @Test(expected = AppException.class) + public void should_throw_exception_if_type_of_index_is_invalid_andWeIndexfield_with_keyword() throws Exception { + try { + Set<String> indices = new HashSet<String>(); + indices.add("indices 1"); + GetFieldMappingsResponse getFieldMappingsResponse = mock(GetFieldMappingsResponse.class); + doReturn(this.indicesClient).when(this.restHighLevelClient).indices(); + when(this.indicesClient.getFieldMapping(any(), any())).thenReturn(getFieldMappingsResponse); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.field("any field", new HashMap()); + builder.endObject(); + BytesReference bytesReference = BytesReference.bytes(builder); + FieldMappingMetaData mappingMetaData = new FieldMappingMetaData(index, bytesReference); + Map<String, FieldMappingMetaData> mapBuilder = new HashMap<>(); + mapBuilder.put("data.any field", mappingMetaData); + Map<String, Map<String, FieldMappingMetaData>> mappingBuilder = new HashMap<>(); + mappingBuilder.put("any index 1", mapBuilder); + mappingBuilder.put("any index 2", mapBuilder); + Map<String, Map<String, Map<String, FieldMappingMetaData>>> mapping = new HashMap<>(); + mapping.put("indices 1", mappingBuilder); + when(getFieldMappingsResponse.mappings()).thenReturn(mapping); + doReturn(mappingResponse).when(this.indicesClient).putMapping(any(), any(RequestOptions.class)); + BulkByScrollResponse response = mock(BulkByScrollResponse.class); + doReturn(response).when(this.restHighLevelClient).updateByQuery(any(), any(RequestOptions.class)); + when(response.getBulkFailures()).thenReturn(new ArrayList<Failure>()); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + this.sut.updateIndexMappingForIndicesOfSameType(indices,"any field invalid"); + } catch (Exception e) { + throw e; + } + } + + @Test(expected = AppException.class) + public void should_throw_exception_if_elastic_search_failedToFetch_andWeIndexfield_with_keyword() throws Exception { + try { + + Set<String> indices = new HashSet<String>(); + indices.add("indices 1"); + indices.add("indices Invalid"); + GetFieldMappingsResponse getFieldMappingsResponse = mock(GetFieldMappingsResponse.class); + doReturn(this.indicesClient).when(this.restHighLevelClient).indices(); + when(this.indicesClient.getFieldMapping(any(), any())).thenThrow(new ElasticsearchException("")); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.field("any field", new HashMap()); + builder.endObject(); + BytesReference bytesReference = BytesReference.bytes(builder); + FieldMappingMetaData mappingMetaData = new FieldMappingMetaData(index, bytesReference); + Map<String, FieldMappingMetaData> mapBuilder = new HashMap<>(); + mapBuilder.put("data.any field", mappingMetaData); + Map<String, Map<String, FieldMappingMetaData>> mappingBuilder = new HashMap<>(); + mappingBuilder.put("any index 1", mapBuilder); + mappingBuilder.put("any index 2", mapBuilder); + Map<String, Map<String, Map<String, FieldMappingMetaData>>> mapping = new HashMap<>(); + mapping.put("indices 1", mappingBuilder); + when(getFieldMappingsResponse.mappings()).thenReturn(mapping); + doReturn(mappingResponse).when(this.indicesClient).putMapping(any(), any(RequestOptions.class)); + BulkByScrollResponse response = mock(BulkByScrollResponse.class); + doReturn(response).when(this.restHighLevelClient).updateByQuery(any(), any(RequestOptions.class)); + when(response.getBulkFailures()).thenReturn(new ArrayList<Failure>()); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + this.sut.updateIndexMappingForIndicesOfSameType(indices,"any field"); + } catch (AppException e) { + throw e; + } + } + + @Test(expected = AppException.class) + public void should_throw_exception_when_elastic_failedToIndex_indices_field_with_keyword() { + try { + Set<String> indices = new HashSet<String>(); + indices.add("indices 1"); + indices.add("indices Invalid"); + GetFieldMappingsResponse getFieldMappingsResponse = mock(GetFieldMappingsResponse.class); + doReturn(this.indicesClient).when(this.restHighLevelClient).indices(); + when(this.indicesClient.getFieldMapping(any(), any())).thenReturn(getFieldMappingsResponse); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + builder.field("any field", new HashMap()); + builder.endObject(); + BytesReference bytesReference = BytesReference.bytes(builder); + FieldMappingMetaData mappingMetaData = new FieldMappingMetaData(index, bytesReference); + Map<String, FieldMappingMetaData> mapBuilder = new HashMap<>(); + mapBuilder.put("data.any field", mappingMetaData); + Map<String, Map<String, FieldMappingMetaData>> mappingBuilder = new HashMap<>(); + mappingBuilder.put("any index 1", mapBuilder); + mappingBuilder.put("any index 2", mapBuilder); + Map<String, Map<String, Map<String, FieldMappingMetaData>>> mapping = new HashMap<>(); + mapping.put("indices 1", mappingBuilder); + when(getFieldMappingsResponse.mappings()).thenReturn(mapping); + doReturn(mappingResponse).when(this.indicesClient).putMapping(any(), any(RequestOptions.class)); + BulkByScrollResponse response = mock(BulkByScrollResponse.class); + doReturn(response).when(this.restHighLevelClient).updateByQuery(any(), any(RequestOptions.class)); + when(response.getBulkFailures()).thenReturn(new ArrayList<Failure>()); + when(this.indicesClient.putMapping(any(), any(RequestOptions.class))).thenThrow(new ElasticsearchException("")); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + this.sut.updateIndexMappingForIndicesOfSameType(indices,"any field"); + } catch (AppException e) { + throw e; + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + + @Test + public void should_returnDocumentMapping_givenValidIndexSchema() { + + try { + Map<String, Object> documentMapping = this.sut.getIndexMappingFromRecordSchema(this.indexSchema); + String documentMappingJson = new Gson().toJson(documentMapping); + assertEquals(this.mappingValid, documentMappingJson); + + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerSchemaServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerSchemaServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e1dd9c641f4c25c50949edec3f6021921040aa2f --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerSchemaServiceTest.java @@ -0,0 +1,403 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import org.apache.http.HttpStatus; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.indexer.OperationType; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.indexer.provider.interfaces.ISchemaCache; +import org.opengroup.osdu.core.common.model.indexer.IndexSchema; +import org.opengroup.osdu.core.common.model.http.RequestStatus; +import org.opengroup.osdu.core.common.search.IndicesService; +import org.opengroup.osdu.indexer.util.ElasticClientHandler; +import org.opengroup.osdu.core.common.search.ElasticIndexNameResolver; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(SpringRunner.class) +@PrepareForTest({RestHighLevelClient.class}) +public class IndexerSchemaServiceTest { + + private final String kind = "tenant:test:test:1.0.0"; + private final String emptySchema = null; + private final String someSchema = "{\"kind\":\"tenant:test:test:1.0.0\", \"schema\":[{\"path\":\"test-path\", \"kind\":\"tenant:test:test:1.0.0\"}]}"; + + @Mock + private JaxRsDpsLog log; + @Mock + private StorageService storageService; + @Mock + private ElasticClientHandler elasticClientHandler; + @Mock + private ElasticIndexNameResolver elasticIndexNameResolver; + @Mock + private IndexerMappingService mappingService; + @Mock + private IndicesService indicesService; + @Mock + private ISchemaCache schemaCache; + @InjectMocks + private IndexSchemaServiceImpl sut; + + @Before + public void setup() { + initMocks(this); + RestHighLevelClient restHighLevelClient = mock(RestHighLevelClient.class); + when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); + } + + @Test + public void should_returnNull_givenEmptySchema_getIndexerInputSchemaSchemaTest() throws Exception { + when(storageService.getStorageSchema(any())).thenReturn(emptySchema); + + IndexSchema indexSchema = this.sut.getIndexerInputSchema(kind, false); + + Assert.assertNotNull(indexSchema); + } + + @Test + public void should_returnValidResponse_givenValidSchema_getIndexerInputSchemaTest() throws Exception { + when(storageService.getStorageSchema(any())).thenReturn(someSchema); + + IndexSchema indexSchema = this.sut.getIndexerInputSchema(kind, false); + + Assert.assertEquals(kind, indexSchema.getKind()); + } + + @Test + public void should_returnValidResponse_givenValidSchemaWithCacheHit_getIndexerInputSchemaTest() throws Exception { + when(storageService.getStorageSchema(any())).thenReturn(someSchema); + when(this.schemaCache.get(kind + "_flattened")).thenReturn(someSchema); + + IndexSchema indexSchema = this.sut.getIndexerInputSchema(kind, false); + + Assert.assertEquals(kind, indexSchema.getKind()); + } + + @Test + public void should_throw500_givenInvalidSchemaCacheHit_getIndexerInputSchemaTest() { + try { + String invalidSchema = "{}}"; + when(storageService.getStorageSchema(any())).thenReturn(invalidSchema); + + this.sut.getIndexerInputSchema(kind, false); + fail("Should throw exception"); + } catch (AppException e) { + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getError().getCode()); + Assert.assertEquals("An error has occurred while normalizing the schema.", e.getError().getMessage()); + } catch (Exception e) { + fail("Should not throw exception" + e.getMessage()); + } + } + + @Test + public void should_return_basic_schema_when_storage_returns_no_schema() { + IndexSchema returnedSchema = this.sut.getIndexerInputSchema(kind, false); + + assertNotNull(returnedSchema.getDataSchema()); + assertNotNull(returnedSchema); + assertEquals(kind, returnedSchema.getKind()); + } + + @Test + public void should_create_schema_when_storage_returns_valid_schema() throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"startDate\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"endDate\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"type \"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"itemguid\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(false); + when(this.storageService.getStorageSchema(kind)).thenReturn(storageSchema); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.mappingService, times(1)).getIndexMappingFromRecordSchema(any()); + verify(this.indicesService, times(1)).createIndex(any(), any(), any(), any(), any()); + verifyNoMoreInteractions(this.mappingService); + } + + @Test + public void should_merge_mapping_when_storage_returns_valid_schema() throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"startDate\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.storageService.getStorageSchema(kind)).thenReturn(storageSchema); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.indicesService, times(0)).createIndex(any(), any(), any(), any(), any()); + verify(this.mappingService, times(1)).createMapping(any(), any(), any(), anyBoolean()); + verifyNoMoreInteractions(this.mappingService); + } + + @Test + public void should_throw_mapping_conflict_when_elastic_backend_cannot_process_schema_changes() throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String reason = String.format("Could not create type mapping %s/completion.", kind.replace(":", "-")); + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.storageService.getStorageSchema(kind)).thenReturn(storageSchema); + when(this.mappingService.createMapping(any(), any(), any(), anyBoolean())).thenThrow(new AppException(HttpStatus.SC_BAD_REQUEST, reason, "")); + + try { + this.sut.processSchemaMessages(schemaMessages); + } catch (AppException e) { + assertEquals(e.getError().getCode(), RequestStatus.SCHEMA_CONFLICT); + assertEquals(e.getError().getMessage(), "error creating or merging index mapping"); + assertEquals(e.getError().getReason(), reason); + } catch (Exception e) { + fail("Should not throw this exception " + e.getMessage()); + } + } + + @Test + public void should_throw_genericAppException_when_elastic_backend_cannot_process_schema_changes() throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String reason = String.format("Could not create type mapping %s/completion.", kind.replace(":", "-")); + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.storageService.getStorageSchema(kind)).thenReturn(storageSchema); + when(this.mappingService.createMapping(any(), any(), any(), anyBoolean())).thenThrow(new AppException(HttpStatus.SC_FORBIDDEN, reason, "blah")); + + try { + this.sut.processSchemaMessages(schemaMessages); + } catch (AppException e) { + assertEquals(e.getError().getCode(), HttpStatus.SC_FORBIDDEN); + assertEquals(e.getError().getMessage(), "blah"); + assertEquals(e.getError().getReason(), reason); + } catch (Exception e) { + fail("Should not throw this exception " + e.getMessage()); + } + } + + @Test + public void should_log_and_do_nothing_when_storage_returns_invalid_schema() throws IOException, URISyntaxException { + String kind = "tenant1:avocet:completion:1.0.0"; + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"" + + "}"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.create_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.storageService.getStorageSchema(kind)).thenReturn(storageSchema); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.log).warning(eq("schema not found for kind: tenant1:avocet:completion:1.0.0")); + } + + @Test + public void should_invalidateCache_when_purge_schema_and_schema_found_in_cache() throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.purge_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.schemaCache.get(kind)).thenReturn("schema"); + when(this.schemaCache.get(kind + "_flattened")).thenReturn("flattened schema"); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.schemaCache, times(2)).get(anyString()); + verify(this.schemaCache, times(2)).delete(anyString()); + } + + @Test + public void should_log_warning_when_purge_schema_and_schema_not_found_in_cache() throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + Map<String, OperationType> schemaMessages = new HashMap<>(); + schemaMessages.put(kind, OperationType.purge_schema); + + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(false); + + this.sut.processSchemaMessages(schemaMessages); + + verify(this.log).warning(eq(String.format("Kind: %s not found", kind))); + } + + @Test + public void should_sync_schema_with_storage() throws Exception { + String kind = "tenant1:avocet:completion:1.0.0"; + String storageSchema = "{" + + " \"kind\": \"tenant1:avocet:completion:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"status\"," + + " \"kind\": \"string\"" + + " }" + + " ]" + + "}"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.indicesService.deleteIndex(any(), any())).thenReturn(true); + when(this.storageService.getStorageSchema(kind)).thenReturn(storageSchema); + + this.sut.syncIndexMappingWithStorageSchema(kind); + + verify(this.mappingService, times(1)).getIndexMappingFromRecordSchema(any()); + verify(this.indicesService, times(1)).isIndexExist(any(), any()); + verify(this.indicesService, times(1)).deleteIndex(any(), any()); + verify(this.indicesService, times(1)).createIndex(any(), any(), any(), any(), any()); + verifyNoMoreInteractions(this.mappingService); + } + + @Test + public void should_throw_exception_while_snapshot_running_sync_schema_with_storage() throws Exception { + String kind = "tenant1:avocet:completion:1.0.0"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.schemaCache.get(kind)).thenReturn(null); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + when(this.indicesService.deleteIndex(any(), any())).thenThrow(new AppException(HttpStatus.SC_CONFLICT, "Index deletion error", "blah")); + + try { + this.sut.syncIndexMappingWithStorageSchema(kind); + } catch (AppException e) { + assertEquals(e.getError().getCode(), HttpStatus.SC_CONFLICT); + assertEquals(e.getError().getMessage(), "blah"); + assertEquals(e.getError().getReason(), "Index deletion error"); + } catch (Exception e) { + fail("Should not throw this exception " + e.getMessage()); + } + + verify(this.indicesService, times(1)).isIndexExist(any(), any()); + verify(this.indicesService, times(1)).deleteIndex(any(), any()); + verify(this.mappingService, never()).getIndexMappingFromRecordSchema(any()); + verify(this.indicesService, never()).createIndex(any(), any(), any(), any(), any()); + } + + @Test + public void should_return_true_while_if_forceClean_requested() throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + + assertTrue(this.sut.isStorageSchemaSyncRequired(kind, true)); + } + + @Test + public void should_return_true_while_if_forceClean_notRequested_and_indexNotFound() throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(false); + + assertTrue(this.sut.isStorageSchemaSyncRequired(kind, false)); + } + + @Test + public void should_return_false_while_if_forceClean_notRequested_and_indexExist() throws IOException { + String kind = "tenant1:avocet:completion:1.0.0"; + when(this.elasticIndexNameResolver.getIndexNameFromKind(kind)).thenReturn(kind.replace(":", "-")); + when(this.indicesService.isIndexExist(any(), any())).thenReturn(true); + + assertFalse(this.sut.isStorageSchemaSyncRequired(kind, false)); + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f2a8886f1372faf894698a8272e35ee8c608efda --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/IndexerServiceTest.java @@ -0,0 +1,354 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service;//// Copyright 2017-2019, Schlumberger +//// +//// Licensed under the Apache License, Version 2.0 (the "License"); +//// you may not use this file except in compliance with the License. +//// You may obtain a copy of the License at +//// +//// http://www.apache.org/licenses/LICENSE-2.0 +//// +//// Unless required by applicable law or agreed to in writing, software +//// distributed under the License is distributed on an "AS IS" BASIS, +//// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//// See the License for the specific language governing permissions and +//// limitations under the License. +// +//package org.opendes.indexer.service; +// +//import com.google.gson.Gson; +//import com.google.gson.reflect.TypeToken; +//import org.elasticsearch.action.bulk.BulkItemResponse; +//import org.elasticsearch.action.bulk.BulkResponse; +//import org.elasticsearch.client.RequestOptions; +//import org.elasticsearch.client.RestHighLevelClient; +//import org.junit.Before; +//import org.junit.Ignore; +//import org.junit.Test; +//import org.junit.runner.RunWith; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.Spy; +//import org.opendes.client.api.DpsHeaders; +//import org.opendes.core.logging.JaxRsDpsLog; +//import org.opendes.core.model.DeploymentEnvironment; +//import org.opendes.core.model.RecordChangedMessages; +//import org.opendes.core.service.IndicesService; +//import org.opendes.core.util.Config; +//import org.opendes.core.util.ElasticClientHandler; +//import org.opendes.core.util.ElasticIndexNameResolver; +//import org.opendes.core.util.HeadersUtil; +//import org.opendes.indexer.logging.AuditLogger; +//import org.opendes.indexer.model.*; +//import org.opendes.indexer.publish.IPublisher; +//import org.opendes.indexer.util.IRequestInfo; +//import org.opendes.indexer.util.IndexerQueueTaskBuilder; +//import org.opendes.indexer.util.JobStatus; +//import org.opendes.indexer.util.RecordInfo; +//import org.powermock.core.classloader.annotations.PrepareForTest; +//import javax.inject.Inject; +//import org.springframework.context.annotation.Lazy; +//import org.springframework.test.context.junit4.SpringRunner; +// +//import java.io.IOException; +//import java.lang.reflect.Type; +//import java.util.*; +// +//import static java.util.Collections.singletonList; +//import static org.junit.Assert.*; +//import static org.mockito.Matchers.any; +//import static org.mockito.Mockito.verify; +//import static org.mockito.Mockito.when; +//import static org.powermock.api.mockito.PowerMockito.mock; +//import static org.powermock.api.mockito.PowerMockito.mockStatic; +// +//@Ignore +//@RunWith(SpringRunner.class) +//@PrepareForTest({RestHighLevelClient.class, BulkResponse.class, StorageAcl.class, HeadersUtil.class, Config.class}) +//public class IndexerServiceTest { +// +// private final String pubsubMsg = "[{\"id\":\"tenant1:doc:test1\",\"kind\":\"tenant1:testindexer1:well:1.0.0\",\"op\":\"update\"}," + +// "{\"id\":\"tenant1:doc:test2\",\"kind\":\"tenant1:testindexer2:well:1.0.0\",\"op\":\"create\"}]"; +// private final String kind1 = "tenant1:testindexer1:well:1.0.0"; +// private final String kind2 = "tenant1:testindexer2:well:1.0.0"; +// private final String recordId1 = "tenant1:doc:test1"; +// private final String recordId2 = "tenant1:doc:test2"; +// private final String failureMassage = "test failure"; +// +// @Mock +// private IndexSchemaService indexSchemaService; +// @Mock +// private IndicesService indicesService; +// @Mock +// private IndexerMappingService indexerMappingService; +// @Mock +// private StorageService storageService; +// @Mock +// private IPublisher publisherImpl; +// @Mock +// private RestHighLevelClient restHighLevelClient; +// @Mock +// private ElasticClientHandler elasticClientHandler; +// @Mock +// private BulkResponse bulkResponse; +// @Mock +// private IRequestInfo requestInfo; +// @Mock +// private ElasticIndexNameResolver elasticIndexNameResolver; +// @Mock +// private AttributeParsingServiceImpl attributeParsingServiceImpl; +// @Mock +// private IndexerQueueTaskBuilder indexerQueueTaskBuilder; +// @Mock +// private JaxRsDpsLog log; +// @Mock +// private AuditLogger auditLogger; +// @InjectMocks +// private IndexerServiceImpl sut; +// @InjectMocks @Spy +// private JobStatus jobStatus = new JobStatus(); +// +// @Inject +// @Lazy +// private DpsHeaders dpsHeaders; +// private RecordChangedMessages recordChangedMessages; +// private List<RecordInfo> recordInfos; +// +// @Before +// public void setup() throws IOException { +// +// mockStatic(StorageAcl.class); +// mockStatic(Config.class); +// +// when(Config.getDeploymentEnvironment()).thenReturn(DeploymentEnvironment.LOCAL); +// when(Config.getElasticClusterName()).thenReturn("CLUSTER"); +// when(Config.getElasticServerAddress()).thenReturn("testsite"); +// +// dpsHeaders = new DpsHeaders(); +// dpsHeaders.put(AppEngineHeaders.TASK_QUEUE_RETRY_COUNT, "1"); +// dpsHeaders.put(DpsHeaders.AUTHORIZATION, "testAuth"); +// when(requestInfo.getHeaders()).thenReturn(dpsHeaders); +// when(requestInfo.getHeadersMapWithDwdAuthZ()).thenReturn(dpsHeaders.getHeaders()); +// +// Type listType = new TypeToken<List<RecordInfo>>() {}.getType(); +// recordInfos = (new Gson()).fromJson(pubsubMsg, listType); +// +// when(elasticClientHandler.createRestClient()).thenReturn(restHighLevelClient); +// when(restHighLevelClient.bulk(any(), any(RequestOptions.class))).thenReturn(bulkResponse); +// +// BulkItemResponse[] responses = new BulkItemResponse[]{prepareResponseFail(), prepareResponseSuccess()}; +// when(bulkResponse.getItems()).thenReturn(responses); +// Map<String, String> attr = new HashMap<>(); +// attr.put(DpsHeaders.ACCOUNT_ID, "slb"); +// recordChangedMessages = RecordChangedMessages.builder().attributes(attr).messageId("xxxx").publishTime("2000-01-02T10:10:44+0000").data("{}").build(); +// when(StorageAcl.flattenAcl(any())).thenReturn(null); +// } +// +// @Test +// public void should_returnNull_givenEmptyJobSubInfo_processRecordChangedMessageTest() throws Exception { +// JobStatus jobStatus = this.sut.processRecordChangedMessages(recordChangedMessages, new ArrayList<>()); +// +// assertNull(jobStatus); +// } +// +// @Test +// public void should_returnValidJobStatus_givenNullSchema_processRecordChangedMessageTest() { +// try { +// indexSchemaServiceMock(kind1, null); +// indexSchemaServiceMock(kind2, null); +// List<ConversionStatus> conversionStatus = new LinkedList<>(); +// List<Records.Entity> validRecords = new ArrayList<>(); +// Map<String, Object> storageData = new HashMap<>(); +// storageData.put("schema1", "test-value"); +// storageData.put("schema2", "test-value"); +// storageData.put("schema3", "test-value"); +// storageData.put("schema4", "test-value"); +// storageData.put("schema5", "test-value"); +// storageData.put("schema6", "test-value"); +// validRecords.add(Records.Entity.builder().id(recordId2).kind(kind2).data(storageData).build()); +// Records storageRecords = Records.builder().records(validRecords).conversionStatuses(conversionStatus).build(); +// +// when(storageService.getStorageRecords(any())).thenReturn(storageRecords); +// when(indicesService.createIndex(any(), any(), any(), any(), any())).thenReturn(true); +// +// JobStatus jobStatus = this.sut.processRecordChangedMessages(recordChangedMessages, recordInfos); +// +// assertEquals(2, jobStatus.getStatusesList().size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.FAIL).size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.WARN).size()); +// } catch (Exception e) { +// fail("Should not throw this exception" + e.getMessage()); +// } +// } +// +// @Test +// public void should_returnValidJobStatus_givenFailedUnitsConversion_processRecordChangedMessageTest() { +// try { +// indexSchemaServiceMock(kind1, null); +// indexSchemaServiceMock(kind2, null); +// List<ConversionStatus> conversionStatuses = new LinkedList<>(); +// List<String> status=new ArrayList<>(); +// status.add("crs bla bla"); +// ConversionStatus conversionStatus=ConversionStatus.builder().status("ERROR").errors(status).id(recordId2).build(); +// conversionStatuses.add(conversionStatus); +// List<Records.Entity> validRecords = new ArrayList<>(); +// Map<String, Object> storageData = new HashMap<>(); +// storageData.put("schema1", "test-value"); +// storageData.put("schema2", "test-value"); +// storageData.put("schema3", "test-value"); +// storageData.put("schema4", "test-value"); +// storageData.put("schema5", "test-value"); +// storageData.put("schema6", "test-value"); +// validRecords.add(Records.Entity.builder().id(recordId2).kind(kind2).data(storageData).build()); +// Records storageRecords = Records.builder().records(validRecords).conversionStatuses(conversionStatuses).build(); +// +// when(storageService.getStorageRecords(any())).thenReturn(storageRecords); +// when(indicesService.createIndex(any(), any(), any(), any(), any())).thenReturn(true); +// +// JobStatus jobStatus = this.sut.processRecordChangedMessages(recordChangedMessages, recordInfos); +// +// assertEquals(2, jobStatus.getStatusesList().size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.FAIL).size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.WARN).size()); +// assertTrue(jobStatus.getJobStatusByRecordId(jobStatus.getIdsByIndexingStatus(IndexingStatus.WARN).get(0)).getIndexProgress().getTrace().contains("crs bla bla")); +// } catch (Exception e) { +// fail("Should not throw this exception" + e.getMessage()); +// } +// } +// +// @Test +// public void should_returnValidJobStatus_givenNullSchemaForARecord_processRecordChangedMessageTest() { +// try { +// List<Records.Entity> validRecords = new ArrayList<>(); +// List<ConversionStatus> conversionStatus = new LinkedList<>(); +// Map<String, Object> storageData = new HashMap<>(); +// storageData.put("schema1", "test-value"); +// storageData.put("schema2", "test-value"); +// storageData.put("schema3", "test-value"); +// storageData.put("schema4", "test-value"); +// storageData.put("schema5", "test-value"); +// storageData.put("schema6", "test-value"); +// validRecords.add(Records.Entity.builder().id(recordId2).kind(kind2).data(storageData).build()); +// Records storageRecords = Records.builder().records(validRecords).conversionStatuses(conversionStatus).build(); +// when(storageService.getStorageRecords(any())).thenReturn(storageRecords); +// +// Map<String, String> schema = createSchema(); +// indexSchemaServiceMock(kind1, schema); +// indexSchemaServiceMock(kind2, null); +// when(elasticIndexNameResolver.getIndexNameFromKind(kind2)).thenReturn("tenant1-testindexer2-well-1.0.0"); +// when(indicesService.createIndex(any(), any(), any(), any(), any())).thenReturn(true); +// JobStatus jobStatus = sut.processRecordChangedMessages(recordChangedMessages, recordInfos); +// +// assertEquals(2, jobStatus.getStatusesList().size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.FAIL).size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.WARN).size()); +// assertEquals("Indexed Successfully", jobStatus.getStatusesList().get(1).getIndexProgress().getTrace().pop()); +// assertEquals("schema not found", jobStatus.getStatusesList().get(1).getIndexProgress().getTrace().pop()); +// } catch (Exception e) { +// fail("Should not throw this exception" + e.getMessage()); +// } +// } +// +// @Test +// public void should_returnValidJobStatus_givenValidCreateAndUpdateRecords_processRecordChangedMessagesTest() { +// try { +// Map<String, Object> storageData = new HashMap<>(); +// storageData.put("schema1", "test-value"); +// List<ConversionStatus> conversionStatus = new LinkedList<>(); +// List<Records.Entity> validRecords = new ArrayList<>(); +// validRecords.add(Records.Entity.builder().id(recordId2).kind(kind2).data(storageData).build()); +// Records storageRecords = Records.builder().records(validRecords).conversionStatuses(conversionStatus).build(); +// +// when(storageService.getStorageRecords(any())).thenReturn(storageRecords); +// when(indicesService.createIndex(any(), any(), any(), any(), any())).thenReturn(true); +// Map<String, String> schema = createSchema(); +// indexSchemaServiceMock(kind2, schema); +// indexSchemaServiceMock(kind1, null); +// JobStatus jobStatus = sut.processRecordChangedMessages(recordChangedMessages, recordInfos); +// +// assertEquals(2, jobStatus.getStatusesList().size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.FAIL).size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.SUCCESS).size()); +// } catch (Exception e) { +// fail("Should not throw this exception" + e.getMessage()); +// } +// } +// +// @Test +// public void should_properlyUpdateAuditLogs_givenValidCreateAndUpdateRecords() { +// try { +// Map<String, Object> storageData = new HashMap<>(); +// List<ConversionStatus> conversionStatus = new LinkedList<>(); +// +// storageData.put("schema1", "test-value"); +// List<Records.Entity> validRecords = new ArrayList<>(); +// validRecords.add(Records.Entity.builder().id(recordId2).kind(kind2).data(storageData).build()); +// Records storageRecords = Records.builder().records(validRecords).conversionStatuses(conversionStatus).build(); +// +// when(this.storageService.getStorageRecords(any())).thenReturn(storageRecords); +// when(this.indicesService.createIndex(any(), any(), any(), any(), any())).thenReturn(true); +// Map<String, String> schema = createSchema(); +// indexSchemaServiceMock(kind2, schema); +// indexSchemaServiceMock(kind1, null); +// JobStatus jobStatus = this.sut.processRecordChangedMessages(recordChangedMessages, recordInfos); +// +// assertEquals(2, jobStatus.getStatusesList().size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.FAIL).size()); +// assertEquals(1, jobStatus.getIdsByIndexingStatus(IndexingStatus.SUCCESS).size()); +// +// verify(this.auditLogger).indexCreateRecordSuccess(singletonList("RecordStatus(id=tenant1:doc:test2, kind=tenant1:testindexer2:well:1.0.0, operationType=create, status=SUCCESS)")); +// verify(this.auditLogger).indexUpdateRecordFail(singletonList("RecordStatus(id=tenant1:doc:test1, kind=tenant1:testindexer1:well:1.0.0, operationType=update, status=FAIL)")); +// } catch (Exception e) { +// fail("Should not throw this exception" + e.getMessage()); +// } +// } +// +// private BulkItemResponse prepareResponseFail() { +// BulkItemResponse responseFail = mock(BulkItemResponse.class); +// when(responseFail.isFailed()).thenReturn(true); +// when(responseFail.getFailureMessage()).thenReturn(failureMassage); +// when(responseFail.getId()).thenReturn(recordId1); +// when(responseFail.getFailure()).thenReturn(new BulkItemResponse.Failure("failure index", "failure type", "failure id", new Exception("test failure"))); +// return responseFail; +// } +// +// private BulkItemResponse prepareResponseSuccess() { +// BulkItemResponse responseSuccess = mock(BulkItemResponse.class); +// when(responseSuccess.getId()).thenReturn(recordId2); +// return responseSuccess; +// } +// +// private void indexSchemaServiceMock(String kind, Map<String, String> schema) { +// if (schema == null) { +// IndexSchema indexSchema = IndexSchema.builder().kind(kind).dataSchema(null).build(); +// when(indexSchemaService.getIndexerInputSchema(kind)).thenReturn(indexSchema); +// } else { +// IndexSchema indexSchema = IndexSchema.builder().kind(kind).dataSchema(schema).build(); +// when(indexSchemaService.getIndexerInputSchema(kind)).thenReturn(indexSchema); +// } +// } +// +// private Map<String, String> createSchema() { +// Map<String, String> schema = new HashMap<>(); +// schema.put("schema1", "keyword"); +// schema.put("schema2", "boolean"); +// schema.put("schema3", "date"); +// schema.put("schema6", "object"); +// return schema; +// } +//} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ReindexServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ReindexServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..502699c256cc6b3eb0301e175dacb320ac9cce12 --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/ReindexServiceTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.indexer.RecordQueryResponse; +import org.opengroup.osdu.core.common.model.indexer.RecordReindexRequest; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.search.Config; +import org.opengroup.osdu.indexer.util.IndexerQueueTaskBuilder; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.modules.junit4.PowerMockRunnerDelegate; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.*; + +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(PowerMockRunner.class) +@PowerMockRunnerDelegate(SpringRunner.class) +@PrepareForTest({Config.class}) +public class ReindexServiceTest { + + private final String cursor = "100"; + + private final String correlationId = UUID.randomUUID().toString(); + + @Mock + private StorageService storageService; + @Mock + private IRequestInfo requestInfo; + @Mock + private IndexerQueueTaskBuilder indexerQueueTaskBuilder; + @Mock + private JaxRsDpsLog log; + @InjectMocks + private ReindexServiceImpl sut; + + private RecordReindexRequest recordReindexRequest; + private RecordQueryResponse recordQueryResponse; + + private Map<String, String> httpHeaders; + + @Before + public void setup() { + initMocks(this); + + mockStatic(UUID.class); + + recordReindexRequest = RecordReindexRequest.builder().kind("tenant:test:test:1.0.0").cursor(cursor).build(); + recordQueryResponse = new RecordQueryResponse(); + + httpHeaders = new HashMap<>(); + httpHeaders.put(DpsHeaders.AUTHORIZATION, "testAuth"); + httpHeaders.put(DpsHeaders.CORRELATION_ID, correlationId); + DpsHeaders standardHeaders = DpsHeaders.createFromMap(httpHeaders); + when(requestInfo.getHeaders()).thenReturn(standardHeaders); + when(requestInfo.getHeadersMapWithDwdAuthZ()).thenReturn(httpHeaders); + when(requestInfo.getHeadersWithDwdAuthZ()).thenReturn(standardHeaders); + } + + @Test + public void should_returnNull_givenNullResponseResult_reIndexRecordsTest() { + try { + recordQueryResponse.setResults(null); + when(storageService.getRecordsByKind(any())).thenReturn(recordQueryResponse); + + String response = sut.reindexRecords(recordReindexRequest, false); + + Assert.assertNull(response); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + @Test + public void should_returnNull_givenEmptyResponseResult_reIndexRecordsTest() { + try { + recordQueryResponse.setResults(new ArrayList<>()); + when(storageService.getRecordsByKind(any())).thenReturn(recordQueryResponse); + + String response = sut.reindexRecords(recordReindexRequest, false); + + Assert.assertNull(response); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + @Ignore + @Test + public void should_returnRecordQueryRequestPayload_givenValidResponseResult_reIndexRecordsTest() { + try { + recordQueryResponse.setCursor(cursor); + List<String> results = new ArrayList<>(); + results.add("test1"); + recordQueryResponse.setResults(results); + + mockStatic(Config.class); + when(Config.getStorageRecordsBatchSize()).thenReturn(1); + + when(storageService.getRecordsByKind(any())).thenReturn(recordQueryResponse); + + String taskQueuePayload = sut.reindexRecords(recordReindexRequest, false); + + Assert.assertEquals("{\"kind\":\"tenant:test:test:1.0.0\",\"cursor\":\"100\"}", taskQueuePayload); + } catch (Exception e) { + fail("Should not throw exception" + e.getMessage()); + } + } + + @Test + public void should_returnRecordChangedMessage_givenValidResponseResult_reIndexRecordsTest() { + try { + List<String> results = new ArrayList<>(); + results.add("test1"); + recordQueryResponse.setResults(results); + when(storageService.getRecordsByKind(any())).thenReturn(recordQueryResponse); + + String taskQueuePayload = sut.reindexRecords(recordReindexRequest, false); + + Assert.assertEquals(String.format("{\"data\":\"[{\\\"id\\\":\\\"test1\\\",\\\"kind\\\":\\\"tenant:test:test:1.0.0\\\",\\\"op\\\":\\\"create\\\"}]\",\"attributes\":{\"correlation-id\":\"%s\"}}", correlationId), taskQueuePayload); + } catch (Exception e) { + fail("Should not throw exception" + e.getMessage()); + } + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/StorageServiceTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/StorageServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e8388a141ee52a42bcdd1d0db27025f92ccbc78e --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/service/StorageServiceTest.java @@ -0,0 +1,251 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.service; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.model.indexer.IndexingStatus; +import org.opengroup.osdu.core.common.model.indexer.JobStatus; +import org.opengroup.osdu.core.common.model.indexer.RecordInfo; +import org.opengroup.osdu.core.common.model.http.HttpResponse; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.provider.interfaces.IRequestInfo; +import org.opengroup.osdu.core.common.http.IUrlFetchService; +import org.opengroup.osdu.core.common.model.indexer.RecordQueryResponse; +import org.opengroup.osdu.core.common.model.indexer.RecordReindexRequest; +import org.opengroup.osdu.core.common.model.indexer.Records; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(SpringRunner.class) +public class StorageServiceTest { + + @Mock + private IUrlFetchService urlFetchService; + @Mock + private JobStatus jobStatus; + @Mock + private JaxRsDpsLog log; + @Mock + private IRequestInfo requestInfo; + @InjectMocks + private StorageServiceImpl sut; + + private List<String> ids; + private static final String RECORD_ID1 = "tenant1:doc:1dbf528e0e0549cab7a08f29fbfc8465"; + private static final String RECORDS_ID2 = "tenant1:doc:15e790a69beb4d789b1f979e2af2e813"; + + @Before + public void setup() { + + String recordChangedMessages = "[{\"id\":\"tenant1:doc:1dbf528e0e0549cab7a08f29fbfc8465\",\"kind\":\"tenant1:testindexer1528919679710:well:1.0.0\",\"op\":\"purge\"}," + + "{\"id\":\"tenant1:doc:15e790a69beb4d789b1f979e2af2e813\",\"kind\":\"tenant1:testindexer1528919679710:well:1.0.0\",\"op\":\"create\"}]"; + + when(this.requestInfo.getHeadersMap()).thenReturn(new HashMap<>()); + when(this.requestInfo.getHeaders()).thenReturn(new DpsHeaders()); + + Type listType = new TypeToken<List<RecordInfo>>() {}.getType(); + + List<RecordInfo> msgs = (new Gson()).fromJson(recordChangedMessages, listType); + jobStatus.initialize(msgs); + ids = Arrays.asList(RECORD_ID1, RECORDS_ID2); + + ReflectionTestUtils.setField(this.sut, "STORAGE_RECORDS_BATCH_SIZE", "20"); + } + + @Test + public void should_return404_givenNullData_getValidStorageRecordsTest() throws URISyntaxException { + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(null); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + should_return404_getValidStorageRecordsTest(); + } + + @Test + public void should_return404_givenEmptyData_getValidStorageRecordsTest() throws URISyntaxException { + + String emptyDataFromStorage = "{\"records\":[],\"notFound\":[]}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(emptyDataFromStorage); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + should_return404_getValidStorageRecordsTest(); + } + + @Test + public void should_returnOneValidRecords_givenValidData_getValidStorageRecordsTest() throws URISyntaxException { + + String validDataFromStorage = "{\"records\":[{\"id\":\"testid\", \"version\":1, \"kind\":\"tenant:test:test:1.0.0\"}],\"notFound\":[\"invalid1\"], \"conversionStatuses\": []}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(validDataFromStorage); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + Records storageRecords = this.sut.getStorageRecords(ids); + + assertEquals(1, storageRecords.getRecords().size()); + } + + @Test + public void should_logMissingRecord_given_storageMissedRecords() throws URISyntaxException { + + String validDataFromStorage = "{\"records\":[{\"id\":\"tenant1:doc:1dbf528e0e0549cab7a08f29fbfc8465\", \"version\":1, \"kind\":\"tenant:test:test:1.0.0\"}],\"notFound\":[]}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(validDataFromStorage); + + when(this.urlFetchService.sendRequest(any())).thenReturn(httpResponse); + Records storageRecords = this.sut.getStorageRecords(ids); + + assertEquals(1, storageRecords.getRecords().size()); + verify(this.jobStatus).addOrUpdateRecordStatus(singletonList(RECORDS_ID2), IndexingStatus.FAIL, HttpStatus.NOT_FOUND.value(), "Partial response received from Storage service - missing records", "Partial response received from Storage service: tenant1:doc:15e790a69beb4d789b1f979e2af2e813"); + } + + @Test + public void should_returnValidJobStatus_givenFailedUnitsConversion_processRecordChangedMessageTest() throws URISyntaxException { + String validDataFromStorage = "{\"records\":[{\"id\":\"tenant1:doc:15e790a69beb4d789b1f979e2af2e813\", \"version\":1, \"kind\":\"tenant:test:test:1.0.0\"}],\"notFound\":[],\"conversionStatuses\":[{\"id\":\"tenant1:doc:15e790a69beb4d789b1f979e2af2e813\",\"status\":\"ERROR\",\"errors\":[\"crs conversion failed\"]}]}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(validDataFromStorage); + + when(this.urlFetchService.sendRequest(any())).thenReturn(httpResponse); + Records storageRecords = this.sut.getStorageRecords(singletonList(RECORDS_ID2)); + + assertEquals(1, storageRecords.getRecords().size()); + verify(this.jobStatus).addOrUpdateRecordStatus(RECORDS_ID2, IndexingStatus.WARN, HttpStatus.BAD_REQUEST.value(), "crs conversion failed", String.format("record-id: %s | %s", "tenant1:doc:15e790a69beb4d789b1f979e2af2e813", "crs conversion failed")); + } + + @Test + public void should_returnValidResponse_givenValidRecordQueryRequest_getRecordListByKind() throws Exception { + + RecordReindexRequest recordReindexRequest = RecordReindexRequest.builder().kind("tenant:test:test:1.0.0").cursor("100").build(); + + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setBody(new Gson().toJson(recordReindexRequest, RecordReindexRequest.class)); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + RecordQueryResponse recordQueryResponse = this.sut.getRecordsByKind(recordReindexRequest); + + assertEquals("100", recordQueryResponse.getCursor()); + assertNull(recordQueryResponse.getResults()); + } + + @Test + public void should_returnValidResponse_givenValidKind_getSchemaByKind() throws Exception { + + String validSchemaFromStorage = "{" + + " \"kind\": \"tenant:test:test:1.0.0\"," + + " \"schema\": [" + + " {" + + " \"path\": \"msg\"," + + " \"kind\": \"string\"" + + " }," + + " {" + + " \"path\": \"references.entity\"," + + " \"kind\": \"string\"" + + " }" + + " ]," + + " \"ext\": null" + + "}"; + String kind = "tenant:test:test:1.0.0"; + + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setResponseCode(HttpStatus.OK.value()); + httpResponse.setBody(validSchemaFromStorage); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + String recordSchemaResponse = this.sut.getStorageSchema(kind); + + assertNotNull(recordSchemaResponse); + } + + @Test + public void should_returnNullResponse_givenAbsentKind_getSchemaByKind() throws Exception { + + String kind = "tenant:test:test:1.0.0"; + + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setResponseCode(HttpStatus.NOT_FOUND.value()); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + + String recordSchemaResponse = this.sut.getStorageSchema(kind); + + assertNull(recordSchemaResponse); + } + + @Test + public void should_returnOneValidRecords_givenValidData_getValidStorageRecordsWithInvalidConversionTest() throws URISyntaxException { + + String validDataFromStorage = "{\"records\":[{\"id\":\"testid\", \"version\":1, \"kind\":\"tenant:test:test:1.0.0\"}],\"notFound\":[\"invalid1\"],\"conversionStatuses\": [{\"id\":\"testid\",\"status\":\"ERROR\",\"errors\":[\"conversion error occurred\"] } ]}"; + + HttpResponse httpResponse = mock(HttpResponse.class); + Mockito.when(httpResponse.getBody()).thenReturn(validDataFromStorage); + + when(this.urlFetchService.sendRequest(ArgumentMatchers.any())).thenReturn(httpResponse); + Records storageRecords = this.sut.getStorageRecords(ids); + + assertEquals(1, storageRecords.getRecords().size()); + + assertEquals(1, storageRecords.getConversionStatuses().get(0).getErrors().size()); + + assertEquals("conversion error occurred", storageRecords.getConversionStatuses().get(0).getErrors().get(0)); + } + + private void should_return404_getValidStorageRecordsTest() { + try { + this.sut.getStorageRecords(ids); + fail("Should throw exception"); + } catch (AppException e) { + assertEquals(HttpStatus.NOT_FOUND.value(), e.getError().getCode()); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } +} diff --git a/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImplTest.java b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..58dbef93eb28c9c3c471bf47193d883964fd831a --- /dev/null +++ b/provider/indexer-reference/src/test/java/org/opengroup/osdu/indexer/util/ServiceAccountJwtGcpClientImplTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2020 Google LLC + * Copyright 2020 EPAM Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.opengroup.osdu.indexer.util; + +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.services.iam.v1.Iam; +import com.google.api.services.iam.v1.model.SignJwtResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.opengroup.osdu.core.common.model.http.AppException; +import org.opengroup.osdu.core.common.logging.JaxRsDpsLog; +import org.opengroup.osdu.core.common.model.search.DeploymentEnvironment; +import org.opengroup.osdu.core.common.model.search.IdToken; +import org.opengroup.osdu.core.common.provider.interfaces.IJwtCache; +import org.opengroup.osdu.core.common.search.Config; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.powermock.api.mockito.PowerMockito.when; + +@Ignore +@RunWith(SpringRunner.class) +@PrepareForTest({GoogleNetHttpTransport.class, GoogleCredential.class, NetHttpTransport.class, SignJwtResponse.class, Iam.Builder.class, HttpClients.class, EntityUtils.class, Config.class}) +public class ServiceAccountJwtGcpClientImplTest { + + private static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1UVXlPREE0TXpFd09BPT0ifQ.eyJzdWIiOiJtemh1OUBzbGIuY29tIiwiaXNzIjoic2F1dGgtcHJldmlldy5zbGIuY29tIiwiYXVkIjoidGVzdC1zbGJkZXYtZGV2cG9ydGFsLnNsYmFwcC5jb20iLCJpYXQiOjE1MjgxNDg5MTUsImV4cCI6MTUyODIzNTMxNSwicHJvdmlkZXIiOiJzbGIuY29tIiwiY2xpZW50IjoidGVzdC1zbGJkZXYtZGV2cG9ydGFsLnNsYmFwcC5jb20iLCJ1c2VyaWQiOiJtemh1OUBzbGIuY29tIiwiZW1haWwiOiJtemh1OUBzbGIuY29tIiwiYXV0aHoiOiJ7XCJhY2NvdW50Q291bnRyeVwiOntcImNvZGVcIjpcInVzXCIsXCJpZFwiOjU3MTU5OTkxMDE4MTI3MzYsXCJuYW1lXCI6XCJVbml0ZWQgU3RhdGVzIG9mIEFtZXJpY2FcIn0sXCJhY2NvdW50SWRcIjo1NjkxODc4ODMzOTEzODU2LFwiYWNjb3VudE5hbWVcIjpcIlNJUyBJbnRlcm5hbCBIUVwiLFwiY3JlYXRlZFwiOlwiMjAxOC0wNS0wM1QxNzoyNTo1NS40NDNaXCIsXCJkZXBhcnRtZW50TWFuYWdlclwiOm51bGwsXCJzdWJzY3JpcHRpb25zXCI6W3tcImFjY291bnRJZFwiOjU2OTE4Nzg4MzM5MTM4NTYsXCJjb250cmFjdElkXCI6NTc1MTcwMDIxMjE1NDM2OCxcImNyZWF0ZWRcIjpcIjIwMTgtMDUtMDNUMTc6MzM6MDkuNTczWlwiLFwiY3JtQ29udHJhY3RJZFwiOlwiU0lTLUlOVEVSTkFMLUhRLVFBXCIsXCJjcm1Db250cmFjdEl0ZW1JZFwiOlwiZGV2bGlcIixcImV4cGlyYXRpb25cIjpcIjE5NzAtMDEtMDFUMDA6MDA6MDAuMDAwWlwiLFwiaWRcIjo1MDc5Mjg4NTA0MTIzMzkyLFwicHJvZHVjdFwiOntcImNvZGVcIjpcImRldmVsb3Blci1saWdodFwiLFwiY29tY2F0TmFtZVwiOlwiTm90IGluIENvbUNhdFwiLFwiZmVhdHVyZVNldHNcIjpbe1wiYXBwbGljYXRpb25cIjp7XCJjb2RlXCI6XCJhcGlkZXZlbG9wZXJwb3J0YWxcIixcImlkXCI6NTE2ODkzMDY5NTkzODA0OCxcIm5hbWVcIjpcIkFQSSBEZXZlbG9wZXIgUG9ydGFsXCIsXCJ0eXBlXCI6XCJXZWJBcHBcIn0sXCJjbGFpbXNcIjpudWxsLFwiaWRcIjo1MTkxNTcyMjg3MTI3NTUyLFwibmFtZVwiOlwiRGV2ZWxvcGVyXCIsXCJ0eXBlXCI6XCJCQVNFXCJ9XSxcImlkXCI6NTE1MDczMDE1MTI2NDI1NixcIm5hbWVcIjpcIkRldmVsb3BlciBQb3J0YWxcIixcInBhcnROdW1iZXJcIjpcIlNERUwtUEItU1VCVVwifX1dLFwidXNlckVtYWlsXCI6XCJtemh1OUBzbGIuY29tXCIsXCJ1c2VyTmFtZVwiOlwiTWluZ3lhbmcgWmh1XCJ9XG4iLCJsYXN0bmFtZSI6IlpodSIsImZpcnN0bmFtZSI6Ik1pbmd5YW5nIiwiY291bnRyeSI6IiIsImNvbXBhbnkiOiIiLCJqb2J0aXRsZSI6IiIsInN1YmlkIjoiNDE3YjczMjktYmMwNy00OTFmLWJiYzQtZTQ1YjRhMWFiYjVjLVd3U0c0dyIsImlkcCI6ImNvcnAyIiwiaGQiOiJzbGIuY29tIn0.WQfGr1Xu-6IdaXdoJ9Fwzx8O2el1UkFPWo1vk_ujiAfdOjAR46UG5SrBC7mzC7gYRyK3a4fimBmbv3uRVJjTNXdxXRLZDw0SvXUMIOqjUGLom491ESbrtka_Xz7vGO-tWyDcEQDTfFzQ91LaVN7XdzL18_EDTXZoPhKb-zquyk9WLQxP9Mw-3Yh-UrbvC9nl1-GRn1IVbzp568kqkpOVUFM9alYSGw-oMGDZNt1DIYOJnpGaw2RB5B3AKvNivZH_Xdac7ZTzQbsDOt8B8DL2BphuxcJ9jshCJkM2SHQ15uErv8sfnzMwdF08e_0QcC_30I8eX9l8yOu6TnwwqlXunw"; + + @Mock + private JaxRsDpsLog log; + @Mock + private GoogleCredential credential; + @Mock + private NetHttpTransport httpTransport; + @Mock + private SignJwtResponse signJwtResponse; + @Mock + private Iam iam; + @Mock + private Iam.Projects iamProject; + @Mock + private Iam.Projects.ServiceAccounts iamProjectServiceAccounts; + @Mock + private Iam.Projects.ServiceAccounts.SignJwt signJwt; + @Mock + private CloseableHttpClient httpClient; + @Mock + private CloseableHttpResponse httpResponse; +// @InjectMocks +// private TenantInfoServiceImpl tenantInfoServiceProvider; +// @Mock +// private TenantInfoServiceImpl tenantInfoService; + @Mock + private IJwtCache cacheService; + @InjectMocks @Spy + private ServiceAccountJwtGcpClientImpl sut; + @Before + public void setup() throws Exception { + initMocks(this); + +// mockStatic(GoogleNetHttpTransport.class); +// mockStatic(GoogleCredential.class); +// mockStatic(HttpClients.class); +// mockStatic(EntityUtils.class); +// mockStatic(Config.class); + + when(GoogleNetHttpTransport.newTrustedTransport()).thenReturn(httpTransport); + when(GoogleCredential.getApplicationDefault()).thenReturn(credential); + when(credential.createScopedRequired()).thenReturn(true); + when(credential.createScoped(any())).thenReturn(credential); + when(HttpClients.createDefault()).thenReturn(httpClient); + when(httpClient.execute(any())).thenReturn(httpResponse); + when(Config.getDeploymentEnvironment()).thenReturn(DeploymentEnvironment.LOCAL); + when(Config.getGoogleAudiences()).thenReturn("aud"); + +// when(this.tenantInfoServiceProvider).thenReturn(this.tenantInfoService); + + TenantInfo tenantInfo = new TenantInfo(); + tenantInfo.setServiceAccount("tenant"); +// when(this.tenantInfoService.getTenantInfo()).thenReturn(tenantInfo); + + when(this.sut.getIam()).thenReturn(iam); + when(this.iam.projects()).thenReturn(iamProject); + when(this.iamProject.serviceAccounts()).thenReturn(iamProjectServiceAccounts); + when(this.iamProjectServiceAccounts.signJwt(any(), any())).thenReturn(signJwt); + when(this.signJwt.execute()).thenReturn(signJwtResponse); + when(this.signJwtResponse.getSignedJwt()).thenReturn("testJwt"); + + } + + @Test + public void should_returnCachedToken_givenCachedToken_getIdTokenTest() { + String tokenValue = "tokenValue"; + IdToken idToken = IdToken.builder().tokenValue(tokenValue).expirationTimeMillis(System.currentTimeMillis() + 10000000L).build(); + when(this.cacheService.get(any())).thenReturn(idToken); + + String returnedIdToken = this.sut.getIdToken(tokenValue); + + Assert.assertEquals(tokenValue, returnedIdToken); + } + + @Test + public void should_returnValidToken_getIdTokenTest() throws Exception { + when(EntityUtils.toString(any())).thenReturn(String.format("{\"id_token\":\"%s\"}", JWT_TOKEN)); + + String returnedToken = this.sut.getIdToken("tenant"); + + Assert.assertEquals(JWT_TOKEN, returnedToken); + } + + @Test + public void should_return500_given_invalidJWTResponse_getIdTokenException() { + try { + when(EntityUtils.toString(any())).thenReturn(String.format("{\"id_token\":\"%s\"}", "invalid jwt")); + + this.sut.getIdToken("tenant"); + fail("Should throw exception"); + } catch (AppException e) { + Assert.assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getError().getCode()); + Assert.assertEquals("Invalid token, error decoding", e.getError().getMessage()); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } + + @Test + public void should_return403_given_missingIdTokenResponse_getIdTokenException() { + try { + when(EntityUtils.toString(any())).thenReturn("{}"); + + this.sut.getIdToken("tenant"); + fail("Should throw exception"); + } catch (AppException e) { + Assert.assertEquals(HttpStatus.SC_FORBIDDEN, e.getError().getCode()); + Assert.assertEquals("The user is not authorized to perform this action", e.getError().getMessage()); + } catch (Exception e) { + fail("Should not throw this exception" + e.getMessage()); + } + } +} diff --git a/provider/indexer-reference/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/provider/indexer-reference/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000000000000000000000000000000000..ca6ee9cea8ec189a088d50559325d4e84ff8ad09 --- /dev/null +++ b/provider/indexer-reference/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file