diff --git a/NOTICE b/NOTICE index 5196dd9e9e341fbaee2f8ec7e17806cf0f7f8e0a..2f780f61f12577ce576f03f733f404faba769d03 100644 --- a/NOTICE +++ b/NOTICE @@ -25,7 +25,6 @@ The following software have components provided under the terms of this license: - Apache Log4j Core (from ) - Apache Log4j JUL Adapter (from ) - Apache Log4j SLF4J Binding (from ) -- Apache Log4j to SLF4J Adapter (from ) - Asynchronous Http Client (from ) - Asynchronous Http Client Netty Utils (from ) - Bean Validation API (from http://beanvalidation.org) @@ -86,6 +85,7 @@ The following software have components provided under the terms of this license: - Microsoft Application Insights Java SDK Spring Boot starter (from https://github.com/Microsoft/ApplicationInsights-Java) - Microsoft Application Insights Java SDK Web Module (from https://github.com/Microsoft/ApplicationInsights-Java) - Microsoft Application Insights Log4j 2 Appender (from https://github.com/Microsoft/ApplicationInsights-Java) +- Microsoft Azure Netty HTTP Client Library (from https://github.com/Azure/azure-sdk-for-java) - Mockito (from http://mockito.org) - Netty Reactive Streams Implementation (from ) - Netty/Buffer (from http://netty.io/) @@ -113,7 +113,6 @@ The following software have components provided under the terms of this license: - Okio (from ) - OpenCensus (from https://github.com/census-instrumentation/opencensus-java) - OpenCensus (from https://github.com/census-instrumentation/opencensus-java) -- Reactive Object Pool (from https://github.com/reactor/reactor-pool) - Reactive Streams Netty driver (from https://github.com/reactor/reactor-netty) - Retrofit (from ) - SnakeYAML (from http://www.snakeyaml.org) @@ -125,7 +124,6 @@ The following software have components provided under the terms of this license: - Spring Boot AutoConfigure (from http://projects.spring.io/spring-boot/) - Spring Boot Json Starter (from https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-starters/spring-boot-starter-json) - Spring Boot Log4J2 Starter (from http://projects.spring.io/spring-boot/) -- Spring Boot Logging Starter (from http://projects.spring.io/spring-boot/) - Spring Boot Starter (from http://projects.spring.io/spring-boot/) - Spring Boot Tomcat Starter (from http://projects.spring.io/spring-boot/) - Spring Boot Validation Starter (from http://projects.spring.io/spring-boot/) @@ -246,8 +244,6 @@ EPL-1.0 The following software have components provided under the terms of this license: - JUnit Jupiter (Aggregator) (from https://junit.org/junit5/) -- Logback Classic Module (from ) -- Logback Core Module (from ) - Microsoft Application Insights Java SDK Core (from https://github.com/Microsoft/ApplicationInsights-Java) - Microsoft Application Insights Java SDK Spring Boot starter (from https://github.com/Microsoft/ApplicationInsights-Java) - Microsoft Application Insights Java SDK Web Module (from https://github.com/Microsoft/ApplicationInsights-Java) @@ -315,8 +311,6 @@ The following software have components provided under the terms of this license: - Java Native Access (from https://github.com/java-native-access/jna) - Java Native Access Platform (from https://github.com/java-native-access/jna) - Javassist (from http://www.javassist.org/) -- Logback Classic Module (from ) -- Logback Core Module (from ) - Microsoft Application Insights Java SDK Core (from https://github.com/Microsoft/ApplicationInsights-Java) - Microsoft Application Insights Java SDK Spring Boot starter (from https://github.com/Microsoft/ApplicationInsights-Java) - Microsoft Application Insights Java SDK Web Module (from https://github.com/Microsoft/ApplicationInsights-Java) diff --git a/pom.xml b/pom.xml index ed1aae4c7264546dfb58fdedaef60fb064ae90c2..c4e11784bdac94c5d19348ee61f56d79a037b919 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ org.opengroup.osdu core-lib-azure jar - 0.0.16 + 0.0.17 core-lib-azure @@ -57,6 +57,12 @@ + + com.azure + azure-core-http-netty + 1.5.3 + + org.springframework @@ -80,6 +86,16 @@ org.opengroup.osdu os-core-common 0.3.4 + + + org.springframework.boot + spring-boot-starter-logging + + + org.apache.logging.log4j + log4j-api + + @@ -159,7 +175,6 @@ ${jackson.version} test - diff --git a/src/main/java/org/opengroup/osdu/azure/di/CosmosDBConfiguration.java b/src/main/java/org/opengroup/osdu/azure/di/CosmosDBConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..1f90097fd3e32d58f45ff25b21de295252b2a152 --- /dev/null +++ b/src/main/java/org/opengroup/osdu/azure/di/CosmosDBConfiguration.java @@ -0,0 +1,20 @@ +package org.opengroup.osdu.azure.di; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +/** + * A configuration bean class to set up CosmosDb variables. + */ +@Configuration +@Getter +public class CosmosDBConfiguration { + + @Value("${tenantInfo.container.name}") + private String tenantInfoContainer; + + @Value("${azure.cosmosdb.database}") + private String cosmosDBName; + +} diff --git a/src/main/java/org/opengroup/osdu/azure/multitenancy/TenantFactoryImpl.java b/src/main/java/org/opengroup/osdu/azure/multitenancy/TenantFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..9d47e4d957f812896a4d4e587eef9ba67052788c --- /dev/null +++ b/src/main/java/org/opengroup/osdu/azure/multitenancy/TenantFactoryImpl.java @@ -0,0 +1,119 @@ +// Copyright © Microsoft Corporation +// +// 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.azure.multitenancy; + +import org.opengroup.osdu.azure.CosmosStore; +import org.opengroup.osdu.azure.di.CosmosDBConfiguration; +import org.opengroup.osdu.core.common.cache.ICache; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; +import org.opengroup.osdu.core.common.provider.interfaces.ITenantFactory; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; + +/** + * Implementation for ITenantFactory. + */ +@Component +@Lazy +public class TenantFactoryImpl implements ITenantFactory { + + @Autowired + @Lazy + private CosmosDBConfiguration cosmosDBConfiguration; + + @Autowired + @Lazy + private CosmosStore cosmosStore; + + private Map tenants; + + /** + * @param tenantName Tenant name + * @return true or false depending on whether tenant is present + */ + public boolean exists(final String tenantName) { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.containsKey(tenantName); + } + + /** + * @param tenantName Tenant name + * @return tenantInfo object + */ + public TenantInfo getTenantInfo(final String tenantName) { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.get(tenantName); + } + + /** + * @return list of tenantInfo objects for all the tenants + */ + public Collection listTenantInfo() { + if (this.tenants == null) { + initTenants(); + } + return this.tenants.values(); + } + + /** + * @param tenantName Tenant name + * @param host Host name + * @param port Port + * @param expireTimeSeconds Expiry time in seconds + * @param classOfV Class reference + * @param Template class + * @return cache + */ + public ICache createCache(final String tenantName, final String host, final int port, final int expireTimeSeconds, final Class classOfV) { + return null; + } + + + /** + * Flush the cache. + */ + public void flushCache() { + + } + + /** + * Initialise the local cache for tenants. + */ + private void initTenants() { + this.tenants = new HashMap<>(); + + // TODO partition id should not be required because tenant details will be kept in a known partition + cosmosStore.findAllItems(DpsHeaders.DATA_PARTITION_ID, cosmosDBConfiguration.getCosmosDBName(), cosmosDBConfiguration.getTenantInfoContainer(), TenantInfoDoc.class). + forEach(tenantInfoDoc -> { + TenantInfo ti = new TenantInfo(); + String tenantName = tenantInfoDoc.getId(); + ti.setName(tenantName); + ti.setComplianceRuleSet(tenantInfoDoc.getComplianceRuleSet()); + ti.setDataPartitionId(tenantName); + this.tenants.put(tenantName, ti); + } + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/opengroup/osdu/azure/multitenancy/TenantInfoDoc.java b/src/main/java/org/opengroup/osdu/azure/multitenancy/TenantInfoDoc.java new file mode 100644 index 0000000000000000000000000000000000000000..dbdfc307962d82d1b83f3718449cc01c5dd8393b --- /dev/null +++ b/src/main/java/org/opengroup/osdu/azure/multitenancy/TenantInfoDoc.java @@ -0,0 +1,33 @@ +// Copyright © Microsoft Corporation +// +// 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.azure.multitenancy; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * TenantInfoDoc class. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class TenantInfoDoc { + private String id; + private String serviceprincipalAppId; + private String complianceRuleSet; + private String[] groups; +} diff --git a/src/main/java/org/opengroup/osdu/azure/util/AzureServicePrincipal.java b/src/main/java/org/opengroup/osdu/azure/util/AzureServicePrincipal.java new file mode 100644 index 0000000000000000000000000000000000000000..709cdc14da0d83fce60510a2fb8c5d94e3bbd80c --- /dev/null +++ b/src/main/java/org/opengroup/osdu/azure/util/AzureServicePrincipal.java @@ -0,0 +1,94 @@ +// Copyright © Microsoft Corporation +// +// 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.azure.util; + +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpMethod; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.netty.NettyAsyncHttpClientBuilder; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import reactor.core.publisher.Mono; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Class to generate the AAD authentication tokens. + */ +public final class AzureServicePrincipal { + + /** + * @param sp_id AZURE CLIENT ID + * @param sp_secret AZURE CLIENT SECRET + * @param tenant_id AZURE TENANT ID + * @param app_resource_id AZURE APP RESOURCE ID + * @return AUTHENTICATION TOKEN + * @throws UnsupportedEncodingException throws UnsupportedEncodingException + */ + public String getIdToken(final String sp_id, final String sp_secret, final String tenant_id, final String app_resource_id) throws UnsupportedEncodingException { + + String aadEndpoint = String.format("https://login.microsoftonline.com/%s/oauth2/token", tenant_id); + HttpRequest httpRequest = new HttpRequest(HttpMethod.POST, aadEndpoint); + Map parameters = new HashMap<>(); + parameters.put("grant_type", "client_credentials"); + parameters.put("client_id", sp_id); + parameters.put("client_secret", sp_secret); + parameters.put("resource", app_resource_id); + httpRequest.setBody(getParamsString(parameters)); + + HttpClient client = createHttpClient(); + + Mono response = client.send(httpRequest); + String content = Objects.requireNonNull(response.block()).getBodyAsString().block(); + + Gson gson = new Gson(); + JsonObject jsonObject = gson.fromJson(content, JsonObject.class); + return jsonObject.get("access_token").getAsString(); + } + + /** + * @param params Map of request parameters + * @return parameter string + * @throws UnsupportedEncodingException throws exception unsupported encoding is found + */ + private String getParamsString(final Map params) + throws UnsupportedEncodingException { + StringBuilder result = new StringBuilder(); + + for (Map.Entry entry : params.entrySet()) { + result.append(URLEncoder.encode(entry.getKey(), "UTF-8")); + result.append("="); + result.append(URLEncoder.encode(entry.getValue(), "UTF-8")); + result.append("&"); + } + + String resultString = result.toString(); + return resultString.length() > 0 + ? resultString.substring(0, resultString.length() - 1) + : resultString; + } + + /** + * @return HttpClient + */ + HttpClient createHttpClient() { + return new NettyAsyncHttpClientBuilder().build(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..d817f227c62398260efe79c3a1446363c18c94d7 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,19 @@ +# +# Copyright © Microsoft Corporation +# +# 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. +# + +# Azure CosmosDB configuration +tenantInfo.container.name=TenantInfo +azure.cosmosdb.database=${cosmosdb_database} \ No newline at end of file diff --git a/src/test/java/org/opengroup/osdu/azure/multitenancy/TenantFactoryImplTest.java b/src/test/java/org/opengroup/osdu/azure/multitenancy/TenantFactoryImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..63714e97ddfa9024f26ccee4e0a04b5b74db1f27 --- /dev/null +++ b/src/test/java/org/opengroup/osdu/azure/multitenancy/TenantFactoryImplTest.java @@ -0,0 +1,114 @@ +package org.opengroup.osdu.azure.multitenancy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opengroup.osdu.core.common.model.http.DpsHeaders; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.when; +import org.opengroup.osdu.azure.CosmosStore; +import org.opengroup.osdu.azure.di.CosmosDBConfiguration; +import org.opengroup.osdu.core.common.model.tenant.TenantInfo; + +@ExtendWith(MockitoExtension.class) +public class TenantFactoryImplTest { + + private static final String tenantName = "opendes"; + private static final String cosmosDatabase = "cosmos-database"; + private static final String cosmosContainer = "tenant-info"; + private static final String notFound = "not-found"; + private static final String[] groups = {"first"}; + private static final String serviceprincipalAppId = "sp-id"; + private static final String complianceRuleSet = "compliance-rule-set"; + + @InjectMocks + private TenantFactoryImpl tenantFactory; + + @Mock + private CosmosDBConfiguration cosmosDBConfiguration; + + @Mock + private CosmosStore cosmosStore; + + @BeforeEach + public void init() { + when(cosmosDBConfiguration.getCosmosDBName()).thenReturn(cosmosDatabase); + when(cosmosDBConfiguration.getTenantInfoContainer()).thenReturn(cosmosContainer); + } + + @Test + public void returnsTrueWhenTenantNameFound() { + TenantInfoDoc doc = new TenantInfoDoc(tenantName, serviceprincipalAppId, complianceRuleSet, groups); + + doReturn(Collections.singletonList(doc)).when(cosmosStore).findAllItems(eq(DpsHeaders.DATA_PARTITION_ID), eq(cosmosDatabase), eq(cosmosContainer), any()); + boolean result = tenantFactory.exists(tenantName); + + assertTrue(result); + } + + @Test + public void returnsFalseWhenTenantNameNotFound() { + TenantInfoDoc doc = new TenantInfoDoc(tenantName + notFound, serviceprincipalAppId, complianceRuleSet, groups); + + doReturn(Collections.singletonList(doc)).when(cosmosStore).findAllItems(eq(DpsHeaders.DATA_PARTITION_ID), eq(cosmosDatabase), eq(cosmosContainer), any()); + boolean result = tenantFactory.exists(tenantName); + + assertFalse(result); + } + + @Test + public void returnsTenantInfoObjectWhenTenantNameFound() { + TenantInfoDoc doc = new TenantInfoDoc(tenantName, serviceprincipalAppId, complianceRuleSet, groups); + + doReturn(Collections.singletonList(doc)).when(cosmosStore).findAllItems(eq(DpsHeaders.DATA_PARTITION_ID), eq(cosmosDatabase), eq(cosmosContainer), any()); + TenantInfo result = tenantFactory.getTenantInfo(tenantName); + + TenantInfo expected = new TenantInfo(); + expected.setName(tenantName); + expected.setDataPartitionId(tenantName); + expected.setComplianceRuleSet(complianceRuleSet); + + assertEquals(expected, result); + } + + @Test + public void returnsNullWhenTenantNameNotFound() { + TenantInfoDoc doc = new TenantInfoDoc(tenantName + notFound, serviceprincipalAppId, complianceRuleSet, groups); + + doReturn(Collections.singletonList(doc)).when(cosmosStore).findAllItems(eq(DpsHeaders.DATA_PARTITION_ID), eq(cosmosDatabase), eq(cosmosContainer), any()); + TenantInfo result = tenantFactory.getTenantInfo(tenantName); + + assertNull(result); + } + + @Test + public void returnsListOfAllTenants() { + TenantInfoDoc doc = new TenantInfoDoc(tenantName, serviceprincipalAppId, complianceRuleSet, groups); + + doReturn(Collections.singletonList(doc)).when(cosmosStore).findAllItems(eq(DpsHeaders.DATA_PARTITION_ID), eq(cosmosDatabase), eq(cosmosContainer), any()); + List result = new ArrayList<>(tenantFactory.listTenantInfo()); + + TenantInfo tenantInfo = new TenantInfo(); + tenantInfo.setName(tenantName); + tenantInfo.setDataPartitionId(tenantName); + tenantInfo.setComplianceRuleSet(complianceRuleSet); + + List expected = Collections.singletonList(tenantInfo); + + assertEquals(expected, result); + } +} diff --git a/src/test/java/org/opengroup/osdu/azure/util/AzureServicePrincipalTest.java b/src/test/java/org/opengroup/osdu/azure/util/AzureServicePrincipalTest.java new file mode 100644 index 0000000000000000000000000000000000000000..53d90545ef34ca90d605b401c776229de1ed22f3 --- /dev/null +++ b/src/test/java/org/opengroup/osdu/azure/util/AzureServicePrincipalTest.java @@ -0,0 +1,79 @@ +package org.opengroup.osdu.azure.util; + +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class AzureServicePrincipalTest { + + private static final String accessToken = "some-access-token"; + private static final String spId = "client-id"; + private static final String spSecret = "client-secret"; + private static final String tenantId = "tenant-id"; + private static final String appResourceId = "app-resource-id"; + + @Mock + private HttpClient httpClient; + + @Mock + private Mono responseMono; + + @Mock + private HttpResponse httpResponse; + + @Spy + private AzureServicePrincipal azureServicePrincipal; + + @Test + public void ShouldSuccessfullyGenerateToken() throws Exception { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("access_token", accessToken); + + Mono contentMono = Mono.just(jsonObject.toString()); + + when(azureServicePrincipal.createHttpClient()).thenReturn(httpClient); + when(httpClient.send(any(HttpRequest.class))).thenReturn(responseMono); + when(responseMono.block()).thenReturn(httpResponse); + when(httpResponse.getBodyAsString()).thenReturn(contentMono); + + String result = azureServicePrincipal.getIdToken(spId, spSecret, tenantId, appResourceId); + + assertEquals(accessToken, result); + verify(httpClient, times(1)).send(any(HttpRequest.class)); + verify(responseMono, times(1)).block(); + verify(httpResponse, times(1)).getBodyAsString(); + + } + + /** + * This test is added for end to verification whether tokens are getting generated. + */ + // + @Disabled + @Test + public void VerifyingEndToEndScenario() throws Exception { + + String spId = ""; + String spSecret = ""; + String tenantId = ""; + String appResourceId = ""; + + String result = new AzureServicePrincipal().getIdToken(spId, spSecret, tenantId, appResourceId); + + } +}