+
+
+ +

This sample demonstrates how to take advantage of MSAL JS for adding Azure AD authentication to your + AngularJS apps.

+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/README-a.md b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/README-a.md new file mode 100644 index 000000000000..673ba43027a1 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/README-a.md @@ -0,0 +1,65 @@ +## Overview +This sample project demonstrates how to use Azure CosmosDB via Spring Boot Starter `azure-cosmosdb-spring-boot-starter` to store data in and retrieve data from your Azure Cosmos DB. + +## Prerequisites + +* An Azure subscription; if you don't already have an Azure subscription, you can activate your [MSDN subscriber benefits](https://azure.microsoft.com/en-us/pricing/member-offers/msdn-benefits-details/) or sign up for a [free Azure account](https://azure.microsoft.com/en-us/free/). + +* A [Java Development Kit (JDK)](http://www.oracle.com/technetwork/java/javase/downloads/), version 1.8. + +* [Apache Maven](http://maven.apache.org/), version 3.0 or later. + +## Quick Start + +### Create an Azure Cosmos DB on Azure + +1. Go to [Azure portal](https://portal.azure.com/) and click +New . +2. Click Databases, and then click Azure Cosmos DB to create your database. +3. Navigate to the database you have created, and click Access keys and copy your URI and access keys for your database. + +### Config the sample + +1. Navigate to `src/main/resources` and open `application.properties`. +2. replace below properties in `application.properties` with information of your database. + ```properties + azure.cosmosdb.uri=your-cosmosdb-uri + azure.cosmosdb.key=your-cosmosdb-key + azure.cosmosdb.database=your-cosmosdb-databasename + ``` + +### Run the sample + +1. Change directory to folder `azure-spring-boot-sample-cosmosdb`. +2. Run below commands. + + - Use Maven + + ``` + mvn package + java -jar target/azure-spring-boot-sample-cosmosdb-0.0.1-beta.1.jar + ``` + + - Use Gradle + + ``` + gradle bootRepackage + java -jar build/libs/azure-spring-boot-sample-cosmosdb-0.0.1-beta.1.jar + ``` + +### Known issue + +Directly running the sample app from IDE IntelliJ or Eclipse has below security exception if using the *released* starter. The root cause is that the release `spring-data-azure-cosmosdb` jar is code-signed by us. We're working actively to resolve this issue. + +``` +Caused by: java.lang.SecurityException: class "com.microsoft.azure.sample.User_Accessor_yhb3bq"'s signer information does not match signer information of other classes in the same package + at java.lang.ClassLoader.checkCerts(ClassLoader.java:898) ~[na:1.8.0_131] + at java.lang.ClassLoader.preDefineClass(ClassLoader.java:668) ~[na:1.8.0_131] + at java.lang.ClassLoader.defineClass(ClassLoader.java:761) ~[na:1.8.0_131] +``` + +If `com.fasterxml.jackson.databind.JsonMappingException` is thrown during deserialization, with error message `Can not construct instance of {your.pojo.class}: no suitable constructor found, can not deserialize from Object value (missing default constructor...`, add [Lombok annotatations](https://projectlombok.org/features/all) `@Data` and `@AllArgsConstructor` for your POJO class, or use [Jackson annotations](https://github.com/FasterXML/jackson-annotations#using-constructors-or-factory-methods) `@JsonCreator` and `@JsonProperty` for the full argument constructor. + +### More details + +Please refer to [this article](https://docs.microsoft.com/en-us/java/azure/spring-framework/configure-spring-boot-starter-java-app-with-cosmos-db) for the tutorial about how to use the Spring Boot Starter with Azure Cosmos DB API. + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/pom.xml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/pom.xml new file mode 100644 index 000000000000..f414a39b51e6 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + com.microsoft.azure + azure-spring-boot-samples + 0.0.1-beta.1 + ../pom.xml + + + azure-cosmosdb-spring-boot-sample + jar + + Azure Cosmos DB Spring Boot Starter Sample + Sample project for Azure Document DB Starter + https://github.com/Azure/azure-sdk-for-java + + + + com.microsoft.azure + azure-cosmosdb-spring-boot-starter + + + org.projectlombok + lombok + provided + + + + + ${project.basedir}/../.. + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/CosmosSampleApplication.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/CosmosSampleApplication.java new file mode 100644 index 000000000000..9e62398c0869 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/CosmosSampleApplication.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package sample.cosmosdb; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.annotation.PostConstruct; +import java.util.Optional; + +@SpringBootApplication +public class CosmosSampleApplication implements CommandLineRunner { + + private static final Logger LOGGER = LoggerFactory.getLogger(CosmosSampleApplication.class); + + @Autowired + private UserRepository repository; + + public static void main(String[] args) { + SpringApplication.run(CosmosSampleApplication.class, args); + } + + public void run(String... var1) throws Exception { + final User testUser = new User("testId", "testFirstName", "testLastName", "test address line one"); + + // Save the User class to Azure CosmosDB database. + final Mono saveUserMono = repository.save(testUser); + + final Flux firstNameUserFlux = repository.findByFirstName("testFirstName"); + + // Nothing happens until we subscribe to these Monos. + // findById will not return the user as user is not present. + final Mono findByIdMono = repository.findById(testUser.getId()); + final User findByIdUser = findByIdMono.block(); + Assert.isNull(findByIdUser, "User must be null"); + + final User savedUser = saveUserMono.block(); + Assert.state(savedUser != null, "Saved user must not be null"); + Assert.state(savedUser.getFirstName().equals(testUser.getFirstName()), "Saved user first name doesn't match"); + + firstNameUserFlux.collectList().block(); + + final Optional optionalUserResult = repository.findById(testUser.getId()).blockOptional(); + Assert.isTrue(optionalUserResult.isPresent(), "Cannot find user."); + + final User result = optionalUserResult.get(); + Assert.state(result.getFirstName().equals(testUser.getFirstName()), "query result firstName doesn't match!"); + Assert.state(result.getLastName().equals(testUser.getLastName()), "query result lastName doesn't match!"); + + LOGGER.info("findOne in User collection get result: {}", result.toString()); + } + + @PostConstruct + public void setup() { + // For this example, remove all of the existing records. + this.repository.deleteAll().block(); + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/User.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/User.java new file mode 100644 index 000000000000..eb33600eee13 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/User.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.cosmosdb; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; + +@Document(collection = "mycollection") +@NoArgsConstructor +@Getter +@Setter +@AllArgsConstructor +public class User { + @Id + private String id; + private String firstName; + @PartitionKey + private String lastName; + private String address; + + @Override + public String toString() { + return String.format("%s %s, %s", firstName, lastName, address); + } +} + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/UserRepository.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/UserRepository.java new file mode 100644 index 000000000000..e44a11860e1d --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb/src/main/java/sample/cosmosdb/UserRepository.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.cosmosdb; + +import com.microsoft.azure.spring.data.cosmosdb.repository.ReactiveCosmosRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +@Repository +public interface UserRepository extends ReactiveCosmosRepository { + + Flux findByFirstName(String firstName); +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/README-a.md b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/README-a.md new file mode 100644 index 000000000000..4a7166546f4f --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/README-a.md @@ -0,0 +1,10 @@ +## Overview +The sample of Spring Boot Starter `azure-spring-boot-starter-data-gremlin` please refer to +[How to use Spring Data Gremlin Starter](https://docs.microsoft.com/en-us/java/azure/spring-framework/configure-spring-data-gremlin-java-app-with-cosmos-db?view=azure-java-stable). + +##Notice +for this configuration item in application.properties +``` +//if your endpoint's protocol is HTTPS, the value will true, if the protocol is HTTP, the value will be false +gremlin.ssl-enabled=true/false +``` diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/pom.xml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/pom.xml new file mode 100644 index 000000000000..4c144622cb96 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + azure-spring-boot-samples + com.microsoft.azure + 0.0.1-beta.1 + ../pom.xml + + + spring-data-gremlin-boot-sample + jar + + Spring Data Gremlin Boot Starter Sample + Sample project for Spring Data Gremlin + https://github.com/Azure/azure-sdk-for-java + + + + com.azure + spring-data-gremlin-boot-starter + + + org.projectlombok + lombok + provided + + + + + ${project.basedir}/../.. + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/GremlinApplication.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/GremlinApplication.java new file mode 100644 index 000000000000..4367b083d3a1 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/GremlinApplication.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package sample.gremlin; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Optional; + +@Slf4j +@SpringBootApplication +public class GremlinApplication implements CommandLineRunner { + + @Autowired + private PersonRepository repository; + + public static void main(String[] args) { + SpringApplication.run(GremlinApplication.class, args); + } + + public void run(String... vars) { + final Person person = new Person("fake-id", "fake-name", 123); + + this.repository.deleteAll(); + this.repository.save(person); + + final Optional foundPerson = this.repository.findById(person.getId()); + Assert.isTrue(foundPerson.isPresent(), "optional of Person should be present"); + Assert.state(foundPerson.get().equals(person), "should be the equals"); + + log.info("Find users by id: {}", foundPerson.get().toString()); + + final List foundPersons = this.repository.findByNameAndLevel(person.getName(), person.getLevel()); + Assert.isTrue(foundPersons.size() == 1, "should be only one element"); + Assert.state(foundPersons.get(0).getId().equals(person.getId()), "should be the same id"); + + log.info("Find users by name and level: {}", foundPersons.get(0).toString()); + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/Person.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/Person.java new file mode 100644 index 000000000000..69e2044fefe0 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/Person.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package sample.gremlin; + +import com.microsoft.spring.data.gremlin.annotation.Vertex; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; + +@Vertex +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Person { + + @Id + private String id; + + private String name; + + private int level; +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/PersonRepository.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/PersonRepository.java new file mode 100644 index 000000000000..6a971fde5947 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-data-gremlin/src/main/java/sample/gremlin/PersonRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package sample.gremlin; + +import com.microsoft.spring.data.gremlin.repository.GremlinRepository; + +import java.util.List; + +public interface PersonRepository extends GremlinRepository { + + List findByNameAndLevel(String name, int level); +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/README-a.md b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/README-a.md new file mode 100644 index 000000000000..79a9a6ba69ff --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/README-a.md @@ -0,0 +1,118 @@ +# Azure Key Vault Secrets Spring Boot Starter Sample +This sample illustrates how to use [Azure Key Vault Secrets Spring Boot Starter](../../azure-spring-boot-starter-keyvault-secrets/README.md). + +In this sample, a secret named `spring-datasource-url` is stored into an Azure Key Vault, and a sample Spring application will use its value as a configuration property value. + +## Setup Azure Key Vault +First, we need to store secret `spring-datasource-url` into Azure Key Vault. + +- Create one azure service principal by using Azure CLI or via [Azure Portal](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal). Save your service principal id and password for later use. +You can use the following az cli commands to create a service principal: +```bash +az login +az account set --subscription + +# create azure service principal by azure cli +az ad sp create-for-rbac --name +# save the appId and password from output +``` +Save the service principal id and password contained in the output from above command. + +- Create Azure Key Vault by using Azure CLI or via [Azure Portal](https://portal.azure.com). You also need to grant appropriate permissions to the service principal created. +You can use the following az cli commands: +```bash +az keyvault create --name \ + --resource-group \ + --location \ + --enabled-for-deployment true \ + --enabled-for-disk-encryption true \ + --enabled-for-template-deployment true \ + --sku standard +az keyvault set-policy --name \ + --secret-permission get list \ + --spn +``` +> **IMPORTANT** +> +> The property `azure.keyvault.secret.keys` specifies which exact secrets the application will load from Key Vault. If this property is not set, which means the application will have to **list** all the secrets in Key Vault, you have to grant both **LIST** and **GET** secret permission to the service principal. Otherwise, only **GET** secret permission is needed. + +Save the displayed Key Vault uri for later use. + +- Set secret in Azure Key Vault by using Azure CLI or via Azure Portal. +You can use the following az cli commands: +```bash +az keyvault secret set --name spring-datasource-url \ + --value jdbc:mysql://localhost:3306/moviedb \ + --vault-name +az keyvault secret set --name \ + --value \ + --vault-name +``` + + +## Add Dependency + +"azure-keyvault-secrets-spring-boot-starter" is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +```xml + + com.microsoft.azure + azure-keyvault-secrets-spring-boot-starter + 2.1.7 + +``` + +## Add the property setting +Open `application.properties` file and add below properties to specify your Azure Key Vault url, Azure service principle client id and client key. + +```properties +azure.keyvault.uri=put-your-azure-keyvault-uri-here +azure.keyvault.client-id=put-your-azure-client-id-here +azure.keyvault.client-key=put-your-azure-client-key-here +azure.keyvault.tenant-id=put-your-azure-tenant-id-here + +# Uncomment following property if you want to specify the secrets to load from Key Vault +# azure.keyvault.secret.keys=yourSecretPropertyName1,yourSecretPropertyName2 +``` + +## Get Key Vault secret value as property +Now, you can use Azure Key Vault secret value as a configuration property. + +``` +@SpringBootApplication +public class SampleApplication implements CommandLineRunner { + + @Value("${yourSecretPropertyName}") + private String mySecretProperty; + + @Value("${spring.datasource.url}") + private String dbUrl; + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + + public void run(String... varl) throws Exception { + System.out.println("property yourSecretPropertyName value is: " + mySecretProperty); + System.out.println("property spring.datasource.url is: " + dbUrl); + } + +} +``` + + +## Run the sample + - Use Maven + + ``` + mvn package + java -jar target/azure-spring-boot-sample-keyvault-secrets-0.0.1-beta.1.jar + ``` + + - Use Gradle + + ``` + gradle bootRepackage + java -jar build/libs/azure-spring-boot-sample-keyvault-secrets-0.0.1-beta.1.jar + ``` diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/pom.xml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/pom.xml new file mode 100644 index 000000000000..ce81b66474a8 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + + com.microsoft.azure + azure-spring-boot-samples + 0.0.1-beta.1 + ../pom.xml + + + azure-keyvault-secrets-spring-boot-sample + jar + + Azure Key Vault Secrets Spring Boot Starter Sample + Sample project for Azure Key Vault Secrets Starter + https://github.com/Azure/azure-sdk-for-java + + + + com.microsoft.azure + azure-keyvault-secrets-spring-boot-starter + + + + + ${project.basedir}/../.. + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/provision.bat b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/provision.bat new file mode 100644 index 000000000000..87dbe09a504b --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/provision.bat @@ -0,0 +1,51 @@ +@echo off + +SET LOCATION=%1 +SET RESOURCEGROUPNAME=kvdemorg +SET APPNAME=kvdemoapp1 +SET VAULTNAME=%RESOURCEGROUPNAME%vault + +:createResourceGroup +FOR /F "tokens=*" %%A in ('az group exists -n %RESOURCEGROUPNAME%') DO SET "existingRG=%%A" +IF "%existingRG%"=="false" ( + ECHO Create resource group %RESOURCEGROUPNAME% + CALL az group create -n %RESOURCEGROUPNAME% -l %LOCATION% +) else ( + ECHO Resource group %RESOURCEGROUPNAME% exists already +) + + +:createServicePrinciple +FOR /F "tokens=*" %%A in ('az ad app list --display-name %APPNAME% --query [0].appId') DO SET "existingAPPID=%%A" +IF "%existingAPPID%"=="" ( + ECHO Create app %APPNAME% + FOR /F "tokens=*" %%C in ('az ad app create --display-name %APPNAME% --identifier-uris http://test.com/test --homepage http://test.com/test --query appId') DO SET "APPID=%%C" + + ECHO Create service principle + CALL az ad sp create --id %APPID% + SET OBJECTID=%APPID% +) else ( + SET OBJECTID=%existingAPPID% +) +ECHO clientId=%OBJECTID% +rem FOR /F "tokens=*" %%B in ('az ad sp reset-credentials --name %OBJECTID% --query password') DO SET "SPKEY=%%B" +ECHO clientKey=%SPKEY% + +:createKeyVault +ECHO Create Key Vault %VAULTNAME% +SET createkvcmd="az keyvault create --name %VAULTNAME% --resource-group %RESOURCEGROUPNAME% --location %LOCATION% --enabled-for-deployment true --enabled-for-disk-encryption true --enabled-for-template-deployment true --sku standard --query properties.vaultUri" +FOR /F "tokens=*" %%A in ('%createkvcmd%') DO SET "KEYVAULTURI=%%A" +ECHO vaultUri=https://%VAULTNAME%.vault.azure.net + +:setKeyVaultPolicy +ECHO Set keyvault policy +CALL az keyvault set-policy --name %VAULTNAME% --secret-permission set get list delete --object-id %OBJECTID% >> LOG.txt + + +:setSecret +ECHO Set secrets to key vault +CALL az keyvault secret set --vault-name %VAULTNAME% --name spring-datasource-url --value jdbc:mysql//localhost:3306/testdb >> LOG.txt +CALL az keyvault secret set --vault-name %VAULTNAME% --name mysecretproperty --value secretvalue >> LOG.txt + +:eof +ECHO Preparation done!! diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/src/main/java/sample/keyvault/SampleApplication.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/src/main/java/sample/keyvault/SampleApplication.java new file mode 100644 index 000000000000..71c6b8b29f32 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/src/main/java/sample/keyvault/SampleApplication.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.keyvault; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleApplication implements CommandLineRunner { + private static final Logger LOGGER = LoggerFactory.getLogger(SampleApplication.class); + + @Value("${yourSecretPropertyName}") + private String mySecretProperty; + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + + public void run(String... varl) throws Exception { + LOGGER.info("property yourSecretPropertyName in Azure Key Vault: {}", mySecretProperty); + + System.out.println("property yourSecretPropertyName in Azure Key Vault: " + mySecretProperty); + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/README-a.md b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/README-a.md new file mode 100644 index 000000000000..42007e772c10 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/README-a.md @@ -0,0 +1,47 @@ +## Overview +This sample project demonstrates how to use Azure Media Services via Spring Boot Starter `azure-spring-boot-starter-mediaservices`. + +## Prerequisites + +* An Azure subscription; if you don't already have an Azure subscription, you can activate your [MSDN subscriber benefits](https://azure.microsoft.com/en-us/pricing/member-offers/msdn-benefits-details/) or sign up for a [free Azure account](https://azure.microsoft.com/en-us/free/). + +* A [Java Development Kit (JDK)](http://www.oracle.com/technetwork/java/javase/downloads/), version 1.8. + +* [Apache Maven](http://maven.apache.org/), version 3.0 or later. + +## Quick Start + +### Create Azure Media Services on Azure + +1. Go to [Azure portal](https://portal.azure.com/) and create the service by following this [link](https://docs.microsoft.com/en-us/azure/media-services/media-services-portal-create-account). +2. Mark down the `TENANT`, `CLIENT ID` and `CLIENT SECRET` from this [link](https://docs.microsoft.com/en-us/azure/media-services/latest/stream-files-dotnet-quickstart#access-the-media-services-api), + `REST API ENDPOINT` from the service. + +### Config the sample + +1. Navigate to `src/main/resources` and open `application.properties`. +2. Fill in the `tenant`, `client-id`, `client-secret` and `rest-api-endpoint` with `TENANT`, `CLIENT ID`, `CLIENT SECRET` +and `REST API ENDPOINT` respectively. + +### Run the sample + +1. Change directory to folder `azure-spring-boot-sample-mediaservices`. +2. Run below commands. + + - Use Maven + + ``` + mvn package + java -jar target/azure-spring-boot-sample-mediaservices-0.0.1-beta.1.jar + ``` + + - Use Gradle + + ``` + gradle bootRepackage + java -jar build/libs/azure-spring-boot-sample-mediaservices-0.0.1-beta.1.jar + ``` + +## License + +The video file `video.mp4` under `src/main/resources` folder is from GitHub repository [big-buck-bunny-480p-5s](https://github.com/bower-media-samples/big-buck-bunny-480p-5s) without any modification and is under the [Creative Commons Attribution 3.0 license](http://creativecommons.org/licenses/by/3.0/). diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/pom.xml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/pom.xml new file mode 100644 index 000000000000..c8116e791888 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + + com.microsoft.azure + azure-spring-boot-samples + 0.0.1-beta.1 + ../pom.xml + + + azure-mediaservices-spring-boot-sample + jar + + Azure Media Services Spring Boot Starter Sample + Sample project for Azure Media Services Starter + https://github.com/Azure/azure-sdk-for-java + + + + com.microsoft.azure + azure-mediaservices-spring-boot-starter + + + + + ${project.basedir}/../.. + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/src/main/java/sample/mediaservices/MediaServicesSampleApplication.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/src/main/java/sample/mediaservices/MediaServicesSampleApplication.java new file mode 100644 index 000000000000..b844a0d0c992 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/src/main/java/sample/mediaservices/MediaServicesSampleApplication.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.mediaservices; + +import com.microsoft.windowsazure.exception.ServiceException; +import com.microsoft.windowsazure.services.media.MediaContract; +import com.microsoft.windowsazure.services.media.WritableBlobContainerContract; +import com.microsoft.windowsazure.services.media.models.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStream; +import java.util.EnumSet; + +@SpringBootApplication +public class MediaServicesSampleApplication implements CommandLineRunner { + + @Autowired + private MediaContract mediaService; + + public static void main(String[] args) { + SpringApplication.run(MediaServicesSampleApplication.class); + } + + // Note: Here is the minimum sample code that demonstrates how MediaContract is + // autowired and used. + // More information can be found at + // https://docs.microsoft.com/en-us/azure/media-services/media-services-java-how-to-use + @Override + public void run(String... args) throws Exception { + // Upload a local file to an Asset + final AssetInfo uploadAsset = uploadFileAndCreateAsset("video.mp4"); + System.out.println("Uploaded Asset Id: " + uploadAsset.getId()); + System.out.println("Sample completed!"); + } + + private AssetInfo uploadFileAndCreateAsset(String fileName) + throws ServiceException, IOException { + final WritableBlobContainerContract uploader; + final AssetInfo resultAsset; + final AccessPolicyInfo uploadAccessPolicy; + LocatorInfo uploadLocator = null; + + // Create an Asset + resultAsset = mediaService + .create(Asset.create().setName(fileName).setAlternateId("altId")); + System.out.println("Created Asset " + fileName); + + // Create an AccessPolicy that provides Write access for 15 minutes + uploadAccessPolicy = mediaService.create(AccessPolicy.create("uploadAccessPolicy", + 15.0, EnumSet.of(AccessPolicyPermission.WRITE))); + + // Create a Locator using the AccessPolicy and Asset + uploadLocator = mediaService.create(Locator.create(uploadAccessPolicy.getId(), + resultAsset.getId(), LocatorType.SAS)); + + // Create the Blob Writer using the Locator + uploader = mediaService.createBlobWriter(uploadLocator); + + // The local file that will be uploaded to your Media Services account + try (final InputStream input = new ClassPathResource(fileName).getInputStream()) { + System.out.println("Uploading " + fileName); + + // Upload the local file to the asset + uploader.createBlockBlob(fileName, input); + } + // Inform Media Services about the uploaded files + mediaService.action(AssetFile.createFileInfos(resultAsset.getId())); + System.out.println("Uploaded Asset File " + fileName); + + mediaService.delete(Locator.delete(uploadLocator.getId())); + mediaService.delete(AccessPolicy.delete(uploadAccessPolicy.getId())); + + return resultAsset; + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/src/main/resources/video.mp4 b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/src/main/resources/video.mp4 new file mode 100644 index 000000000000..7c9f92207228 Binary files /dev/null and b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-mediaservices/src/main/resources/video.mp4 differ diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/README-a.md b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/README-a.md new file mode 100644 index 000000000000..6ab2fffd1d78 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/README-a.md @@ -0,0 +1,57 @@ +## Overview + +This sample project demonstrates how to use Spring JMS for Azure Service Bus Queue via Spring Boot Starter `azure-spring-boot-starter-servicebus-jms`. + +Running this sample will be charged by Azure. You can check the usage and bill at this [link](https://azure.microsoft.com/en-us/account/). + +## Prerequisites + +* An Azure subscription. If you don't already have an Azure subscription, you can activate your [MSDN subscriber benefits](https://azure.microsoft.com/en-us/pricing/member-offers/msdn-benefits-details/) or sign up for a [free Azure account](https://azure.microsoft.com/en-us/free/). + +* A [Java Development Kit (JDK)](http://www.oracle.com/technetwork/java/javase/downloads/), version 1.8. + +* [Apache Maven](http://maven.apache.org/), version 3.0 or later. + +## Quick Start + +### Create Service Bus on Azure and Apply it's Properties + +1. Go to [Azure portal](https://portal.azure.com/) and create the service by following this [link](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-create-namespace-portal). + +2. Update [application.properties](src/main/resources/application.properties) + + ``` + # Fill service bus namespace connection string copied from portal + spring.jms.servicebus.connection-string=[servicebus-namespace-connection-string] + + # The idle timeout in milliseconds after which the connection will be failed if the peer sends no AMQP frames + # Default is 1800000 + spring.jms.servicebus.idle-timeout=[idle-timeout] + ``` + +3. Specify your queue name. Update `QUEUE_NAME` in [QueueSendController](src/main/java/sample/jms/queue/QueueSendController.java) and [QueueReceiveController](src/main/java/sample/jms/queue/QueueReceiveController.java). + +### How to run + +1. Run the `mvn clean spring-boot:run` in the root of the code sample to get the app running. + +2. Send a POST request to service bus queue. + ``` + $ curl -X POST localhost:8080/queue?message=hello + ``` + +3. Verify in your app's logs that a similar message was posted: + ``` + Sending message + Received message from queue: hello + ``` + +4. Delete the resources on [Azure Portal](http://ms.portal.azure.com/) to avoid extra charges. + +### More usage + +Please check the following table for reference links of detailed Service Bus usage. + +Type | Reference Link +--- | --- +`Queues` | [https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-queues](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-queues) diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/pom.xml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/pom.xml new file mode 100644 index 000000000000..119d090080e3 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + com.microsoft.azure + azure-spring-boot-samples + 0.0.1-beta.1 + ../pom.xml + + + azure-servicebus-jms-queue-spring-boot-sample + jar + + Azure Service Bus JMS Queue Spring Boot Starter Sample + Sample project for Spring Boot Service Bus JMS Starter + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter-web + + + + com.microsoft.azure + azure-servicebus-jms-spring-boot-starter + + + + junit + junit + 4.12 + test + + + org.springframework.boot + spring-boot-test-autoconfigure + test + + + org.springframework + spring-test + 5.1.9.RELEASE + test + + + org.assertj + assertj-core + 3.11.1 + test + + + + + + ${project.basedir}/../.. + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/QueueReceiveController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/QueueReceiveController.java new file mode 100644 index 000000000000..bf8a7e3da7af --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/QueueReceiveController.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.queue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class QueueReceiveController { + + private static final String QUEUE_NAME = "que001"; + + private final Logger logger = LoggerFactory.getLogger(QueueReceiveController.class); + + @JmsListener(destination = QUEUE_NAME, containerFactory = "jmsListenerContainerFactory") + public void receiveMessage(User user) { + + logger.info("Received message from queue: {}", user.getName()); + + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/QueueSendController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/QueueSendController.java new file mode 100644 index 000000000000..e44c10beff55 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/QueueSendController.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.queue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class QueueSendController { + + private static final String QUEUE_NAME = "que001"; + + private static final Logger logger = LoggerFactory.getLogger(QueueSendController.class); + + @Autowired + private JmsTemplate jmsTemplate; + + @PostMapping("/queue") + public String postMessage(@RequestParam String message) { + + logger.info("Sending message"); + + jmsTemplate.convertAndSend(QUEUE_NAME, new User(message)); + + return message; + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/ServiceBusJMSQueueApplication.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/ServiceBusJMSQueueApplication.java new file mode 100644 index 000000000000..8be171bf01ce --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/ServiceBusJMSQueueApplication.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.queue; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jms.annotation.EnableJms; + +@SpringBootApplication +@EnableJms +public class ServiceBusJMSQueueApplication { + + public static void main(String[] args) { + + SpringApplication.run(ServiceBusJMSQueueApplication.class, args); + + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/User.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/User.java new file mode 100644 index 000000000000..af2683f92f7c --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/java/sample/jms/queue/User.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.queue; + +import java.io.Serializable; + +public class User implements Serializable { + + private static final long serialVersionUID = -295422703255886286L; + private String name; + + User(String name) { + setName(name); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/resources/log4j.properties b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/resources/log4j.properties new file mode 100644 index 000000000000..0e61ac79ee3a --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/main/resources/log4j.properties @@ -0,0 +1,7 @@ +log4j.rootLogger=ERROR, stdout +log4j.logger.org.apache.qpid.jms=ERROR + +# CONSOLE appender +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/test/java/sample/jms/queue/ServiceBusJMSQueueApplicationIT.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/test/java/sample/jms/queue/ServiceBusJMSQueueApplicationIT.java new file mode 100644 index 000000000000..86e86f4a4f5c --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/test/java/sample/jms/queue/ServiceBusJMSQueueApplicationIT.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.queue; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ServiceBusJMSQueueApplication.class) +@AutoConfigureMockMvc +@TestPropertySource(locations = "classpath:application-test.properties") +public class ServiceBusJMSQueueApplicationIT { + + @Autowired + private MockMvc mvc; + + @Rule + public OutputCapture capture = new OutputCapture(); + + @Test + public void testQueueSendAndReceiveMessage() throws Exception { + final String message = UUID.randomUUID().toString(); + + mvc.perform(post("/queue?message=" + message)).andExpect(status().isOk()) + .andExpect(content().string(message)); + + final String messageReceivedLog = String.format("Received message from queue: %s", message); + + boolean messageReceived = false; + + for (int i = 0; i < 100; i++) { + final String output = capture.toString(); + if (!messageReceived && output.contains(messageReceivedLog)) { + messageReceived = true; + } + + if (messageReceived) { + break; + } + + Thread.sleep(1000); + } + + assertThat(messageReceived).isTrue(); + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/test/resources/application-test.properties b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/test/resources/application-test.properties new file mode 100644 index 000000000000..cc7397b82146 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-queue/src/test/resources/application-test.properties @@ -0,0 +1,3 @@ +spring.jms.servicebus.connection-string=[servicebus-namespace-connection-string] + +spring.jms.servicebus.idle-timeout=[idle-timeout] diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/README-a.md b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/README-a.md new file mode 100644 index 000000000000..349f49d9bb38 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/README-a.md @@ -0,0 +1,62 @@ +## Overview + +This sample project demonstrates how to use Spring JMS Topic for Azure Service Bus via Spring Boot Starter `azure-spring-boot-starter-servicebus-jms`. + +Running this sample will be charged by Azure. You can check the usage and bill at this [link](https://azure.microsoft.com/en-us/account/). + +## Prerequisites + +* An Azure subscription. If you don't already have an Azure subscription, you can activate your [MSDN subscriber benefits](https://azure.microsoft.com/en-us/pricing/member-offers/msdn-benefits-details/) or sign up for a [free Azure account](https://azure.microsoft.com/en-us/free/). + +* A [Java Development Kit (JDK)](http://www.oracle.com/technetwork/java/javase/downloads/), version 1.8. + +* [Apache Maven](http://maven.apache.org/), version 3.0 or later. + +## Quick Start + +### Create Service Bus on Azure and Apply it's Properties + +1. Go to [Azure portal](https://portal.azure.com/) and create the service by following this [link](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-create-namespace-portal). + +2. Update [application.properties](src/main/resources/application.properties) + + ``` + # Fill service bus namespace connection string copied from portal + spring.jms.servicebus.connection-string=[servicebus-namespace-connection-string] + + # The JMS client id needs to be specified when using topic and durable subscription + # Default is empty string + spring.jms.servicebus.topic-client-id=[topic-client-id] + + # The idle timeout in milliseconds after which the connection will be failed if the peer sends no AMQP frames + # Default is 1800000 + spring.jms.servicebus.idle-timeout=[idle-timeout] + ``` + +3. Specify your topic name and subscription name. Update `TOPIC_NAME` in [TopicSendController](src/main/java/sample/jms/topic/TopicSendController.java) and [TopicReceiveController](src/main/java/sample/jms/topic/TopicReceiveController.java), and `SUBSCRIPTION_NAME` in [TopicReceiveController](src/main/java/sample/jms/topic/TopicReceiveController.java). + +### How to run + +1. Run the `mvn clean spring-boot:run` in the root of the code sample to get the app running. + +2. Send a POST request to service bus topic. + ``` + $ curl -X POST localhost:8080/topic?message=hello + ``` + +3. Verify in your app's logs that a similar message was posted: + ``` + Sending message + Received message from topic: hello + ``` + +4. Delete the resources on [Azure Portal](http://ms.portal.azure.com/) to avoid extra charges. + +### More usage + +Please check the following table for reference links of detailed Service Bus usage. + +Type | Reference Link +--- | --- +`Topics` | [https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-topics-subscriptions](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-topics-subscriptions) +`Subscriptions` | [https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-topics-subscriptions](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-topics-subscriptions) diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/pom.xml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/pom.xml new file mode 100644 index 000000000000..a5cd1e285a1e --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + com.microsoft.azure + azure-spring-boot-samples + 0.0.1-beta.1 + ../pom.xml + + + azure-servicebus-jms-topic-spring-boot-sample + jar + + Azure Service Bus JMS Topic Spring Boot Starter Sample + Sample project for Spring Boot Service Bus JMS Starter + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter-web + + + + com.microsoft.azure + azure-servicebus-jms-spring-boot-starter + + + + junit + junit + 4.12 + test + + + org.springframework.boot + spring-boot-test-autoconfigure + test + + + org.springframework + spring-test + 5.1.9.RELEASE + test + + + org.assertj + assertj-core + 3.11.1 + test + + + + + + ${project.basedir}/../.. + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/ServiceBusJMSTopicApplication.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/ServiceBusJMSTopicApplication.java new file mode 100644 index 000000000000..72a7ba4ebf69 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/ServiceBusJMSTopicApplication.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.topic; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jms.annotation.EnableJms; + +@SpringBootApplication +@EnableJms +public class ServiceBusJMSTopicApplication { + + public static void main(String[] args) { + + SpringApplication.run(ServiceBusJMSTopicApplication.class, args); + + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/TopicReceiveController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/TopicReceiveController.java new file mode 100644 index 000000000000..8e67e303f419 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/TopicReceiveController.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.topic; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class TopicReceiveController { + + private static final String TOPIC_NAME = "tpc001"; + + private static final String SUBSCRIPTION_NAME = "sub001"; + + private final Logger logger = LoggerFactory.getLogger(TopicReceiveController.class); + + @JmsListener(destination = TOPIC_NAME, containerFactory = "topicJmsListenerContainerFactory", + subscription = SUBSCRIPTION_NAME) + public void receiveMessage(User user) { + + logger.info("Received message from topic: {}", user.getName()); + + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/TopicSendController.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/TopicSendController.java new file mode 100644 index 000000000000..a31a25b94821 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/TopicSendController.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.topic; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TopicSendController { + + private static final String TOPIC_NAME = "tpc001"; + + private static final Logger logger = LoggerFactory.getLogger(TopicSendController.class); + + @Autowired + private JmsTemplate jmsTemplate; + + @PostMapping("/topic") + public String postMessage(@RequestParam String message) { + + logger.info("Sending message"); + + jmsTemplate.convertAndSend(TOPIC_NAME, new User(message)); + + return message; + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/User.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/User.java new file mode 100644 index 000000000000..1c58cdd1d211 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/java/sample/jms/topic/User.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.topic; + +import java.io.Serializable; + +public class User implements Serializable { + + private static final long serialVersionUID = -295422703255886286L; + private String name; + + User(String name) { + setName(name); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/resources/log4j.properties b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/resources/log4j.properties new file mode 100644 index 000000000000..0e61ac79ee3a --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/main/resources/log4j.properties @@ -0,0 +1,7 @@ +log4j.rootLogger=ERROR, stdout +log4j.logger.org.apache.qpid.jms=ERROR + +# CONSOLE appender +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d [%-15.15t] - %-5p %-30.30c{1} - %m%n diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/test/java/sample/jms/topic/ServiceBusJMSTopicApplicationIT.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/test/java/sample/jms/topic/ServiceBusJMSTopicApplicationIT.java new file mode 100644 index 000000000000..27873b4a26e6 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/test/java/sample/jms/topic/ServiceBusJMSTopicApplicationIT.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.jms.topic; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ServiceBusJMSTopicApplication.class) +@AutoConfigureMockMvc +@TestPropertySource(locations = "classpath:application-test.properties") +public class ServiceBusJMSTopicApplicationIT { + + @Autowired + private MockMvc mvc; + + @Rule + public OutputCapture capture = new OutputCapture(); + + @Test + public void testTopicSendAndReceiveMessage() throws Exception { + final String message = UUID.randomUUID().toString(); + + mvc.perform(post("/topic?message=" + message)).andExpect(status().isOk()) + .andExpect(content().string(message)); + + final String messageReceivedLog = String.format("Received message from topic: %s", message); + + boolean messageReceived = false; + + for (int i = 0; i < 100; i++) { + final String output = capture.toString(); + if (!messageReceived && output.contains(messageReceivedLog)) { + messageReceived = true; + } + + if (messageReceived) { + break; + } + + Thread.sleep(1000); + } + + assertThat(messageReceived).isTrue(); + } + +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/test/resources/application-test.properties b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/test/resources/application-test.properties new file mode 100644 index 000000000000..078746ab1fd7 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus-jms-topic/src/test/resources/application-test.properties @@ -0,0 +1,5 @@ +spring.jms.servicebus.connection-string=[servicebus-namespace-connection-string] + +spring.jms.servicebus.topic-client-id=[topic-client-id] + +spring.jms.servicebus.idle-timeout=[idle-timeout] diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/README-a.md b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/README-a.md new file mode 100644 index 000000000000..96a55dd0abe3 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/README-a.md @@ -0,0 +1,53 @@ +## Overview +This sample project demonstrates how to use Service Bus via Spring Boot Starter `azure-spring-boot-starter-servicebus`. + +## Prerequisites + +* An Azure subscription; if you don't already have an Azure subscription, you can activate your [MSDN subscriber benefits](https://azure.microsoft.com/en-us/pricing/member-offers/msdn-benefits-details/) or sign up for a [free Azure account](https://azure.microsoft.com/en-us/free/). + +* A [Java Development Kit (JDK)](http://www.oracle.com/technetwork/java/javase/downloads/), version 1.8. + +* [Apache Maven](http://maven.apache.org/), version 3.0 or later. + +## Quick Start + +### Create Service Bus on Azure + +1. Go to [Azure portal](https://portal.azure.com/) and create the service by following this [link](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-create-namespace-portal). +2. Mark down the `Primary Connection String`. +3. In the `Overview` blade, create queue and topic. Mark down your queue name and topic name. +4. Click your created topic, add subscription in the `Subscriptions` blade. Mark down your subscription name. + +### Config the sample + +1. Navigate to `src/main/resources` and open `application.properties`. +2. Fill in the `connection-string`, `queue-name`,`topic-name` and `subscription-name`. + +### Run the sample + +1. Change directory to folder `azure-spring-boot-sample-servicebus`. +2. Run below commands. + + - Use Maven + + ``` + mvn package + java -jar target/azure-spring-boot-sample-servicebus-0.0.1-beta.1.jar + ``` + + - Use Gradle + + ``` + gradle bootJar + java -jar build/libs/azure-spring-boot-sample-servicebus-0.0.1-beta.1.jar + ``` + +### More usage + +Please check the following table for reference links of detailed Service Bus usage. + +Type | Reference Link +--- | --- +`Queues` | [https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-queues](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-queues) +`Topics` | [https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-topics-subscriptions](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-topics-subscriptions) +`Subscriptions` | [https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-topics-subscriptions](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-java-how-to-use-topics-subscriptions) diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/pom.xml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/pom.xml new file mode 100644 index 000000000000..c6328fa2c520 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + + com.microsoft.azure + azure-spring-boot-samples + 0.0.1-beta.1 + ../pom.xml + + + azure-servicebus-spring-boot-sample + jar + + Azure Service Bus Spring Boot Starter Sample + Sample project for Spring Boot Service Bus Starter + https://github.com/Azure/azure-sdk-for-java + + + + com.microsoft.azure + azure-servicebus-spring-boot-starter + + + + + ${project.basedir}/../.. + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/src/main/java/sample/servicebus/ServiceBusSampleApplication.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/src/main/java/sample/servicebus/ServiceBusSampleApplication.java new file mode 100644 index 000000000000..ecf08e7feb10 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-servicebus/src/main/java/sample/servicebus/ServiceBusSampleApplication.java @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package sample.servicebus; + +import com.microsoft.azure.servicebus.*; +import com.microsoft.azure.servicebus.primitives.ServiceBusException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@SpringBootApplication +public class ServiceBusSampleApplication implements CommandLineRunner { + + @Autowired + private QueueClient queueClient; + @Autowired + private TopicClient topicClient; + @Autowired + private SubscriptionClient subscriptionClient; + + public static void main(String[] args) { + SpringApplication.run(ServiceBusSampleApplication.class); + } + + public void run(String... var1) throws ServiceBusException, InterruptedException { + sendQueueMessage(); + receiveQueueMessage(); + + sendTopicMessage(); + receiveSubscriptionMessage(); + } + + // NOTE: Please be noted that below are the minimum code for demonstrating the usage of autowired clients. + // For complete documentation of Service Bus, reference https://azure.microsoft.com/en-us/services/service-bus/ + private void sendQueueMessage() throws ServiceBusException, InterruptedException { + final String messageBody = "queue message"; + System.out.println("Sending message: " + messageBody); + final Message message = new Message(messageBody.getBytes(StandardCharsets.UTF_8)); + queueClient.send(message); + } + + private void receiveQueueMessage() throws ServiceBusException, InterruptedException { + queueClient.registerMessageHandler(new MessageHandler(), new MessageHandlerOptions()); + + TimeUnit.SECONDS.sleep(5); + queueClient.close(); + } + + private void sendTopicMessage() throws ServiceBusException, InterruptedException { + final String messageBody = "topic message"; + System.out.println("Sending message: " + messageBody); + final Message message = new Message(messageBody.getBytes(StandardCharsets.UTF_8)); + topicClient.send(message); + topicClient.close(); + } + + private void receiveSubscriptionMessage() throws ServiceBusException, InterruptedException { + subscriptionClient.registerMessageHandler(new MessageHandler(), new MessageHandlerOptions()); + + TimeUnit.SECONDS.sleep(5); + subscriptionClient.close(); + } + + static class MessageHandler implements IMessageHandler { + public CompletableFuture onMessageAsync(IMessage message) { + final String messageString = new String(message.getBody(), StandardCharsets.UTF_8); + System.out.println("Received message: " + messageString); + return CompletableFuture.completedFuture(null); + } + + public void notifyException(Throwable exception, ExceptionPhase phase) { + System.out.println(phase + " encountered exception:" + exception.getMessage()); + } + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/README-a.md b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/README-a.md new file mode 100644 index 000000000000..b7588b5c369d --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/README-a.md @@ -0,0 +1,107 @@ +## Overview +This sample project demonstrates how to use Azure Storage via Spring Boot Starter `azure-spring-boot-starter-storage`. + +## Prerequisites + +* An Azure subscription; if you don't already have an Azure subscription, you can activate your [MSDN subscriber benefits](https://azure.microsoft.com/en-us/pricing/member-offers/msdn-benefits-details/) or sign up for a [free Azure account](https://azure.microsoft.com/en-us/free/). + +* A [Java Development Kit (JDK)](http://www.oracle.com/technetwork/java/javase/downloads/), version 1.8. + +* [Apache Maven](http://maven.apache.org/), version 3.0 or later. + +## Quick Start + +### Create storage account on Azure + +1. Go to [Azure portal](https://portal.azure.com/) and create the account by following this [link](https://docs.microsoft.com/en-us/azure/storage/storage-create-storage-account). +2. In the `Access keys` blade, mark down the `CONNECTION STRING`. + +### Config the sample + +1. Navigate to `src/main/resources` and open `application.properties`. +2. Fill in the `connection-string`. + +### Run the sample + +1. Change directory to folder `azure-spring-boot-sample-storage`. +2. Run below commands. + + - Use Maven + + ``` + mvn package + java -jar target/azure-spring-boot-sample-storage-0.0.1-beta.1.jar + ``` + + - Use Gradle + + ``` + gradle bootRepackage + java -jar build/libs/azure-spring-boot-sample-storage-0.0.1-beta.1.jar + ``` + +## Sample usage + +### List the blobs in a container + +1. Navigate to `src/main/java/com/microsoft/azure` and open `StorageSampleApplication.java` +2. Add below method + +```java +private void listBlobInContainer(String containerName) throws StorageException, URISyntaxException { + + // Create the blob client. + final CloudBlobClient blobClient = cloudStorageAccount.createCloudBlobClient(); + + // Retrieve reference to a previously created container. + final CloudBlobContainer container = blobClient.getContainerReference(containerName); + + // Loop over blobs within the container and output the URI to each of them. + for (final ListBlobItem blobItem : container.listBlobs()) { + System.out.println(blobItem.getUri()); + } + } +``` + +3. Import `ListBlobItem` + +``` +import com.microsoft.azure.storage.blob.ListBlobItem; +``` + +4. Update `run` method and save + +```java +public void run(String... var1) throws URISyntaxException, StorageException, IOException { + createContainerIfNotExists("mycontainer"); + + // List the blobs in a container + listBlobInContainer("mycontainer"); + } +``` + +5. Run below commands. + +``` +mvn package +java -jar target/azure-spring-boot-sample-storage-0.0.1-beta.1.jar +``` + +### More usage + +Please check the following table for reference links of detailed Storage usage. + +Storage Type | Reference Link +--- | --- +`Blob Storage` | [https://docs.microsoft.com/en-us/azure/storage/storage-java-how-to-use-blob-storage](https://docs.microsoft.com/en-us/azure/storage/storage-java-how-to-use-blob-storage) +`Queue Storage` | [https://docs.microsoft.com/en-us/azure/storage/storage-java-how-to-use-queue-storage](https://docs.microsoft.com/en-us/azure/storage/storage-java-how-to-use-queue-storage) +`Table Storage` | [https://docs.microsoft.com/en-us/azure/storage/storage-java-how-to-use-table-storage](https://docs.microsoft.com/en-us/azure/storage/storage-java-how-to-use-table-storage) +`File Storage` | [https://docs.microsoft.com/en-us/azure/storage/storage-java-how-to-use-file-storage](https://docs.microsoft.com/en-us/azure/storage/storage-java-how-to-use-file-storage) + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/pom.xml b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/pom.xml new file mode 100644 index 000000000000..32577495acab --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + com.microsoft.azure + azure-spring-boot-samples + 0.0.1-beta.1 + ../pom.xml + + + azure-storage-spring-boot-sample + jar + + Azure Storage Spring Boot Starter Sample + Sample project for Azure Storage Starter + https://github.com/Azure/azure-sdk-for-java + + + + com.azure + azure-storage-spring-boot-starter + + + com.google.code.findbugs + annotations + 3.0.1 + + + + + ${project.basedir}/../.. + + + diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/java/sample/storage/StorageSampleApplication.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/java/sample/storage/StorageSampleApplication.java new file mode 100644 index 000000000000..f209c45d80f7 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/java/sample/storage/StorageSampleApplication.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package sample.storage; + +import com.microsoft.azure.spring.autoconfigure.storage.StorageProperties; +import com.microsoft.azure.storage.blob.BlockBlobURL; +import com.microsoft.azure.storage.blob.ContainerURL; +import com.microsoft.azure.storage.blob.ServiceURL; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.io.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +@SuppressFBWarnings({"RV_RETURN_VALUE_IGNORED"}) +@SpringBootApplication +public class StorageSampleApplication implements CommandLineRunner { + private static final String SOURCE_FILE = "storageTestFile.txt"; + + @Autowired + private ServiceURL serviceURL; + + @Autowired + private ContainerURL containerURL; + + @Autowired + private StorageProperties properties; + + public static void main(String[] args) { + SpringApplication.run(StorageSampleApplication.class); + } + + public void run(String... var1) throws IOException { + final File sourceFile = new File(this.getClass().getClassLoader().getResource(SOURCE_FILE).getFile()); + final File downloadFile = Files.createTempFile("azure-storage-test", null).toFile(); + + StorageService.createContainer(containerURL, properties.getContainerName()); + final BlockBlobURL blockBlobURL = containerURL.createBlockBlobURL(SOURCE_FILE); + + System.out.println("Enter a command:"); + System.out.println("(P)utBlob | (G)etBlob | (D)eleteBlobs | (E)xitSample"); + final BufferedReader reader = + new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8.name())); + + boolean isExit = false; + while (!isExit) { + System.out.println("Enter a command:"); + final String input = reader.readLine(); + if (input == null) { + continue; + } + + switch(input) { + case "P": + StorageService.uploadFile(blockBlobURL, sourceFile); + break; + case "G": + StorageService.downloadBlob(blockBlobURL, downloadFile); + break; + case "D": + StorageService.deleteBlob(blockBlobURL); + break; + case "E": + System.out.println("Cleaning up container and tmp file..."); + containerURL.delete(null, null).blockingGet(); + FileUtils.deleteQuietly(downloadFile); + isExit = true; + break; + default: + break; + } + } + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/java/sample/storage/StorageService.java b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/java/sample/storage/StorageService.java new file mode 100644 index 000000000000..37e39a357944 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/java/sample/storage/StorageService.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package sample.storage; + +import com.microsoft.azure.storage.blob.BlobRange; +import com.microsoft.azure.storage.blob.BlockBlobURL; +import com.microsoft.azure.storage.blob.ContainerURL; +import com.microsoft.azure.storage.blob.TransferManager; +import com.microsoft.azure.storage.blob.models.ContainerCreateResponse; +import com.microsoft.rest.v2.RestException; +import com.microsoft.rest.v2.util.FlowableUtil; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +@SuppressFBWarnings({"RV_RETURN_VALUE_IGNORED"}) +public class StorageService { + public static void uploadFile(BlockBlobURL blob, File sourceFile) throws IOException { + logInfo("Start uploading file %s...", sourceFile); + final AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(sourceFile.toPath()); + + TransferManager.uploadFileToBlockBlob(fileChannel, blob, 8 * 1024 * 1024, null) + .toCompletable() + .doOnComplete(() -> logInfo("File %s is uploaded.", sourceFile.toPath())) + .doOnError(error -> logError("Failed to upload file %s with error %s.", sourceFile.toPath(), + error.getMessage())) + .blockingAwait(); + } + + public static void deleteBlob(BlockBlobURL blockBlobURL) { + logInfo("Start deleting file %s...", blockBlobURL.toURL()); + blockBlobURL.delete(null, null, null) + .toCompletable() + .doOnComplete(() -> logInfo("Blob %s is deleted.", blockBlobURL.toURL())) + .doOnError(error -> logError("Failed to delete blob %s with error %s.", + blockBlobURL.toURL(), error.getMessage())) + .blockingAwait(); + } + + public static void downloadBlob(BlockBlobURL blockBlobURL, File downloadToFile) { + logInfo("Start downloading file %s to %s...", blockBlobURL.toURL(), downloadToFile); + FileUtils.deleteQuietly(downloadToFile); + + blockBlobURL.download(new BlobRange().withOffset(0).withCount(4 * 1024 * 1024L), null, false, null) + .flatMapCompletable( + response -> { + final AsynchronousFileChannel channel = AsynchronousFileChannel + .open(Paths.get(downloadToFile.getAbsolutePath()), StandardOpenOption.CREATE, + StandardOpenOption.WRITE); + return FlowableUtil.writeFile(response.body(null), channel); + }) + .doOnComplete(() -> logInfo("File is downloaded to %s.", downloadToFile)) + .doOnError(error -> logError("Failed to download file from blob %s with error %s.", + blockBlobURL.toURL(), error.getMessage())) + .blockingAwait(); + } + + public static void createContainer(ContainerURL containerURL, String containerName) { + logInfo("Start creating container %s...", containerName); + try { + final ContainerCreateResponse response = containerURL.create(null, null, null).blockingGet(); + logInfo("Storage container %s created with status code: %s.", containerName, response.statusCode()); + } catch (RestException e) { + if (e.response().statusCode() != 409) { + logError("Failed to create container %s.", containerName, e); + throw e; + } else { + logInfo("%s container already exists.", containerName); + } + } + } + + private static void logInfo(String log, Object... params) { + System.out.println(String.format(log, params)); + } + + private static void logError(String log, Object... params) { + System.err.println(String.format(log, params)); + } +} diff --git a/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/resources/storageTestFile.txt b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/resources/storageTestFile.txt new file mode 100644 index 000000000000..880293fc7b75 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/azure-spring-boot-sample-storage/src/main/resources/storageTestFile.txt @@ -0,0 +1 @@ +This is a sample file to test Azure storage. \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-samples/pom.xml b/sdk/spring/azure-spring-boot-samples/pom.xml new file mode 100644 index 000000000000..9cff69408069 --- /dev/null +++ b/sdk/spring/azure-spring-boot-samples/pom.xml @@ -0,0 +1,152 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.2.0.RELEASE + + + + com.microsoft.azure + azure-spring-boot-samples + pom + 1.0.0 + + Azure Spring Boot Samples + Samples for Azure Spring Boot + https://github.com/Azure/azure-sdk-for-java + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + + microsoft + Microsoft Corporation + + + + + scm:git:git://github.com/azure/azure-sdk-for-java + scm:git:git://github.com/azure/azure-sdk-for-java + https://github.com/azure/azure-sdk-for-java + + + + GitHub + https://github.com/azure/azure-sdk-for-java/issues + + + + 1.8 + ${project.basedir}/.. + UTF-8 + UTF-8 + + + + + + com.microsoft.azure + azure-spring-boot-bom + 2.2.5-beta.1 + pom + import + + + commons-io + commons-io + 2.3 + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.17 + + + validate + validate + + ${project.rootdir}/config/checkstyle.xml + UTF-8 + true + true + true + true + + + check + + + + + false + + true + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.5 + + Max + Low + true + ${project.build.directory}/findbugs + + ${project.rootdir}/config/findbugs-exclude.xml + + + + org.apache.ant + ant + 1.9.4 + + + + + compile + + check + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + azure-spring-boot-sample-active-directory + azure-spring-boot-sample-active-directory-b2c-oidc + azure-spring-boot-sample-active-directory-backend + azure-spring-boot-sample-active-directory-backend-v2 + azure-spring-boot-sample-active-directory-stateless + azure-spring-boot-sample-cosmosdb + azure-spring-boot-sample-data-gremlin + azure-spring-boot-sample-keyvault-secrets + azure-spring-boot-sample-mediaservices + azure-spring-boot-sample-servicebus + azure-spring-boot-sample-servicebus-jms-queue + azure-spring-boot-sample-servicebus-jms-topic + azure-spring-boot-sample-storage + + + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory-b2c/README.md b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/README.md new file mode 100644 index 000000000000..8f0873bab62c --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/README.md @@ -0,0 +1,238 @@ +# Azure AAD B2C Spring Boot Starter client library for Java +## Overview + +Azure Active Directory (Azure AD) B2C is an identity management service that enables you to customize and control how +customers sign up, sign in, and manage their profiles when using your applications. Azure AD B2C enables these actions +while protecting the identities of your customers at the same time. + +## Prerequisites + +The following prerequisites are required in order to complete the steps in this article: + +* A supported Java Development Kit (JDK). For more information about the JDKs available for use when developing on Azure, see . +* [Apache Maven](http://maven.apache.org/), version 3.0 or later. +* Azure subscription. + +If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) before you begin. + +## Getting started + +### Create the Active Directory instance + +1. Log into . + +2. Click **+Create a resource**, then **Identity**, and then **Azure Active Directory B2C**. + +3. Enter your **Organization name** and your **Initial domain name**, record the **domain name** as your +`${your-tenant-name}` and click **Create**. + +4. Select your account name on the top-right of the Azure portal toolbar, then click **Switch directory**. + +5. Select your new Azure Active Directory from the drop-down menu. + +6. Search `b2c` and click `Azure AD B2C` service. + +### Add an application registration for your Spring Boot app + +1. Select **Azure AD B2C** from the portal menu, click **Applications**, and then click **Add**. + +2. Specify your application **Name**, add `http://localhost:8080/home` for the **Reply URL**, record the +**Application ID** as your `${your-client-id}` and then click **Save**. + +3. Select **Keys** from your application, click **Generate key** to generate `${your-client-secret}` and then **Save**. + +4. Select **User flows** on your left, and then **Click** **New user flow **. + +5. Choose **Sign up or in**, **Profile editing** and **Password reset** to create user flows +respectively. Specify your user flow **Name** and **User attributes and claims**, click **Create**. + +## Examples +### Configure and compile your app + +1. Extract the files from the project archive you created and downloaded earlier in this tutorial into a directory. + +2. Navigate to the parent folder for your project, and open the `pom.xml` Maven project file in a text editor. + +3. Add the dependencies for Spring OAuth2 security to the `pom.xml`: + + ```xml + + com.azure + azure-spring-boot-starter-active-directory-b2c + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity5 + + ``` + +4. Save and close the *pom.xml* file. + +5. Navigate to the *src/main/resources* folder in your project and open the *application.yml* file in a text editor. + +6. Specify the settings for your app registration using the values you created earlier; for example: + + ```yaml + azure: + activedirectory: + b2c: + tenant: ${your-tenant-name} + client-id: ${your-client-id} + client-secret: ${your-client-secret} + reply-url: ${your-reply-url-from-aad} # should be absolute url. + logout-success-url: ${you-logout-success-url} + user-flows: + sign-up-or-sign-in: ${your-sign-up-or-in-user-flow} + profile-edit: ${your-profile-edit-user-flow} # optional + password-reset: ${your-password-reset-user-flow} # optional + ``` + Where: + + | Parameter | Description | + |---|---| + | `azure.activedirectory.b2c.tenant` | Contains your AD B2C's `${your-tenant-name` from earlier. | + | `azure.activedirectory.b2c.client-id` | Contains the `${your-client-id}` from your application that you completed earlier. | + | `azure.activedirectory.b2c.client-secret` | Contains the `${your-client-secret}` from your application that you completed earlier. | + | `azure.activedirectory.b2c.reply-url` | Contains one of the **Reply URL** from your application that you completed earlier. | + | `azure.activedirectory.b2c.logout-success-url` | Specify the URL when your application logout successfully. | + | `azure.activedirectory.b2c.user-flows` | Contains the name of the user flows that you completed earlier. + +7. Save and close the *application.yml* file. + +8. Create a folder named *controller* in the Java source folder for your application. + +9. Create a new Java file named *HelloController.java* in the *controller* folder and open it in a text editor. + +10. Enter the following code, then save and close the file: + + ```java + package sample.aad.controller; + + import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; + import org.springframework.security.oauth2.core.user.OAuth2User; + import org.springframework.stereotype.Controller; + import org.springframework.ui.Model; + import org.springframework.web.bind.annotation.GetMapping; + + @Controller + public class WebController { + + private void initializeModel(Model model, OAuth2AuthenticationToken token) { + if (token != null) { + final OAuth2User user = token.getPrincipal(); + + model.addAttribute("grant_type", user.getAuthorities()); + model.addAllAttributes(user.getAttributes()); + } + } + + @GetMapping(value = "/") + public String index(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "home"; + } + + @GetMapping(value = "/greeting") + public String greeting(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "greeting"; + } + + @GetMapping(value = "/home") + public String home(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "home"; + } + } + ``` + +11. Create a folder named *security* in the Java source folder for your application. + +12. Create a new Java file named *WebSecurityConfig.java* in the *security* folder and open it in a text editor. + +13. Enter the following code, then save and close the file: + + ```java + package sample.aad.security; + + import com.microsoft.azure.spring.autoconfigure.btoc.AADB2COidcLoginConfigurer; + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + + @EnableWebSecurity + public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { + + private final AADB2COidcLoginConfigurer configurer; + + public WebSecurityConfiguration(AADB2COidcLoginConfigurer configurer) { + this.configurer = configurer; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest() + .authenticated() + .and() + .apply(configurer) + ; + } + } + ``` +14. Copy the `greeting.html` and `home.html` from [Azure AD B2C Spring Boot Sample](https://github.com/Microsoft/azure-spring-boot/tree/master/azure-spring-boot-samples/azure-active-directory-b2c-oidc-spring-boot-sample/src/main/resources/templates), and replace the +`${your-profile-edit-user-flow}` and `${your-password-reset-user-flow}` with your user flow name +respectively that completed earlier. + +### Build and test your app + +1. Open a command prompt and change directory to the folder where your app's *pom.xml* file is located. + +2. Build your Spring Boot application with Maven and run it; for example: + + ```shell + mvn clean package + mvn spring-boot:run + ``` + +3. After your application is built and started by Maven, open in a web browser; +you should be redirected to login page. + +4. Click linke with name of `${your-sign-up-or-in}` user flow, you should be rediected Azure AD B2C to start the authentication process. + +4. After you have logged in successfully, you should see the sample `home page` from the browser. + +### Allow telemetry + +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. + +``` +azure.activedirectory.b2c.allow-telemetry=false +``` + +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. + +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + +## Key concepts + +## Troubleshooting + +## Next steps + +## Contributing + +## Summary + +In this documentation, you created a new Java web application using the Azure Active Directory B2C starter, +configured a new Azure AD B2C tenant and registered a new application in it, and then configured your +application to use the Spring annotations and classes to protect the web app. + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory-b2c/pom.xml b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/pom.xml new file mode 100644 index 000000000000..75cbdb6c76be --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/pom.xml @@ -0,0 +1,119 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-active-directory-b2c + 2.2.5-beta.1 + + Azure AD B2C Spring Security Integration Spring Boot Starter + Spring Boot Starter for Azure AD B2C and Spring Security Integration + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + + + org.springframework + spring-web + 5.2.0.RELEASE + + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.springframework.security + spring-security-core + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-web + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-config + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-oauth2-core + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-oauth2-client + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-oauth2-jose + 5.2.0.RELEASE + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + javax.validation:validation-api:[2.0.1.Final] + org.springframework:spring-web:[5.2.0.RELEASE] + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + org.springframework.security:spring-security-config:[5.2.0.RELEASE] + org.springframework.security:spring-security-core:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-client:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-core:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-jose:[5.2.0.RELEASE] + org.springframework.security:spring-security-web:[5.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/README.md b/sdk/spring/azure-spring-boot-starter-active-directory/README.md new file mode 100644 index 000000000000..687147857ca4 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory/README.md @@ -0,0 +1,225 @@ +# Azure AAD Spring Boot Starter client library for Java + +## Overview +With Spring Starter for Azure Active Directory, now you can get started quickly to build the authentication workflow for a web application that uses Azure AD and OAuth 2.0 to secure its back end. It also enables developers to create a role based authorization workflow for a Web API secured by Azure AD, with the power of the Spring Security Filter Chain. + +### Key concepts +This package provides 2 ways to integrate with Spring Security and authenticate with Azure Active Directory. +* Authenticate in backend, auto configuration for common Azure Active Directory OAuth2 properties and `OAuth2UserService` to map authorities are provided. +* Authenticate in frontend, sends bearer authorization code to backend, in backend a Spring Security filter validates the Jwt token from Azure AD and save authentication. The Jwt token is also used to acquire a On-Behalf-Of token for Azure AD Graph API so that authenticated user's membership information is available for authorization of access of API resources. Below is a diagram that shows the layers and typical flow for Single Page Application with Spring Boot web API backend that uses the filter for Authentication and Authorization. +![Single Page Application + Spring Boot Web API + Azure AD](resource/spring-aad.png) + +The authorization flow is composed of 3 phrases: +* Login with credentials and validate id_token from Azure AD +* Get On-Behalf-Of token and membership info from Azure AD Graph API +* Evaluate the permission based on membership info to grant or deny access + +##Getting started +#### Register the Application in Azure AD +* **Register a new application**: Go to Azure Portal - Azure Active Directory - App registrations - New application registration to register the application in Azure Active Directory. `Application ID` is `client-id` in `application.properties`. +* **Grant permissions to the application**: After application registration succeeded, go to API ACCESS - Required permissions - DELEGATED PERMISSIONS, tick `Access the directory as the signed-in user` and `Sign in and read user profile`. Click `Grant Permissions` (Note: you will need administrator privilege to grant permission). +* **Create a client secret key for the application**: Go to API ACCESS - Keys to create a secret key (`client-secret`). + +#### Add Maven Dependency + +`azure-spring-boot-starter-active-directory` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-active-directory;dependency}) +```xml + + com.azure + azure-spring-boot-starter-active-directory + 1.0.0-beta.1 + +``` +[//]: # ({x-version-update-end}) + +## Examples +#### Configure application.properties and autowire beans + +Refer to different samples for different authentication ways. + +##### Authenticate in backend + +Please refer to [azure-spring-boot-sample-active-directory-backend](../azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend/README-a.md) for authenticate in backend. + +Configure application.properties: +```properties +spring.security.oauth2.client.registration.azure.client-id=xxxxxx-your-client-id-xxxxxx +spring.security.oauth2.client.registration.azure.client-secret=xxxxxx-your-client-secret-xxxxxx + +azure.activedirectory.tenant-id=xxxxxx-your-tenant-id-xxxxxx +azure.activedirectory.active-directory-groups=group1, group2 +``` + +Autowire `OAuth2UserService` bean in `WebSecurityConfigurerAdapter`: +```java + @Autowired + private OAuth2UserService oidcUserService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login() + .userInfoEndpoint() + .oidcUserService(oidcUserService); + } +``` + +##### Authenticate in frontend + +Please refer to [azure-active-directory-spring-boot-sample](../azure-spring-boot-samples/azure-spring-boot-sample-active-directory/README-a.md) for how to integrate Spring Security and Azure AD for authentication and authorization in a Single Page Application (SPA) scenario. + +Configure application.properties: +```properties +azure.activedirectory.client-id=Application-ID-in-AAD-App-registrations +azure.activedirectory.client-secret=Key-in-AAD-API-ACCESS +azure.activedirectory.active-directory-groups=Aad-groups e.g. group1,group2,group3 +``` + +If you're using [Azure China](https://docs.microsoft.com/en-us/azure/china/china-welcome), please append an extra line to the `application.properties` file: +```properties +azure.activedirectory.environment=cn +``` + +* Autowire `AADAuthenticationFilter` in `WebSecurityConfig.java` file + +``` +@Autowired +private AADAuthenticationFilter aadAuthFilter; +``` + +* Role-based Authorization with annotation `@PreAuthorize("hasRole('GROUP_NAME')")` +* Role-based Authorization with method `isMemberOf()` + +##### Authenticate stateless APIs using AAD app roles +This scenario fits best for stateless Spring backends exposing an API to SPAs ([OAuth 2.0 implicit grant flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-implicit-grant-flow)) +or service-to-service access using the [client credentials grant flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-client-creds-grant-flow). + +The stateless processing can be activated with the `azure.activedirectory.session-stateless` property. +The authorization is using the [AAD AppRole feature](https://docs.microsoft.com/en-us/azure/architecture/multitenant-identity/app-roles#roles-using-azure-ad-app-roles), +so instead of using the `groups` claim the token has a `roles` claim which contains roles [configured in your manifest](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps#examples). + +Configure your `application properties`: + +```properties +azure.activedirectory.session-stateless=true +azure.activedirectory.client-id=xxxxxx-your-client-id-xxxxxx +``` + +Define your roles in your application registration manifest: +```json + "appRoles": [ + { + "allowedMemberTypes": [ + "User" + ], + "displayName": "My demo", + "id": "00000000-0000-0000-0000-000000000000", + "isEnabled": true, + "description": "My demo role.", + "value": "MY_ROLE" + } + ], +``` + +Autowire the auth filter and attach it to the filter chain: +```java + @Autowired + private AADAppRoleStatelessAuthenticationFilter appRoleAuthFilter; + + @Override + protected void configure(HttpSecurity http) throws Exception { + [...] + http.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilterBefore(appRoleAuthFilter, UsernamePasswordAuthenticationFilter.class); + } +``` + +* Role-based Authorization with annotation `@PreAuthorize("hasRole('MY_ROLE')")` +* Role-based Authorization with method `isMemberOf()` + +The roles you want to use within your application have to be [set up in the manifest of your +application registration](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps). + +##### Using The Microsoft Graph API +By default, azure-spring-boot is set up to utilize the Azure AD Graph. If you would prefer, it can be set up to utilize the Microsoft Graph instead. In order to do this, you will need to update the app registration in Azure to grant the application permissions to the Microsoft Graph API and add some properties to the application.properties file. + +* **Grant permissions to the application**: After application registration succeeded, go to API permissions - Add a permission, select `Microsoft Graph`, select Delegated permissions, tick `Directory.AccessAsUser.All - Access the directory as the signed-in user` and `Use.Read - Sign in and read user profile`. Click `Add Permissions` (Note: you will need administrator privilege to grant permission). Furthermore, you can remove the API permissions to the Azure Active Directory Graph, as these will not be needed. + +* **Configure your `application properties`**: +```properties +azure.activedirectory.environment=global-v2-graph +azure.activedirectory.user-group.key=@odata.type +azure.activedirectory.user-group.value=#microsoft.graph.group +azure.activedirectory.user-group.object-id-key=id +``` + +If you're using [Azure China](https://docs.microsoft.com/en-us/azure/china/china-welcome), please set the environment property in the `application.properties` file to: +```properties +azure.activedirectory.environment=cn-v2-graph +``` + +Please refer to [azure-spring-boot-sample-active-directory-backend-v2](../azure-spring-boot-samples/azure-spring-boot-sample-active-directory-backend-v2/) to see a sample configured to use the Microsoft Graph API. +### Using Microsoft identity platform endpoints +If you want to use v2 version endpoints to do authorization and authentication, please pay attention to the attributes of claims, because there are some attributes exits in v1 version id-token by default but not in v2 version id-token, if you have to get that attribute, please make sure to add it into your scope. +There is the doc [Difference between v1 and v2](https://docs.microsoft.com/en-us/azure/active-directory/develop/azure-ad-endpoint-comparison), For example, the name attribute doesn't exist in v2 token, if you want it, you need add `profile` to your scope, like this: +```properties +spring.security.oauth2.client.registration.azure.scope=openid, https://graph.microsoft.com/user.read, profile +``` +You can see more details in this link: [details](https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens) + +### AAD Conditional Access Policy +Now azure-active-directory-spring-boot-starter has supported AAD conditional access policy, if you are using this policy, you need add **AADOAuth2AuthorizationRequestResolver** and **AADAuthenticationFailureHandler** to your WebSecurityConfigurerAdapter. +``` +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class AADOAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { + @Autowired + private OAuth2UserService oidcUserService; + + @Autowired + ApplicationContext applicationContext; + + @Override + protected void configure(HttpSecurity http) throws Exception { + final ClientRegistrationRepository clientRegistrationRepository = + applicationContext.getBean(ClientRegistrationRepository.class); + http.authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2Login() + .userInfoEndpoint() + .oidcUserService(oidcUserService) + .and() + .authorizationEndpoint() + .authorizationRequestResolver( + new AADOAuth2AuthorizationRequestResolver(clientRegistrationRepository)) + .and() + .failureHandler(new AADAuthenticationFailureHandler()); + } +} +``` +## Next steps +#### Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. +Microsoft uses this information to improve our tooling experience. Participation is voluntary. +If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +```properties +azure.activedirectory.allow-telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + +## Key concepts + +## Troubleshooting + +## Contributing + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml new file mode 100644 index 000000000000..dee646aab56f --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-active-directory + 2.2.5-beta.1 + + Azure AD Spring Security Integration Spring Boot Starter + Spring Boot Starter for Azure AD and Spring Security Integration + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + org.springframework + spring-web + 5.2.0.RELEASE + + + org.springframework.security + spring-security-core + 5.2.0.RELEASE + + + org.springframework.security + spring-security-web + 5.2.0.RELEASE + + + org.springframework.security + spring-security-config + 5.2.0.RELEASE + + + com.microsoft.azure + msal4j + 1.3.0 + + + com.nimbusds + nimbus-jose-jwt + 7.9 + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + com.fasterxml.jackson.core:jackson-databind:[2.10.1] + com.nimbusds:nimbus-jose-jwt:[7.9] + org.springframework:spring-web:[5.2.0.RELEASE] + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + org.springframework.security:spring-security-config:[5.2.0.RELEASE] + org.springframework.security:spring-security-core:[5.2.0.RELEASE] + org.springframework.security:spring-security-web:[5.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/resource/spa-oauth2.png b/sdk/spring/azure-spring-boot-starter-active-directory/resource/spa-oauth2.png new file mode 100644 index 000000000000..46b6e1323787 Binary files /dev/null and b/sdk/spring/azure-spring-boot-starter-active-directory/resource/spa-oauth2.png differ diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/resource/spring-aad.png b/sdk/spring/azure-spring-boot-starter-active-directory/resource/spring-aad.png new file mode 100644 index 000000000000..42782d8bce12 Binary files /dev/null and b/sdk/spring/azure-spring-boot-starter-active-directory/resource/spring-aad.png differ diff --git a/sdk/spring/azure-spring-boot-starter-active-directory/src/main/resources/aad.enable.config b/sdk/spring/azure-spring-boot-starter-active-directory/src/main/resources/aad.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory/src/main/resources/aad.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-cosmosdb/README.md b/sdk/spring/azure-spring-boot-starter-cosmosdb/README.md new file mode 100644 index 000000000000..cfac8c453ef6 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-cosmosdb/README.md @@ -0,0 +1,176 @@ +## Azure Cosmos DB Spring Boot Starter client library for Java + +[Azure Cosmos DB](https://azure.microsoft.com/en-us/services/cosmos-db/) is a globally-distributed database service that allows developers to work with data using a variety of standard APIs, such as SQL, MongoDB, Graph, and Azure Table storage. + +## TOC + +* [Key concepts](#key-concepts) +* [Examples](#examples) +* [Getting started](#getting-started) + +## Key concepts +- Spring Data ReactiveCrudRepository basic CRUD functionality + - save + - findAll + - findOne by Id + - deleteAll + - delete by Id + - delete entity +- Spring Data [@Id](https://github.com/spring-projects/spring-data-commons/blob/db62390de90c93a78743c97cc2cc9ccd964994a5/src/main/java/org/springframework/data/annotation/Id.java) annotation. + There're 2 ways to map a field in domain class to `id` of Azure Cosmos DB document. + - annotate a field in domain class with @Id, this field will be mapped to document `id` in Cosmos DB. + - set name of this field to `id`, this field will be mapped to document `id` in Cosmos DB. + [Note] if both way applied, +- Custom collection Name. + By default, collection name will be class name of user domain class. To customize it, add annotation `@Document(collection="myCustomCollectionName")` to your domain class, that's all. +- Supports [Azure Cosmos DB partition](https://docs.microsoft.com/en-us/azure/cosmos-db/partition-data). To specify a field of your domain class to be partition key field, just annotate it with `@PartitionKey`. When you do CRUD operation, please specify your partition value. For more sample on partition CRUD, please refer to [test here](./test/java/com/microsoft/azure/spring/data/cosmosdb/repository/AddressRepositoryIT.java) +- Supports [Spring Data custom query](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#repositories.query-methods.details) find operation. +- Supports [spring-boot-starter-data-rest](https://projects.spring.io/spring-data-rest/). +- Supports List and nested type in domain class. + +## Examples +Please refer to [sample project here](../azure-spring-boot-samples/azure-spring-boot-sample-cosmosdb). + +## Getting started + +### Add the dependency + +`azure-spring-boot-starter-cosmosdb` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +```xml + + com.azure + azure-spring-boot-starter-cosmosdb + +``` + +### Add the property setting + +Open `application.properties` file and add below properties with your Cosmos DB credentials. + +``` +azure.cosmosdb.uri=your-cosmosdb-uri +azure.cosmosdb.key=your-cosmosdb-key +azure.cosmosdb.database=your-cosmosdb-databasename +``` + +Property `azure.cosmosdb.consistency-level` is also supported. + +Property `azure.cosmosdb.cosmosKeyCredential` is also supported. CosmosKeyCredential feature provides capability to +rotate keys on the fly. You can switch keys using switchToSecondaryKey(). For more information on this, see the Sample +Application code. + +### Define an entity +Define a simple entity as Document in Cosmos DB. + +``` +@Document(collection = "mycollection") +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class User { + @Id + private String id; + private String firstName; + @PartitionKey + private String lastName; + private String address; + + @Override + public String toString() { + return String.format("User: %s %s, %s", firstName, lastName, address); + } +} +``` +`id` field will be used as document `id` in Azure Cosmos DB. Or you can annotate any field with `@Id` to map it to document `id`. + +Annotation `@Document(collection="mycollection")` is used to specify the collection name of your document in Azure Cosmos DB. + +### Create repositories +Extends ReactiveCosmosRepository interface, which provides Spring Data repository support. + +``` +import com.microsoft.azure.spring.data.cosmosdb.repository.ReactiveCosmosRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +@Repository +public interface UserRepository extends ReactiveCosmosRepository { + + Flux findByFirstName(String firstName); +} +``` + +So far ReactiveCosmosRepository provides basic save, delete and find operations. More operations will be supported later. + +### Create an Application class +Here create an application class with all the components +``` +@SpringBootApplication +public class CosmosSampleApplication implements CommandLineRunner { + + private static final Logger LOGGER = LoggerFactory.getLogger(CosmosSampleApplication.class); + + @Autowired + private UserRepository repository; + + public static void main(String[] args) { + SpringApplication.run(CosmosSampleApplication.class, args); + } + + public void run(String... var1) throws Exception { + final User testUser = new User("testId", "testFirstName", "testLastName", "test address line one"); + + // Save the User class to Azure CosmosDB database. + final Mono saveUserMono = repository.save(testUser); + + final Flux firstNameUserFlux = repository.findByFirstName("testFirstName"); + + // Nothing happens until we subscribe to these Monos. + // findById will not return the user as user is not present. + final Mono findByIdMono = repository.findById(testUser.getId()); + final User findByIdUser = findByIdMono.block(); + Assert.isNull(findByIdUser, "User must be null"); + + final User savedUser = saveUserMono.block(); + Assert.state(savedUser != null, "Saved user must not be null"); + Assert.state(savedUser.getFirstName().equals(testUser.getFirstName()), "Saved user first name doesn't match"); + + final List users = firstNameUserFlux.collectList().block(); + + final Optional optionalUserResult = repository.findById(testUser.getId()).blockOptional(); + Assert.isTrue(optionalUserResult.isPresent(), "Cannot find user."); + + final User result = optionalUserResult.get(); + Assert.state(result.getFirstName().equals(testUser.getFirstName()), "query result firstName doesn't match!"); + Assert.state(result.getLastName().equals(testUser.getLastName()), "query result lastName doesn't match!"); + + LOGGER.info("findOne in User collection get result: {}", result.toString()); + } + + @PostConstruct + public void setup() { + // For this example, remove all of the existing records. + this.repository.deleteAll().block(); + } +} +``` +Autowired UserRepository interface, then can do save, delete and find operations. + +### Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +``` +azure.cosmosdb.allow-telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + +### Next steps + +Besides using this Azure CosmosDb Spring Boot Starter, you can directly use Spring Data for Azure CosmosDb package for more complex scenarios. Please refer to [Spring Data for Azure CosmosDB](https://github.com/Microsoft/spring-data-cosmosdb) for more details. + +## Troubleshooting +## Contributing +## Next steps diff --git a/sdk/spring/azure-spring-boot-starter-cosmosdb/pom.xml b/sdk/spring/azure-spring-boot-starter-cosmosdb/pom.xml new file mode 100644 index 000000000000..fb3ea7cbbecf --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-cosmosdb/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-cosmosdb + 2.2.5-beta.1 + + Azure Cosmos DB Spring Boot Starter + Spring Boot Starter for Azure Document DB service + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + com.microsoft.azure + spring-data-cosmosdb + 2.2.3.FIX1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-cosmosdb/src/main/resources/cosmosdb.enable.config b/sdk/spring/azure-spring-boot-starter-cosmosdb/src/main/resources/cosmosdb.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-cosmosdb/src/main/resources/cosmosdb.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-data-gremlin/README.md b/sdk/spring/azure-spring-boot-starter-data-gremlin/README.md new file mode 100644 index 000000000000..223a7245dd51 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-data-gremlin/README.md @@ -0,0 +1,161 @@ +# Azure Gremlin Spring Boot Starter client library for Java + +## Overview + +**Spring Data Gremlin** provides initial Spring Data support for those databases using Gremlin query language. With annotation oriented programming model, it simplified the mapping to the database entity. It also provides supports for basic and custom query. + +This project works with *any Gremlin-compatible* data store, and also with [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/introduction). Cosmos is a globally-distributed database service that allows developers to work with data using a variety of standard APIs, such as Graph, MongoDB, and SQL. Spring Data Gremlin provides a delightful experience to interact with Azure Cosmos DB Graph API. + +## Getting started + +### Add the dependency +`azure-spring-boot-starter-data-gremlin` is published on Maven Central Repository. If you are using Maven, add the following dependency. + +[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-data-gremlin;dependency}) +```xml + + com.azure + azure-spring-boot-starter-data-gremlin + 1.0.0-beta.1 + +``` +[//]: # ({x-version-update-end}) + +### Setup Configuration +Setup ```application.yml``` file.(Use Azure Cosmos DB Graph as an example.) + +``` +gremlin: + endpoint: url-of-endpoint + port: 443 + username: /dbs/your-db-name/colls/your-collection-name + password: your-password + telemetryAllowed: true # set false to disable telemetry + +``` + +### Define an entity +Define a simple Vertex entity with ```@Vertex```. + +``` +@Vertex +public class Person { + + @Id + private String id; + + private String name; + + private String age; + + ... +} + +``` + +Define a simple Edge entity with ```@Edge```. + +``` +@Edge +public class Relation { + + @Id + private String id; + + private String name; + + @EdgeFrom + private Person personFrom; + + @EdgeTo + private Person personTo; + + ... +} +``` +Define a simple Graph entity with ```@Graph```. + +``` +@Graph +public class Network { + + @Id + private String id; + + public Network() { + this.edges = new ArrayList(); + this.vertexes = new ArrayList(); + } + + @EdgeSet + private List edges; + + @VertexSet + private List vertexes; + + ... +} +``` + +### Create repositories +Extends CosmosRepository interface, which provides Spring Data repository support. + +``` +import GremlinRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PersonRepository extends GremlinRepository { + List findByName(String name); +} +``` + +`findByName` method is custom query method, it will find the person with the ```name``` property. + +### Create an Application class +Here create an application class with all the components + +``` +@SpringBootApplication +public class SampleApplication implements CommandLineRunner { + + @Autowired + private PersonRepository repository; + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + + public void run(String... var1) throws Exception { + + private final Person testUser = new Person("PERSON_ID", "PERSON_NAME", "PERSON_AGE"); + + repository.deleteAll(); + repository.save(testUser); + + ... + } +} +``` +Autowired UserRepository interface, then can do save, delete and find operations. Spring Data Azure Cosmos DB uses the DocumentTemplate to execute the queries behind *find*, *save* methods. You can use the template yourself for more complex queries. + +## Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +``` +gremlin.allow-telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + + +## Filing Issues + +If you encounter any bug, please file an issue [here](https://github.com/Microsoft/spring-data-gremlin/issues/new?template=custom.md). + +To suggest a new feature or changes that could be made, file an issue the same way you would for a bug. + +## Key concepts +## Examples +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter-data-gremlin/pom.xml b/sdk/spring/azure-spring-boot-starter-data-gremlin/pom.xml new file mode 100644 index 000000000000..747aa845dc6b --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-data-gremlin/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-data-gremlin + 2.2.5-beta.1 + + Spring Data Gremlin Boot Starter + Spring Boot Starter for Spring Data Gremlin + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + com.microsoft.spring.data.gremlin + spring-data-gremlin + 2.2.3 + + + com.fasterxml.jackson.core + jackson-core + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + com.fasterxml.jackson.core:jackson-core:[2.10.1] + com.microsoft.spring.data.gremlin:spring-data-gremlin:[2.2.3] + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-data-gremlin/src/main/resources/gremlin.enable.config b/sdk/spring/azure-spring-boot-starter-data-gremlin/src/main/resources/gremlin.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-data-gremlin/src/main/resources/gremlin.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md new file mode 100644 index 000000000000..f919dad90ce6 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/README.md @@ -0,0 +1,104 @@ +## Azure Key Vault Secrets Spring boot starter client library for Java +Azure Key Vault Secrets Spring boot starter is Spring starter for [Azure Key Vault Secrets](https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#BKMK_WorkingWithSecrets). With this starter, Azure Key Vault is added as one of Spring PropertySource, so secrets stored in Azure Key Vault could be easily used and conveniently accessed like other externalized configuration property, e.g. properties in files. + +## Examples +Please refer to [sample project here](../azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets). + +## Getting started + +### Add the dependency + +`azure-spring-boot-starter-keyvault-secrets` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-keyvault-secrets;dependency}) +```xml + + com.azure + azure-spring-boot-starter-keyvault-secrets + 1.0.0-beta.1 + +``` +[//]: # ({x-version-update-end}) + +### Custom settings +To use custom configuration, open `application.properties` file and add below properties to specify your Azure Key Vault url, Azure service principal client id and client key. `azure.keyvault.enabled` is used to turn on/off Azure Key Vault Secret property source, default is true. `azure.keyvault.token-acquiring-timeout-seconds` is used to specify the timeout in seconds when acquiring token from Azure AAD. Default value is 60 seconds. This property is optional. `azure.keyvault.refresh-interval` is the period for PropertySource to refresh secret keys, its value is 1800000(ms) by default. This property is optional. `azure.keyvault.secret.keys` is a property to indicate that if application using specific secret keys, if this property is set, application will only load the keys in the property and won't load all the keys from keyvault, that means if you want to update your secrets, you need to restart the application rather than only add secrets in the keyvault. +``` +azure.keyvault.enabled=true +azure.keyvault.uri=put-your-azure-keyvault-uri-here +azure.keyvault.client-id=put-your-azure-client-id-here +azure.keyvault.client-key=put-your-azure-client-key-here +azure.keyvault.tenant-id=put-your-azure-tenant-id-here +azure.keyvault.token-acquire-timeout-seconds=60 +azure.keyvault.refresh-interval=1800000 +azure.keyvault.secret.keys=key1,key2,key3 +``` + +### Use MSI / Managed identities +#### App Services +To use managed identities for App Services - please refer to [How to use managed identities for App Service and Azure Functions](https://docs.microsoft.com/en-us/azure/app-service/app-service-managed-service-identity). + +To use it in an App Service, add the below properties: +``` +azure.keyvault.enabled=true +azure.keyvault.uri=put-your-azure-keyvault-uri-here +``` + +#### VM +To use it for virtual machines, please refer to [Azure AD managed identities for Azure resources documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/). + +To use it in a VM, add the below properties: +``` +azure.keyvault.enabled=true +azure.keyvault.uri=put-your-azure-keyvault-uri-here +azure.keyvault.client-id=put-your-azure-client-id-here +``` + +If you are using system assigned identity you don't need to specify the client-id. + +### Save secrets in Azure Key Vault +Save secrets in Azure Key Vault through [Azure Portal](https://blogs.technet.microsoft.com/kv/2016/09/12/manage-your-key-vaults-from-new-azure-portal/) or [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/keyvault/secret). + +You can use the following Azure CLI command to save secrets, if Key Vault is already created. +``` +az keyvault secret set --name --value --vault-name +``` +> NOTE +> To get detail steps on how setup Azure Key Vault, please refer to sample code readme section ["Setup Azure Key Vault"](../azure-spring-boot-samples/azure-spring-boot-sample-keyvault-secrets/README.md) + +> **IMPORTANT** +> Allowed secret name pattern in Azure Key Vault is ^[0-9a-zA-Z-]+$, for some Spring system properties contains `.` like spring.datasource.url, do below workaround when you save it into Azure Key Vault: simply replace `.` to `-`. `spring.datasource.url` will be saved with name `spring-datasource-url` in Azure Key Vault. While in client application, use original `spring.datasource.url` to retrieve property value, this starter will take care of transformation for you. Purpose of using this way is to integrate with Spring existing property setting. + +### Get Key Vault secret value as property +Now, you can get Azure Key Vault secret value as a configuration property. + +``` +@SpringBootApplication +public class SampleApplication implements CommandLineRunner { + + @Value("${your-property-name}") + private String mySecretProperty; + + public static void main(String[] args) { + SpringApplication.run(SampleApplication.class, args); + } + + public void run(String... varl) throws Exception { + System.out.println( "property your-property-name value is: " + mySecretProperty); + } + +} +``` + +## Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +``` +azure.keyvault.allow.telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + +## Key concepts +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml new file mode 100644 index 000000000000..094e26b7328f --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-keyvault-secrets/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-keyvault-secrets + 2.2.5-beta.1 + + Azure Key Vault Secrets Spring Boot Starter + Spring Boot Starter supporting Azure Key Vault Secrets as PropertySource + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + com.microsoft.azure + azure-client-authentication + 1.7.3 + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-mediaservices/README.md b/sdk/spring/azure-spring-boot-starter-mediaservices/README.md new file mode 100644 index 000000000000..ef0cebec0057 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-mediaservices/README.md @@ -0,0 +1,62 @@ +#Azure Media Services Spring Boot Starter client library for Java + +## Getting started + +### Add the dependency + +`azure-spring-boot-starter-mediaservices` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-mediaservices;dependency}) +```xml + + com.azure + azure-spring-boot-starter-mediaservices + 1.0.0-beta.1 + +``` +[//]: # ({x-version-update-end}) + +### Add the property setting + +Open `application.properties` file and add below properties with your Azure Media Services credentials. + +``` +azure.mediaservices.tenant=put-your-media-service-azure-ad-tenant-domain-here +azure.mediaservices.client-id=put-your-azure-ad-client-id-here +azure.mediaservices.client-secret=put-your-azure-ad-client-secret-here +azure.mediaservices.rest-api-endpoint=put-your-media-service-rest-api-endpoint-here +``` + +# Optional +If you are using network proxy then add below properties to run azure media services behind proxy. + +``` +azure.mediaservices.proxy-host=put-your-network-proxy-host +azure.mediaservices.proxy-port=put-your-network-proxy-port +azure.mediaservices.proxy-scheme=put-your-network-proxy-scheme +``` + +### Add auto-wiring code + +Add below alike code to auto-wire the `MediaContract` object. Then you can use it to upload, encode and set streaming url. For details usage, please reference this [document](https://docs.microsoft.com/en-us/azure/media-services/media-services-java-how-to-use). + +``` +@Autowired +private MediaContract mediaService; +``` + +### Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +``` +azure.mediaservices.allow-telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + + +## Key concepts +## Examples +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter-mediaservices/pom.xml b/sdk/spring/azure-spring-boot-starter-mediaservices/pom.xml new file mode 100644 index 000000000000..4c40e3417233 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-mediaservices/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-mediaservices + 2.2.5-beta.1 + + Azure Media Services Spring Boot Starter + Spring Boot Starter for Azure Media service + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + com.microsoft.azure + azure-media + 0.9.8 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-mediaservices/src/main/resources/mediaservices.enable.config b/sdk/spring/azure-spring-boot-starter-mediaservices/src/main/resources/mediaservices.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-mediaservices/src/main/resources/mediaservices.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-metrics/README.md b/sdk/spring/azure-spring-boot-starter-metrics/README.md new file mode 100644 index 000000000000..4a2e55f73a9e --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-metrics/README.md @@ -0,0 +1,46 @@ +#Azure Metrics Spring Boot Starter client library for Java + +## Getting started + +### Add the dependency + +`azure-spring-boot-starter-metrics` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-metrics;dependency}) +```xml + + com.azure + azure-spring-boot-starter-metrics + 1.0.0-beta.1 + +``` +[//]: # ({x-version-update-end}) + +### Add the property setting + +Open `application.properties` file and add below properties with your instrumentation key. + +``` +management.metrics.export.azuremonitor.instrumentation-key= +``` + +## Troubleshooting +1. If the JDK version you use is greater than 1.8, You may meet this problem: +``` +NoClassDefFoundError: javax/xml/bind/JAXBException +``` + +To solve this issue, you need to add the dependency below into your classpath: +``` + + javax.xml.bind + jaxb-api + 2.3.0 + +``` + +## Key concepts +## Examples +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter-metrics/pom.xml b/sdk/spring/azure-spring-boot-starter-metrics/pom.xml new file mode 100644 index 000000000000..71cc723ee3e8 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-metrics/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-metrics + 2.2.5-beta.1 + + Azure Metrics Spring Boot Starter + Spring Boot Starter for Micrometer Metrics + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + io.micrometer + micrometer-registry-azure-monitor + 1.3.0 + + + org.springframework.boot + spring-boot-actuator-autoconfigure + 2.2.0.RELEASE + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + io.micrometer:micrometer-core:[1.3.0] + io.micrometer:micrometer-registry-azure-monitor:[1.3.0] + org.springframework.boot:spring-boot-actuator-autoconfigure:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-metrics/src/main/resources/metrics.enable.config b/sdk/spring/azure-spring-boot-starter-metrics/src/main/resources/metrics.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-metrics/src/main/resources/metrics.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-servicebus-jms/README.md b/sdk/spring/azure-spring-boot-starter-servicebus-jms/README.md new file mode 100644 index 000000000000..5ac1ff7863b1 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-servicebus-jms/README.md @@ -0,0 +1,34 @@ +#Azure Service Bus JMS Spring Boot Starter client library for Java +## Getting started + +It is Azure Service Bus JMS Spring Boot Starter. + +These code [Service Bus Queue Sample](../../azure-spring-boot-samples/azure-servicebus-jms-queue-spring-boot-sample/) and [Service Bus Topic Sample](../../azure-spring-boot-samples/azure-servicebus-jms-topic-spring-boot-sample/) respectively demonstrates how to use Spring JMS Queue and Topic for Azure Service Bus via the Starter. + +Running these samples will be charged by Azure. You can check the usage and bill at this [link](https://azure.microsoft.com/en-us/account/). + +### Add the dependency + +`azure-spring-boot-starter-servicebus-jms` is published on Maven Central Repository. +Add the following dependency to your project: + +```xml + + com.azure + azure-spring-boot-starter-servicebus-jms + +``` + +### Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +``` +azure.servicebus.allow-telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + +## Key concepts +## Examples +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter-servicebus-jms/pom.xml b/sdk/spring/azure-spring-boot-starter-servicebus-jms/pom.xml new file mode 100644 index 000000000000..2737a51417e9 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-servicebus-jms/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-servicebus-jms + 2.2.5-beta.1 + + Azure Service Bus JMS Spring Boot Starter + Spring Boot Starter for Azure Service Bus JMS + https://github.com/Azure/azure-sdk-for-java + + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + + + org.springframework + spring-jms + 5.2.0.RELEASE + + + + + org.apache.qpid + qpid-jms-client + 0.43.0 + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + false + + + *:*:*:*:runtime + *:*:*:*:compile + *:*:*:*:provided + + + com.microsoft.azure:* + org.apache.qpid:qpid-jms-client:[0.43.0] + org.springframework:spring-jms:[5.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-servicebus-jms/src/main/resources/servicebusjms.enable.config b/sdk/spring/azure-spring-boot-starter-servicebus-jms/src/main/resources/servicebusjms.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-servicebus-jms/src/main/resources/servicebusjms.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-servicebus/README.md b/sdk/spring/azure-spring-boot-starter-servicebus/README.md new file mode 100644 index 000000000000..21365219d84a --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-servicebus/README.md @@ -0,0 +1,84 @@ +#Azure Service Bus Spring Boot Starter client library for Java +## Getting started + +### Add the dependency + +`azure-spring-boot-starter-servicebus` is published on Maven Central Repository. +Add the following dependency to your project: + +*Maven* + +[//]: # ({x-version-update-start;com.azure:azure-spring-boot-starter-servicebus;dependency}) +```xml + + com.azure + azure-spring-boot-starter-servicebus + 1.0.0-beta.1 + +``` +[//]: # ({x-version-update-end}) + +*Gradle*
+```compile 'com.azure:azure-spring-boot-starter-servicebus:1.0.0-beta.1'``` + +### Add the property setting + +Open `application.properties` file and add below property with your Service Bus connection string. + +``` +azure.servicebus.connection-string=Endpoint=myEndpoint;SharedAccessKeyName=mySharedAccessKeyName;SharedAccessKey=mySharedAccessKey +``` + +If you want to use queue, please specify your created queue name and receive mode as below. + +``` +azure.servicebus.queue-name=put-your-queue-name-here +azure.servicebus.queue-receive-mode=peeklock +``` + +For topic, please specify your created topic name. + +``` +azure.servicebus.topic-name=put-your-topic-name-here +``` + +For subscription, please specify your created subscription name and receive mode. + +``` +azure.servicebus.subscription-name=put-your-subscription-name-here +azure.servicebus.subscription-receive-mode=peeklock +``` + +### Add auto-wiring code + +You can use the following code to autowire the Azure Service Bus Queue, Topic, and Subscription clients in your Spring Boot application. Please see sample code in the [azure-servicebus-spring-boot-sample](../azure-spring-boot-samples/azure-spring-boot-sample-servicebus) folder as a reference. + +``` +@Autowired +private QueueClient queueClient; +``` + +``` +@Autowired +private TopicClient topicClient; +``` + +``` +@Autowired +private SubscriptionClient subscriptionClient; +``` + + +### Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +``` +azure.servicebus.allow-telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + +## Key concepts +## Examples +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter-servicebus/pom.xml b/sdk/spring/azure-spring-boot-starter-servicebus/pom.xml new file mode 100644 index 000000000000..c03f04c79ecc --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-servicebus/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-servicebus + 2.2.5-beta.1 + + Azure Service Bus Spring Boot Starter + Spring Boot Starter for Azure Service Bus + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + com.microsoft.azure + azure-servicebus + 1.2.15 + + + com.fasterxml.jackson.core + jackson-core + 2.10.1 + + + commons-codec + commons-codec + 1.13 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + false + + + *:*:*:*:runtime + *:*:*:*:compile + *:*:*:*:provided + + + com.microsoft.azure:* + com.fasterxml.jackson.core:jackson-core:[2.10.1] + commons-codec:commons-codec:[1.13] + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-servicebus/src/main/resources/servicebus.enable.config b/sdk/spring/azure-spring-boot-starter-servicebus/src/main/resources/servicebus.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-servicebus/src/main/resources/servicebus.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-storage/README.md b/sdk/spring/azure-spring-boot-starter-storage/README.md new file mode 100644 index 000000000000..76f6180b0b9f --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-storage/README.md @@ -0,0 +1,60 @@ +#Azure Storage Spring Boot Starter client library for Java +## Getting started + +### Add the dependency + +`azure-spring-boot-starter-storage` is published on Maven Central Repository. +If you are using Maven, add the following dependency. + +```xml + + com.azure + azure-spring-boot-starter-storage + +``` + +### Add the property setting + +Open `application.properties` file and add below property with your Azure Storage connection string. + +``` +azure.storage.account-name=put-your-azure-storage-account-name-here +azure.storage.account-key=put-your-azure-storage-account-key-here +azure.storage.container-name=put-your-azure-storage-container-name-here +``` + +With above configuration, a `ServiceURL` and a `ContainerURL` bean will be created. +`azure.storage.container-name` is optional, you can also create `ContainerURL` from `ServiceURL` by `ServiceURL#createContainerURL(String containerName)`. + +### Add auto-wiring code + +Add below alike code to auto-wire the `ServiceURL` bean and `ContainerURL` bean. For details usage, please reference this [document](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-java-v10#upload-blobs-to-the-container). + +``` +@Autowired +private ServiceURL serviceURL; + +@Autowired +private ContainerURL containerURL; +``` + +### Enable HTTPs + +It's possible to configure Azure to allow only HTTPs connections. By default, the library uses HTTP. If you want to use HTTPs, just add the following configuration in `application.properties`. +``` +azure.storage.enable-https=true +``` + +### Allow telemetry +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. +``` +azure.storage.allow-telemetry=false +``` +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + +## Key concepts +## Examples +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter-storage/pom.xml b/sdk/spring/azure-spring-boot-starter-storage/pom.xml new file mode 100644 index 000000000000..ebafa6653cdc --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-storage/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter-storage + 2.2.5-beta.1 + + Azure Storage Spring Boot Starter + Spring Boot Starter for Azure Storage service + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + com.microsoft.azure + azure-storage-blob + 10.5.0 + + + commons-codec + commons-codec + 1.13 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + false + + + *:*:*:*:runtime + *:*:*:*:compile + *:*:*:*:provided + + + com.microsoft.azure:* + commons-codec:commons-codec:[1.13] + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-storage/src/main/resources/storage.enable.config b/sdk/spring/azure-spring-boot-starter-storage/src/main/resources/storage.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-storage/src/main/resources/storage.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/README.md b/sdk/spring/azure-spring-boot-starter/README.md new file mode 100644 index 000000000000..c7c200f18f9b --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/README.md @@ -0,0 +1,8 @@ +# Azure Spring Boot Starters client library for Java + +## Getting started +## Key concepts +## Examples +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-boot-starter/pom.xml b/sdk/spring/azure-spring-boot-starter/pom.xml new file mode 100644 index 000000000000..772fe5b86f80 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot-starter + 2.2.5-beta.1 + + Azure Spring Boot Starter + Core starter, including auto-configuration support + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/aad.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/aad.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/aad.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/aadb2c.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/aadb2c.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/aadb2c.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/cosmosdb.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/cosmosdb.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/cosmosdb.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/gremlin.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/gremlin.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/gremlin.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/mediaservices.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/mediaservices.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/mediaservices.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/metrics.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/metrics.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/metrics.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/servicebus.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/servicebus.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/servicebus.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/servicebusjms.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/servicebusjms.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/servicebusjms.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter/src/main/resources/storage.enable.config b/sdk/spring/azure-spring-boot-starter/src/main/resources/storage.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter/src/main/resources/storage.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/README.md b/sdk/spring/azure-spring-boot/README.md new file mode 100644 index 000000000000..c8ddb56a9aba --- /dev/null +++ b/sdk/spring/azure-spring-boot/README.md @@ -0,0 +1,54 @@ +#Azure Spring Boot client library for Java + +## Key concepts +This project provides auto-configuration for the following Azure services: + +- [Azure Active Directory](../azure-spring-boot-starter-active-directory) +- [Azure Active Directory B2C](../azure-spring-boot-starter-active-directory-b2c) +- [Cosmos DB SQL API](../azure-spring-boot-starter-cosmosdb) +- [Key Vault](../azure-spring-boot-starter-keyvault-secrets) +- [Media Service](../azure-spring-boot-starter-mediaservices) +- [Service Bus](../azure-spring-boot-starter-servicebus) +- [Storage](../azure-spring-boot-starter-storage) + +This module also provides the ability to automatically inject credentials from Cloud Foundry into your +applications consuming Azure services. It does this by reading the `VCAP_SERVICES` environment +variable and setting the appropriate properties used by auto-configuration code. + +For details, please see sample code in the [azure-spring-boot-sample-cloud-foundry](../azure-spring-boot-samples/azure-spring-boot-sample-cloud-foundry) + +## Getting started +To start a new project using Azure, go on [start.spring.io](https://start.spring.io) and select "Azure +Support": this will configure the project to make sure you can integrate easily with Azure service. + +For instance, let's assume that you want to use Service Bus, you can add the usual `azure-servicebus` +dependency to your project and the Spring Boot auto-configuration will kick-in: + +```xml + + com.microsoft.azure + azure-servicebus + +``` + +Note that there is no need to add a `version` as those are managed already by the project. + +Alternatively you may want to use the [starters](../azure-spring-boot-starters) + +[//]: # ({x-version-update-start;com.azure:azure-servicebus-spring-boot-starter;dependency}) +```xml + + com.azure + azure-servicebus-spring-boot-starter + 1.0.0-beta.1 + +``` +[//]: # ({x-version-update-end}) + +## Examples +Please refer to the [samples](../azure-spring-boot-samples) for more getting started instructions. + +## Troubleshooting +## Next steps +## Contributing + diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml new file mode 100644 index 000000000000..c4cfffaa1afc --- /dev/null +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -0,0 +1,316 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + jar + + Azure Spring Boot AutoConfigure + Azure Spring Boot AutoConfigure + https://github.com/Azure/azure-sdk-for-java + + + + org.springframework.boot + spring-boot-autoconfigure + 2.2.0.RELEASE + + + + org.slf4j + slf4j-api + 1.7.28 + + + + org.springframework + spring-web + 5.2.0.RELEASE + + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + + org.springframework.boot + spring-boot-configuration-processor + 2.2.0.RELEASE + true + + + + javax.validation + validation-api + 2.0.1.Final + true + + + + commons-codec + commons-codec + 1.13 + true + + + + + org.springframework.security + spring-security-core + 5.2.0.RELEASE + true + + + org.springframework.security + spring-security-web + 5.2.0.RELEASE + true + + + org.springframework.security + spring-security-oauth2-client + 5.2.0.RELEASE + true + + + org.springframework.security + spring-security-oauth2-jose + 5.2.0.RELEASE + true + + + org.springframework.security + spring-security-config + 5.2.0.RELEASE + true + + + com.nimbusds + nimbus-jose-jwt + 7.9 + true + + + javax.servlet + javax.servlet-api + 4.0.1 + true + + + + + com.microsoft.azure + spring-data-cosmosdb + 2.2.3.FIX1 + true + + + + com.microsoft.spring.data.gremlin + spring-data-gremlin + 2.2.3 + true + + + + + com.microsoft.azure + azure-cosmos + 3.7.0 + true + + + com.microsoft.azure + azure-media + 0.9.8 + true + + + com.microsoft.azure + azure-servicebus + 1.2.15 + true + + + com.microsoft.azure + azure-storage-blob + 10.5.0 + true + + + com.azure + azure-security-keyvault-secrets + 4.2.0-beta.2 + + + com.azure + azure-identity + 1.0.4 + + + com.microsoft.azure + msal4j + 1.3.0 + + + com.microsoft.azure + azure-client-authentication + 1.7.3 + true + + + + io.micrometer + micrometer-registry-azure-monitor + 1.3.0 + true + + + io.micrometer + micrometer-core + 1.3.0 + true + + + org.springframework.boot + spring-boot-actuator-autoconfigure + 2.2.0.RELEASE + true + + + + + org.springframework.boot + spring-boot-autoconfigure-processor + 2.2.0.RELEASE + true + + + + org.hibernate.validator + hibernate-validator + 6.0.17.Final + true + + + + + org.springframework.boot + spring-boot-starter-test + 2.2.0.RELEASE + test + + + com.vaadin.external.google + android-json + + + + + org.springframework.boot + spring-boot-starter-web + 2.2.0.RELEASE + test + + + org.mockito + mockito-core + 3.0.0 + test + + + com.github.tomakehurst + wiremock-standalone + 2.24.1 + test + + + pl.pragmatists + JUnitParams + 1.1.1 + test + + + + + org.springframework + spring-jms + 5.2.0.RELEASE + true + + + + + org.apache.qpid + qpid-jms-client + 0.43.0 + true + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.azure:* + com.microsoft.azure:* + com.fasterxml.jackson.core:jackson-databind:[2.10.1] + com.microsoft.spring.data.gremlin:spring-data-gremlin:[2.2.3] + com.nimbusds:nimbus-jose-jwt:[7.9] + commons-codec:commons-codec:[1.13] + io.micrometer:micrometer-core:[1.3.0] + io.micrometer:micrometer-registry-azure-monitor:[1.3.0] + javax.servlet:javax.servlet-api:[4.0.1] + javax.annotation:javax.annotation-api:[1.3.2] + javax.validation:validation-api:[2.0.1.Final] + org.slf4j:slf4j-api:[1.7.28] + org.hibernate.validator:hibernate-validator:[6.0.17.Final] + org.apache.qpid:qpid-jms-client:[0.43.0] + org.springframework:spring-web:[5.2.0.RELEASE] + org.springframework:spring-jms:[5.2.0.RELEASE] + org.springframework.boot:spring-boot-actuator-autoconfigure:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-autoconfigure-processor:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-autoconfigure:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-configuration-processor:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-test:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-web:[2.2.0.RELEASE] + org.springframework:spring-web:[5.2.0.RELEASE] + org.springframework:spring-jms:[5.2.0.RELEASE] + org.springframework.security:spring-security-config:[5.2.0.RELEASE] + org.springframework.security:spring-security-core:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-client:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-jose:[5.2.0.RELEASE] + org.springframework.security:spring-security-web:[5.2.0.RELEASE] + + + + + + + + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/Constants.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/Constants.java new file mode 100644 index 000000000000..150011d75cf4 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/Constants.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.microsoft.azure.utils.PropertyLoader; + +public class Constants { + public static final String AZURE_KEYVAULT_USER_AGENT = "spring-boot-starter/" + PropertyLoader.getProjectVersion(); + public static final String AZURE_KEYVAULT_CLIENT_ID = "azure.keyvault.client-id"; + public static final String AZURE_KEYVAULT_CLIENT_KEY = "azure.keyvault.client-key"; + public static final String AZURE_KEYVAULT_TENANT_ID = "azure.keyvault.tenant-id"; + public static final String AZURE_KEYVAULT_CERTIFICATE_PATH = "azure.keyvault.certificate.path"; + public static final String AZURE_KEYVAULT_CERTIFICATE_PASSWORD = "azure.keyvault.certificate.password"; + public static final String AZURE_KEYVAULT_ENABLED = "azure.keyvault.enabled"; + public static final String AZURE_KEYVAULT_VAULT_URI = "azure.keyvault.uri"; + public static final String AZURE_KEYVAULT_REFRESH_INTERVAL = "azure.keyvault.refresh-interval"; + public static final String AZURE_KEYVAULT_SECRET_KEYS = "azure.keyvault.secret.keys"; + public static final String AZURE_KEYVAULT_PROPERTYSOURCE_NAME = "azurekv"; + public static final String AZURE_TOKEN_ACQUIRE_TIMEOUT_IN_SECONDS = "azure.keyvault.token-acquire-timeout-seconds"; + public static final String AZURE_KEYVAULT_ALLOW_TELEMETRY = "azure.keyvault.allow.telemetry"; + + public static final long DEFAULT_REFRESH_INTERVAL_MS = 1800000L; + public static final long TOKEN_ACQUIRE_TIMEOUT_SECS = 60L; + + // for the User-Agent header set in track2 SDKs + private static final String SNAPSHOT_VERSION = "snapshot"; + private static final String AZURE = "az"; + private static final String SPRING = "sp"; + private static final String KEY_VAULT = "kv"; + + public static final String SPRINGBOOT_VERSION = SNAPSHOT_VERSION; + // the max length of application id is 24 + public static final String SPRINGBOOT_KEY_VAULT_APPLICATION_ID = + String.join("-", AZURE, SPRING, KEY_VAULT) + "/" + SPRINGBOOT_VERSION; + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessor.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessor.java new file mode 100644 index 000000000000..94d2381e7cc6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessor.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.config.ConfigFileApplicationListener; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.ClassUtils; + +public class KeyVaultEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + public static final int DEFAULT_ORDER = ConfigFileApplicationListener.DEFAULT_ORDER + 1; + private int order = DEFAULT_ORDER; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (isKeyVaultEnabled(environment)) { + final KeyVaultEnvironmentPostProcessorHelper helper = + new KeyVaultEnvironmentPostProcessorHelper(environment); + helper.addKeyVaultPropertySource(); + } + } + + private boolean isKeyVaultEnabled(ConfigurableEnvironment environment) { + if (environment.getProperty(Constants.AZURE_KEYVAULT_VAULT_URI) == null) { + // User doesn't want to enable Key Vault property initializer. + return false; + } + return environment.getProperty(Constants.AZURE_KEYVAULT_ENABLED, Boolean.class, true) + && isKeyVaultClientAvailable(); + } + + private boolean isKeyVaultClientAvailable() { + return ClassUtils.isPresent("com.azure.security.keyvault.secrets.SecretClient", + KeyVaultEnvironmentPostProcessor.class.getClassLoader()); + } + + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorHelper.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorHelper.java new file mode 100644 index 000000000000..ebde3cf4b174 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorHelper.java @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.policy.HttpLogOptions; +import com.azure.identity.ClientCertificateCredentialBuilder; +import com.azure.identity.ClientSecretCredential; +import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.identity.ManagedIdentityCredentialBuilder; +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.SecretClientBuilder; +import com.microsoft.azure.telemetry.TelemetrySender; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.microsoft.azure.keyvault.spring.Constants.SPRINGBOOT_KEY_VAULT_APPLICATION_ID; +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; + +class KeyVaultEnvironmentPostProcessorHelper { + + private static final Logger LOGGER = LoggerFactory.getLogger(KeyVaultEnvironmentPostProcessorHelper.class); + + private final ConfigurableEnvironment environment; + + KeyVaultEnvironmentPostProcessorHelper(final ConfigurableEnvironment environment) { + this.environment = environment; + // As @PostConstructor not available when post processor, call it explicitly. + sendTelemetry(); + } + + public void addKeyVaultPropertySource() { + final String vaultUri = getProperty(this.environment, Constants.AZURE_KEYVAULT_VAULT_URI); + final Long refreshInterval = Optional.ofNullable( + this.environment.getProperty(Constants.AZURE_KEYVAULT_REFRESH_INTERVAL)) + .map(Long::valueOf).orElse(Constants.DEFAULT_REFRESH_INTERVAL_MS); + final Binder binder = Binder.get(this.environment); + final List secretKeys = binder.bind(Constants.AZURE_KEYVAULT_SECRET_KEYS, Bindable.listOf(String.class)) + .orElse(Collections.emptyList()); + + final TokenCredential tokenCredential = getCredentials(); + final SecretClient secretClient = new SecretClientBuilder() + .vaultUrl(vaultUri) + .credential(tokenCredential) + .httpLogOptions(new HttpLogOptions().setApplicationId(SPRINGBOOT_KEY_VAULT_APPLICATION_ID)) + .buildClient(); + try { + final MutablePropertySources sources = this.environment.getPropertySources(); + final KeyVaultOperation kvOperation = new KeyVaultOperation(secretClient, + vaultUri, + refreshInterval, + secretKeys); + + if (sources.contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) { + sources.addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + new KeyVaultPropertySource(kvOperation)); + } else { + sources.addFirst(new KeyVaultPropertySource(kvOperation)); + } + + } catch (final Exception ex) { + throw new IllegalStateException("Failed to configure KeyVault property source", ex); + } + } + + public TokenCredential getCredentials() { + //use service principle to authenticate + if (this.environment.containsProperty(Constants.AZURE_KEYVAULT_CLIENT_ID) + && this.environment.containsProperty(Constants.AZURE_KEYVAULT_CLIENT_KEY) + && this.environment.containsProperty(Constants.AZURE_KEYVAULT_TENANT_ID)) { + LOGGER.debug("Will use custom credentials"); + final String clientId = getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_ID); + final String clientKey = getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_KEY); + final String tenantId = getProperty(this.environment, Constants.AZURE_KEYVAULT_TENANT_ID); + final ClientSecretCredential clientSecretCredential = new ClientSecretCredentialBuilder() + .clientId(clientId) + .clientSecret(clientKey) + .tenantId(tenantId) + .build(); + return clientSecretCredential; + } + //use certificate to authenticate + if (this.environment.containsProperty(Constants.AZURE_KEYVAULT_CLIENT_ID) + && this.environment.containsProperty(Constants.AZURE_KEYVAULT_CERTIFICATE_PATH) + && this.environment.containsProperty(Constants.AZURE_KEYVAULT_TENANT_ID)) { + // Password can be empty + final String certPwd = this.environment.getProperty(Constants.AZURE_KEYVAULT_CERTIFICATE_PASSWORD); + final String certPath = getProperty(this.environment, Constants.AZURE_KEYVAULT_CERTIFICATE_PATH); + + if (StringUtils.isEmpty(certPwd)) { + return new ClientCertificateCredentialBuilder() + .tenantId(getProperty(this.environment, Constants.AZURE_KEYVAULT_TENANT_ID)) + .clientId(getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_ID)) + .pemCertificate(certPath) + .build(); + } else { + return new ClientCertificateCredentialBuilder() + .tenantId(getProperty(this.environment, Constants.AZURE_KEYVAULT_TENANT_ID)) + .clientId(getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_ID)) + .pfxCertificate(certPath, certPwd) + .build(); + } + } + //use MSI to authenticate + if (this.environment.containsProperty(Constants.AZURE_KEYVAULT_CLIENT_ID)) { + LOGGER.debug("Will use MSI credentials with specified clientId"); + final String clientId = getProperty(this.environment, Constants.AZURE_KEYVAULT_CLIENT_ID); + return new ManagedIdentityCredentialBuilder().clientId(clientId).build(); + } + LOGGER.debug("Will use MSI credentials"); + return new ManagedIdentityCredentialBuilder().build(); + } + + private String getProperty(final ConfigurableEnvironment env, final String propertyName) { + Assert.notNull(env, "env must not be null!"); + Assert.notNull(propertyName, "propertyName must not be null!"); + final String property = env.getProperty(propertyName); + if (property == null || property.isEmpty()) { + throw new IllegalArgumentException("property " + propertyName + " must not be null"); + } + return property; + } + + private boolean allowTelemetry(final ConfigurableEnvironment env) { + Assert.notNull(env, "env must not be null!"); + return env.getProperty(Constants.AZURE_KEYVAULT_ALLOW_TELEMETRY, Boolean.class, true); + } + + private void sendTelemetry() { + if (allowTelemetry(environment)) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(KeyVaultEnvironmentPostProcessorHelper.class)); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java new file mode 100644 index 000000000000..f2107dd3f1ea --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultOperation.java @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.azure.core.http.rest.PagedIterable; +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.azure.security.keyvault.secrets.models.SecretProperties; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class KeyVaultOperation { + private final long cacheRefreshIntervalInMs; + private final List secretKeys; + + private final Object refreshLock = new Object(); + private final SecretClient keyVaultClient; + private final String vaultUri; + + private ArrayList propertyNames = new ArrayList<>(); + private String[] propertyNamesArr; + + private final AtomicLong lastUpdateTime = new AtomicLong(); + private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); + + public KeyVaultOperation(final SecretClient keyVaultClient, + String vaultUri, + final long refreshInterval, + final List secretKeys) { + this.cacheRefreshIntervalInMs = refreshInterval; + this.secretKeys = secretKeys; + this.keyVaultClient = keyVaultClient; + // TODO(pan): need to validate why last '/' need to be truncated. + this.vaultUri = StringUtils.trimTrailingCharacter(vaultUri.trim(), '/'); + fillSecretsList(); + } + + public String[] list() { + try { + this.rwLock.readLock().lock(); + return propertyNamesArr; + } finally { + this.rwLock.readLock().unlock(); + } + } + + private String getKeyvaultSecretName(@NonNull String property) { + if (property.matches("[a-z0-9A-Z-]+")) { + return property.toLowerCase(Locale.US); + } else if (property.matches("[A-Z0-9_]+")) { + return property.toLowerCase(Locale.US).replaceAll("_", "-"); + } else { + return property.toLowerCase(Locale.US) + .replaceAll("-", "") // my-project -> myproject + .replaceAll("_", "") // my_project -> myproject + .replaceAll("\\.", "-"); // acme.myproject -> acme-myproject + } + } + + /** + * For convention we need to support all relaxed binding format from spring, these may include: + *
    + *
  • Spring relaxed binding names
  • + *
  • acme.my-project.person.first-name
  • + *
  • acme.myProject.person.firstName
  • + *
  • acme.my_project.person.first_name
  • + *
  • ACME_MYPROJECT_PERSON_FIRSTNAME
  • + *
+ * But azure keyvault only allows ^[0-9a-zA-Z-]+$ and case insensitive, so there must be some conversion + * between spring names and azure keyvault names. + * For example, the 4 properties stated above should be convert to acme-myproject-person-firstname in keyvault. + * + * @param property of secret instance. + * @return the value of secret with given name or null. + */ + public String get(final String property) { + Assert.hasText(property, "property should contain text."); + final String secretName = getKeyvaultSecretName(property); + + //if user don't set specific secret keys, then refresh token + if (this.secretKeys == null || secretKeys.size() == 0) { + // refresh periodically + refreshPropertyNames(); + } + if (this.propertyNames.contains(secretName)) { + final KeyVaultSecret secret = this.keyVaultClient.getSecret(secretName); + return secret == null ? null : secret.getValue(); + } else { + return null; + } + } + + private void refreshPropertyNames() { + if (System.currentTimeMillis() - this.lastUpdateTime.get() > this.cacheRefreshIntervalInMs) { + synchronized (this.refreshLock) { + if (System.currentTimeMillis() - this.lastUpdateTime.get() > this.cacheRefreshIntervalInMs) { + this.lastUpdateTime.set(System.currentTimeMillis()); + fillSecretsList(); + } + } + } + } + + private void fillSecretsList() { + try { + this.rwLock.writeLock().lock(); + if (this.secretKeys == null || secretKeys.size() == 0) { + this.propertyNames.clear(); + + final PagedIterable secretProperties = keyVaultClient.listPropertiesOfSecrets(); + secretProperties.forEach(s -> { + final String secretName = s.getName().replace(vaultUri + "/secrets/", ""); + addSecretIfNotExist(secretName); + }); + + this.lastUpdateTime.set(System.currentTimeMillis()); + } else { + for (final String secretKey : secretKeys) { + addSecretIfNotExist(secretKey); + } + } + propertyNamesArr = propertyNames.toArray(new String[0]); + } finally { + this.rwLock.writeLock().unlock(); + } + } + + private void addSecretIfNotExist(final String secretName) { + final String secretNameLowerCase = secretName.toLowerCase(Locale.US); + if (!propertyNames.contains(secretNameLowerCase)) { + propertyNames.add(secretNameLowerCase); + } + final String secretNameSeparatedByDot = secretNameLowerCase.replaceAll("-", "."); + if (!propertyNames.contains(secretNameSeparatedByDot)) { + propertyNames.add(secretNameSeparatedByDot); + } + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySource.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySource.java new file mode 100644 index 000000000000..82ebf72e2661 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySource.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import org.springframework.core.env.EnumerablePropertySource; + +public class KeyVaultPropertySource extends EnumerablePropertySource { + + private final KeyVaultOperation operations; + + public KeyVaultPropertySource(KeyVaultOperation operation) { + super(Constants.AZURE_KEYVAULT_PROPERTYSOURCE_NAME, operation); + this.operations = operation; + } + + + public String[] getPropertyNames() { + return this.operations.list(); + } + + + public Object getProperty(String name) { + return operations.get(name); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java new file mode 100644 index 000000000000..77cfa8be0a0e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleStatelessAuthenticationFilter.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.proc.BadJWTException; +import net.minidev.json.JSONArray; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.text.ParseException; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.springframework.util.StringUtils.hasText; + +public class AADAppRoleStatelessAuthenticationFilter extends OncePerRequestFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADAppRoleStatelessAuthenticationFilter.class); + private static final String TOKEN_TYPE = "Bearer "; + private static final JSONArray DEFAULT_ROLE_CLAIM = new JSONArray().appendElement("USER"); + private static final String ROLE_PREFIX = "ROLE_"; + + private final UserPrincipalManager principalManager; + + public AADAppRoleStatelessAuthenticationFilter(UserPrincipalManager principalManager) { + this.principalManager = principalManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + boolean cleanupRequired = false; + + if (hasText(authHeader) && authHeader.startsWith(TOKEN_TYPE)) { + try { + final String token = authHeader.replace(TOKEN_TYPE, ""); + final UserPrincipal principal = principalManager.buildUserPrincipal(token); + final JSONArray roles = Optional.ofNullable((JSONArray) principal.getClaims().get("roles")) + .filter(r -> !r.isEmpty()) + .orElse(DEFAULT_ROLE_CLAIM); + final Authentication authentication = new PreAuthenticatedAuthenticationToken( + principal, null, rolesToGrantedAuthorities(roles)); + authentication.setAuthenticated(true); + LOGGER.info("Request token verification success. {}", authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + cleanupRequired = true; + } catch (BadJWTException ex) { + final String errorMessage = "Invalid JWT. Either expired or not yet valid. " + ex.getMessage(); + LOGGER.warn(errorMessage); + throw new ServletException(errorMessage, ex); + } catch (ParseException | BadJOSEException | JOSEException ex) { + LOGGER.error("Failed to initialize UserPrincipal.", ex); + throw new ServletException(ex); + } + } + + try { + filterChain.doFilter(request, response); + } finally { + if (cleanupRequired) { + //Clear context after execution + SecurityContextHolder.clearContext(); + } + } + } + + protected Set rolesToGrantedAuthorities(JSONArray roles) { + return roles.stream() + .filter(Objects::nonNull) + .map(s -> new SimpleGrantedAuthority(ROLE_PREFIX + s)) + .collect(Collectors.toSet()); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java new file mode 100644 index 000000000000..3311b7fb36a2 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.microsoft.aad.msal4j.MsalServiceException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.savedrequest.DefaultSavedRequest; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class AADAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private AuthenticationFailureHandler defaultHandler; + + public AADAuthenticationFailureHandler() { + this.defaultHandler = new SimpleUrlAuthenticationFailureHandler(AADConstantsHelper.FAILURE_DEFUALT_URL); + } + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + final OAuth2AuthenticationException targetException = (OAuth2AuthenticationException) exception; + //handle conditional access policy + if (AADConstantsHelper.CONDITIONAL_ACCESS_POLICY.equals((targetException.getError().getErrorCode()))) { + //get infos + final Throwable cause = targetException.getCause(); + if (cause instanceof MsalServiceException) { + final MsalServiceException e = (MsalServiceException) cause; + final String claims = e.claims(); + + final DefaultSavedRequest savedRequest = (DefaultSavedRequest) request.getSession() + .getAttribute(AADConstantsHelper.SAVED_REQUEST); + final String savedRequestUrl = savedRequest.getRedirectUrl(); + //put claims into session + request.getSession().setAttribute(AADConstantsHelper.CAP_CLAIMS, claims); + //redirect + response.setStatus(302); + response.sendRedirect(savedRequestUrl); + return; + } + } + //default handle logic + defaultHandler.onAuthenticationFailure(request, response, exception); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java new file mode 100644 index 000000000000..fd19b8c11b26 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.microsoft.aad.msal4j.MsalServiceException; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.source.JWKSetCache; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.util.ResourceRetriever; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.naming.ServiceUnavailableException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.MalformedURLException; +import java.text.ParseException; + +public class AADAuthenticationFilter extends OncePerRequestFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationFilter.class); + + private static final String CURRENT_USER_PRINCIPAL = "CURRENT_USER_PRINCIPAL"; + private static final String CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN = "CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN"; + private static final String CURRENT_USER_PRINCIPAL_JWT_TOKEN = "CURRENT_USER_PRINCIPAL_JWT_TOKEN"; + + private static final String TOKEN_HEADER = "Authorization"; + private static final String TOKEN_TYPE = "Bearer "; + + private AADAuthenticationProperties aadAuthProps; + private ServiceEndpointsProperties serviceEndpointsProps; + private UserPrincipalManager principalManager; + + public AADAuthenticationFilter(AADAuthenticationProperties aadAuthProps, + ServiceEndpointsProperties serviceEndpointsProps, + ResourceRetriever resourceRetriever) { + this.aadAuthProps = aadAuthProps; + this.serviceEndpointsProps = serviceEndpointsProps; + this.principalManager = new UserPrincipalManager(serviceEndpointsProps, aadAuthProps, resourceRetriever, false); + } + + public AADAuthenticationFilter(AADAuthenticationProperties aadAuthProps, + ServiceEndpointsProperties serviceEndpointsProps, + ResourceRetriever resourceRetriever, + JWKSetCache jwkSetCache) { + this.aadAuthProps = aadAuthProps; + this.serviceEndpointsProps = serviceEndpointsProps; + this.principalManager = new UserPrincipalManager(serviceEndpointsProps, + aadAuthProps, + resourceRetriever, + false, + jwkSetCache); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + final String authHeader = request.getHeader(TOKEN_HEADER); + + if (authHeader != null && authHeader.startsWith(TOKEN_TYPE)) { + try { + final String idToken = authHeader.replace(TOKEN_TYPE, ""); + UserPrincipal principal = (UserPrincipal) request + .getSession().getAttribute(CURRENT_USER_PRINCIPAL); + String graphApiToken = (String) request + .getSession().getAttribute(CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN); + final String currentToken = (String) request + .getSession().getAttribute(CURRENT_USER_PRINCIPAL_JWT_TOKEN); + + final AzureADGraphClient client = new AzureADGraphClient(aadAuthProps.getClientId(), + aadAuthProps.getClientSecret(), aadAuthProps, serviceEndpointsProps); + + if (principal == null + || graphApiToken == null + || graphApiToken.isEmpty() + || !idToken.equals(currentToken)) { + principal = principalManager.buildUserPrincipal(idToken); + + final String tenantId = principal.getClaim().toString(); + graphApiToken = client.acquireTokenForGraphApi(idToken, tenantId).accessToken(); + + principal.setUserGroups(client.getGroups(graphApiToken)); + + request.getSession().setAttribute(CURRENT_USER_PRINCIPAL, principal); + request.getSession().setAttribute(CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN, graphApiToken); + request.getSession().setAttribute(CURRENT_USER_PRINCIPAL_JWT_TOKEN, idToken); + } + + final Authentication authentication = new PreAuthenticatedAuthenticationToken( + principal, null, client.convertGroupsToGrantedAuthorities(principal.getUserGroups())); + + authentication.setAuthenticated(true); + LOGGER.info("Request token verification success. {}", authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (MalformedURLException | ParseException | BadJOSEException | JOSEException ex) { + LOGGER.error("Failed to initialize UserPrincipal.", ex); + throw new ServletException(ex); + } catch (ServiceUnavailableException ex) { + LOGGER.error("Failed to acquire graph api token.", ex); + throw new ServletException(ex); + } catch (MsalServiceException ex) { + if (ex.claims() != null && !ex.claims().isEmpty()) { + throw new ServletException("Handle conditional access policy", ex); + } else { + throw ex; + } + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java new file mode 100644 index 000000000000..c5b0f2bfc1bc --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; + +import com.microsoft.azure.telemetry.TelemetrySender; +import com.nimbusds.jose.jwk.source.DefaultJWKSetCache; +import com.nimbusds.jose.jwk.source.JWKSetCache; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.ResourceRetriever; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.util.ClassUtils; + +@Configuration +@ConditionalOnWebApplication +@ConditionalOnResource(resources = "classpath:aad.enable.config") +@ConditionalOnProperty(prefix = AADAuthenticationFilterAutoConfiguration.PROPERTY_PREFIX, value = {"client-id"}) +@EnableConfigurationProperties({AADAuthenticationProperties.class, ServiceEndpointsProperties.class}) +@PropertySource(value = "classpath:serviceEndpoints.properties") +public class AADAuthenticationFilterAutoConfiguration { + private static final Logger LOG = LoggerFactory.getLogger(AADAuthenticationProperties.class); + + public static final String PROPERTY_PREFIX = "azure.activedirectory"; + private static final String PROPERTY_SESSION_STATELESS = "session-stateless"; + + private final AADAuthenticationProperties aadAuthProps; + + private final ServiceEndpointsProperties serviceEndpointsProps; + + public AADAuthenticationFilterAutoConfiguration(AADAuthenticationProperties aadAuthFilterProps, + ServiceEndpointsProperties serviceEndpointsProps) { + this.aadAuthProps = aadAuthFilterProps; + this.serviceEndpointsProps = serviceEndpointsProps; + } + + /** + * Declare AADAuthenticationFilter bean. + * + * @return AADAuthenticationFilter bean + */ + @Bean + @ConditionalOnMissingBean(AADAuthenticationFilter.class) + @ConditionalOnProperty(prefix = PROPERTY_PREFIX, value = {"client-id", "client-secret"}) + @ConditionalOnExpression("${azure.activedirectory.session-stateless:false} == false") + public AADAuthenticationFilter azureADJwtTokenFilter() { + LOG.info("AzureADJwtTokenFilter Constructor."); + return new AADAuthenticationFilter(aadAuthProps, + serviceEndpointsProps, + getJWTResourceRetriever(), + getJWKSetCache()); + } + + @Bean + @ConditionalOnMissingBean(AADAppRoleStatelessAuthenticationFilter.class) + @ConditionalOnProperty(prefix = PROPERTY_PREFIX, value = PROPERTY_SESSION_STATELESS, havingValue = "true") + public AADAppRoleStatelessAuthenticationFilter azureADStatelessAuthFilter(ResourceRetriever resourceRetriever) { + LOG.info("Creating AzureADStatelessAuthFilter bean."); + final boolean useExplicitAudienceCheck = true; + return new AADAppRoleStatelessAuthenticationFilter(new UserPrincipalManager(serviceEndpointsProps, aadAuthProps, + resourceRetriever, useExplicitAudienceCheck)); + } + + @Bean + @ConditionalOnMissingBean(ResourceRetriever.class) + public ResourceRetriever getJWTResourceRetriever() { + return new DefaultResourceRetriever(aadAuthProps.getJwtConnectTimeout(), aadAuthProps.getJwtReadTimeout(), + aadAuthProps.getJwtSizeLimit()); + } + + @Bean + @ConditionalOnMissingBean(JWKSetCache.class) + public JWKSetCache getJWKSetCache() { + return new DefaultJWKSetCache(aadAuthProps.getJwkSetCacheLifespan(), TimeUnit.MILLISECONDS); + } + + @PostConstruct + private void sendTelemetry() { + if (aadAuthProps.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(AADAuthenticationFilterAutoConfiguration.class)); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java new file mode 100644 index 000000000000..99d2b90a0818 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotEmpty; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Validated +@ConfigurationProperties("azure.activedirectory") +public class AADAuthenticationProperties { + + private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationProperties.class); + + private static final String DEFAULT_SERVICE_ENVIRONMENT = "global"; + + private static final long DEFAULT_JWKSETCACHE_LIFESPAN = TimeUnit.MINUTES.toMillis(5); + + /** + * Default UserGroup configuration. + */ + private UserGroupProperties userGroup = new UserGroupProperties(); + + + /** + * Azure service environment/region name, e.g., cn, global + */ + private String environment = DEFAULT_SERVICE_ENVIRONMENT; + /** + * Registered application ID in Azure AD. + * Must be configured when OAuth2 authentication is done in front end + */ + private String clientId; + /** + * API Access Key of the registered application. + * Must be configured when OAuth2 authentication is done in front end + */ + private String clientSecret; + + /** + * Azure AD groups. + */ + private List activeDirectoryGroups = new ArrayList<>(); + + /** + * App ID URI which might be used in the "aud" claim of an id_token. + */ + private String appIdUri; + + /** + * Connection Timeout for the JWKSet Remote URL call. + */ + private int jwtConnectTimeout = RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT; /* milliseconds */ + + /** + * Read Timeout for the JWKSet Remote URL call. + */ + private int jwtReadTimeout = RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT; /* milliseconds */ + + /** + * Size limit in Bytes of the JWKSet Remote URL call. + */ + private int jwtSizeLimit = RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */ + + /** + * The lifespan of the cached JWK set before it expires, default is 5 minutes. + */ + private long jwkSetCacheLifespan = DEFAULT_JWKSETCACHE_LIFESPAN; + + /** + * Azure Tenant ID. + */ + private String tenantId; + + /** + * If Telemetry events should be published to Azure AD. + */ + private boolean allowTelemetry = true; + + /** + * If true activates the stateless auth filter {@link AADAppRoleStatelessAuthenticationFilter}. + * The default is false which activates {@link AADAuthenticationFilter}. + */ + private Boolean sessionStateless = false; + + @DeprecatedConfigurationProperty(reason = "Configuration moved to UserGroup class to keep UserGroup properties " + + "together", replacement = "azure.activedirectory.user-group.allowed-groups") + public List getActiveDirectoryGroups() { + return activeDirectoryGroups; + } + /** + * Properties dedicated to changing the behavior of how the groups are mapped from the Azure AD response. Depending + * on the graph API used the object will not be the same. + */ + public static class UserGroupProperties { + + /** + * Expected UserGroups that an authority will be granted to if found in the response from the MemeberOf Graph + * API Call. + */ + private List allowedGroups = new ArrayList<>(); + + /** + * Key of the JSON Node to get from the Azure AD response object that will be checked to contain the {@code + * azure.activedirectory.user-group.value} to signify that this node is a valid {@code UserGroup}. + */ + @NotEmpty + private String key = "objectType"; + + /** + * Value of the JSON Node identified by the {@code azure.activedirectory.user-group.key} to validate the JSON + * Node is a UserGroup. + */ + @NotEmpty + private String value = "Group"; + + /** + * Key of the JSON Node containing the Azure Object ID for the {@code UserGroup}. + */ + @NotEmpty + private String objectIDKey = "objectId"; + + public List getAllowedGroups() { + return allowedGroups; + } + + public void setAllowedGroups(List allowedGroups) { + this.allowedGroups = allowedGroups; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getObjectIDKey() { + return objectIDKey; + } + + public void setObjectIDKey(String objectIDKey) { + this.objectIDKey = objectIDKey; + } + + @Override + public String toString() { + return "UserGroupProperties{" + + "allowedGroups=" + allowedGroups + + ", key='" + key + '\'' + + ", value='" + value + '\'' + + ", objectIDKey='" + objectIDKey + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserGroupProperties that = (UserGroupProperties) o; + return Objects.equals(allowedGroups, that.allowedGroups) + && Objects.equals(key, that.key) + && Objects.equals(value, that.value) + && Objects.equals(objectIDKey, that.objectIDKey); + } + + @Override + public int hashCode() { + return Objects.hash(allowedGroups, key, value, objectIDKey); + } + } + + + /** + * Validates at least one of the user group properties are populated. + */ + @PostConstruct + public void validateUserGroupProperties() { + if (this.sessionStateless) { + if (!this.activeDirectoryGroups.isEmpty()) { + LOGGER.warn("Group names are not supported if you set 'sessionSateless' to 'true'."); + } + } else if (this.activeDirectoryGroups.isEmpty() && this.getUserGroup().getAllowedGroups().isEmpty()) { + throw new IllegalStateException("One of the User Group Properties must be populated. " + + "Please populate azure.activedirectory.user-group.allowed-groups"); + } + } + + public UserGroupProperties getUserGroup() { + return userGroup; + } + + public void setUserGroup(UserGroupProperties userGroup) { + this.userGroup = userGroup; + } + + public String getEnvironment() { + return environment; + } + + public void setEnvironment(String environment) { + this.environment = environment; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public void setActiveDirectoryGroups(List activeDirectoryGroups) { + this.activeDirectoryGroups = activeDirectoryGroups; + } + + public String getAppIdUri() { + return appIdUri; + } + + public void setAppIdUri(String appIdUri) { + this.appIdUri = appIdUri; + } + + public int getJwtConnectTimeout() { + return jwtConnectTimeout; + } + + public void setJwtConnectTimeout(int jwtConnectTimeout) { + this.jwtConnectTimeout = jwtConnectTimeout; + } + + public int getJwtReadTimeout() { + return jwtReadTimeout; + } + + public void setJwtReadTimeout(int jwtReadTimeout) { + this.jwtReadTimeout = jwtReadTimeout; + } + + public int getJwtSizeLimit() { + return jwtSizeLimit; + } + + public void setJwtSizeLimit(int jwtSizeLimit) { + this.jwtSizeLimit = jwtSizeLimit; + } + + public long getJwkSetCacheLifespan() { + return jwkSetCacheLifespan; + } + + public void setJwkSetCacheLifespan(long jwkSetCacheLifespan) { + this.jwkSetCacheLifespan = jwkSetCacheLifespan; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } + + public Boolean getSessionStateless() { + return sessionStateless; + } + + public void setSessionStateless(Boolean sessionStateless) { + this.sessionStateless = sessionStateless; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java new file mode 100644 index 000000000000..fa700b0e9916 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +public class AADConstantsHelper { + public static final String CONDITIONAL_ACCESS_POLICY = "conditional_access_policy"; + public static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"; + public static final String CAP_CLAIMS = "CAP_Claims"; + public static final String CLAIMS = "claims"; + public static final String FAILURE_DEFUALT_URL = "/login?error"; +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java new file mode 100644 index 000000000000..4427e497dab4 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.util.StringUtils; + +public class AADOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + private OAuth2AuthorizationRequestResolver defaultResolver; + + public AADOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) { + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + return addClaims(request, defaultResolver.resolve(request)); + } + + @Override + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { + return addClaims(request, defaultResolver.resolve(request, clientRegistrationId)); + } + + //add claims to authorization-url + private OAuth2AuthorizationRequest addClaims(HttpServletRequest request, + OAuth2AuthorizationRequest req) { + if (req == null || request == null) { + return req; + } + + final String conditionalAccessPolicyClaims = getConditionalAccessPolicyClaims(request); + if (StringUtils.isEmpty(conditionalAccessPolicyClaims)) { + return req; + } + + final Map extraParams = new HashMap<>(); + if (req.getAdditionalParameters() != null) { + extraParams.putAll(req.getAdditionalParameters()); + } + extraParams.put(AADConstantsHelper.CLAIMS, conditionalAccessPolicyClaims); + return OAuth2AuthorizationRequest + .from(req) + .additionalParameters(extraParams) + .build(); + } + + private String getConditionalAccessPolicyClaims(HttpServletRequest request) { + //claims just for one use + final String claims = request.getSession() + .getAttribute(AADConstantsHelper.CAP_CLAIMS) == null ? "" : (String) request + .getSession() + .getAttribute(AADConstantsHelper.CAP_CLAIMS); + //remove claims in session + if (!StringUtils.isEmpty(claims)) { + request.getSession().removeAttribute(AADConstantsHelper.CAP_CLAIMS); + } + return claims; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java new file mode 100644 index 000000000000..da3a2bf9b8f6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.microsoft.azure.telemetry.TelemetrySender; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.ClassUtils; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; + +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; + +@Configuration +@ConditionalOnResource(resources = "classpath:aad.enable.config") +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnProperty(prefix = "azure.activedirectory", value = "tenant-id") +@PropertySource("classpath:/aad-oauth2-common.properties") +@PropertySource(value = "classpath:serviceEndpoints.properties") +@EnableConfigurationProperties({AADAuthenticationProperties.class, ServiceEndpointsProperties.class}) +public class AADOAuth2AutoConfiguration { + + private final AADAuthenticationProperties aadAuthProps; + + private final ServiceEndpointsProperties serviceEndpointsProps; + + public AADOAuth2AutoConfiguration(AADAuthenticationProperties aadAuthProperties, + ServiceEndpointsProperties serviceEndpointsProps) { + this.aadAuthProps = aadAuthProperties; + this.serviceEndpointsProps = serviceEndpointsProps; + } + + @Bean + @ConditionalOnProperty(prefix = "azure.activedirectory", value = "active-directory-groups") + public OAuth2UserService oidcUserService() { + return new AADOAuth2UserService(aadAuthProps, serviceEndpointsProps); + } + + @PostConstruct + private void sendTelemetry() { + if (aadAuthProps.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(AADOAuth2AutoConfiguration.class)); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java new file mode 100644 index 000000000000..2c6dd0541339 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.microsoft.aad.msal4j.MsalServiceException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.StringUtils; + +import javax.naming.ServiceUnavailableException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Set; + +public class AADOAuth2UserService implements OAuth2UserService { + private static final String CONDITIONAL_ACCESS_POLICY = "conditional_access_policy"; + private static final String INVALID_REQUEST = "invalid_request"; + private static final String SERVER_ERROR = "server_error"; + private static final String DEFAULT_USERNAME_ATTR_NAME = "name"; + + private AADAuthenticationProperties aadAuthProps; + private ServiceEndpointsProperties serviceEndpointsProps; + + public AADOAuth2UserService(AADAuthenticationProperties aadAuthProps, + ServiceEndpointsProperties serviceEndpointsProps) { + this.aadAuthProps = aadAuthProps; + this.serviceEndpointsProps = serviceEndpointsProps; + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + final OidcUserService delegate = new OidcUserService(); + + // Delegate to the default implementation for loading a user + OidcUser oidcUser = delegate.loadUser(userRequest); + final OidcIdToken idToken = userRequest.getIdToken(); + + final String graphApiToken; + final Set mappedAuthorities; + + try { + // https://github.com/MicrosoftDocs/azure-docs/issues/8121#issuecomment-387090099 + // In AAD App Registration configure oauth2AllowImplicitFlow to true + final ClientRegistration registration = userRequest.getClientRegistration(); + + final AzureADGraphClient graphClient = new AzureADGraphClient(registration.getClientId(), + registration.getClientSecret(), aadAuthProps, serviceEndpointsProps); + + graphApiToken = graphClient.acquireTokenForGraphApi(idToken.getTokenValue(), + aadAuthProps.getTenantId()).accessToken(); + + mappedAuthorities = graphClient.getGrantedAuthorities(graphApiToken); + } catch (MalformedURLException e) { + throw wrapException(INVALID_REQUEST, "Failed to acquire token for Graph API.", null, e); + } catch (ServiceUnavailableException e) { + throw wrapException(SERVER_ERROR, "Failed to acquire token for Graph API.", null, e); + } catch (IOException e) { + throw wrapException(SERVER_ERROR, "Failed to map group to authorities.", null, e); + } catch (MsalServiceException e) { + if (e.claims() != null && !e.claims().isEmpty()) { + throw wrapException(CONDITIONAL_ACCESS_POLICY, "Handle conditional access policy", null, e); + } else { + throw e; + } + } + + // Create a copy of oidcUser but use the mappedAuthorities instead + oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), getUserNameAttrName(userRequest)); + + return oidcUser; + } + + private OAuth2AuthenticationException wrapException(String errorCode, String errDesc, String uri, Exception e) { + final OAuth2Error oAuth2Error = new OAuth2Error(errorCode, errDesc, uri); + throw new OAuth2AuthenticationException(oAuth2Error, e); + } + + private String getUserNameAttrName(OAuth2UserRequest userRequest) { + String userNameAttrName = userRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + if (StringUtils.isEmpty(userNameAttrName)) { + userNameAttrName = DEFAULT_USERNAME_ATTR_NAME; + } + + return userNameAttrName; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java new file mode 100644 index 000000000000..b207c04cf0d9 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IClientCredential; +import com.microsoft.aad.msal4j.MsalServiceException; +import com.microsoft.aad.msal4j.OnBehalfOfParameters; +import com.microsoft.aad.msal4j.UserAssertion; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import javax.naming.ServiceUnavailableException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class AzureADGraphClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(AzureADGraphClient.class); + private static final SimpleGrantedAuthority DEFAULT_AUTHORITY = new SimpleGrantedAuthority("ROLE_USER"); + private static final String DEFAULT_ROLE_PREFIX = "ROLE_"; + private static final String MICROSOFT_GRAPH_SCOPE = "https://graph.microsoft.com/user.read"; + private static final String AAD_GRAPH_API_SCOPE = "https://graph.windows.net/user.read"; + + private final String clientId; + private final String clientSecret; + private final ServiceEndpoints serviceEndpoints; + private final AADAuthenticationProperties aadAuthenticationProperties; + + private static final String V2_VERSION_ENV_FLAG = "v2-graph"; + private boolean aadMicrosoftGraphApiBool; + + public AzureADGraphClient(String clientId, String clientSecret, AADAuthenticationProperties aadAuthProps, + ServiceEndpointsProperties serviceEndpointsProps) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.aadAuthenticationProperties = aadAuthProps; + this.serviceEndpoints = serviceEndpointsProps.getServiceEndpoints(aadAuthProps.getEnvironment()); + + this.initAADMicrosoftGraphApiBool(aadAuthProps.getEnvironment()); + } + + private void initAADMicrosoftGraphApiBool(String endpointEnv) { + this.aadMicrosoftGraphApiBool = endpointEnv.contains(V2_VERSION_ENV_FLAG); + } + + private String getUserMembershipsV1(String accessToken) throws IOException { + final URL url = new URL(serviceEndpoints.getAadMembershipRestUri()); + final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + // Set the appropriate header fields in the request header. + + if (this.aadMicrosoftGraphApiBool) { + conn.setRequestMethod(HttpMethod.GET.toString()); + conn.setRequestProperty(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", accessToken)); + conn.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + conn.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } else { + conn.setRequestMethod(HttpMethod.GET.toString()); + conn.setRequestProperty("api-version", "1.6"); + conn.setRequestProperty(HttpHeaders.AUTHORIZATION, accessToken); + conn.setRequestProperty(HttpHeaders.ACCEPT, "application/json;odata=minimalmetadata"); + } + final String responseInJson = getResponseStringFromConn(conn); + final int responseCode = conn.getResponseCode(); + if (responseCode == HTTPResponse.SC_OK) { + return responseInJson; + } else { + throw new IllegalStateException("Response is not " + + HTTPResponse.SC_OK + ", response json: " + responseInJson); + } + } + + private static String getResponseStringFromConn(HttpURLConnection conn) throws IOException { + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + final StringBuilder stringBuffer = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuffer.append(line); + } + return stringBuffer.toString(); + } + } + + public List getGroups(String graphApiToken) throws IOException { + return loadUserGroups(graphApiToken); + } + + private List loadUserGroups(String graphApiToken) throws IOException { + final String responseInJson = getUserMembershipsV1(graphApiToken); + final List lUserGroups = new ArrayList<>(); + final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance(); + final JsonNode rootNode = objectMapper.readValue(responseInJson, JsonNode.class); + final JsonNode valuesNode = rootNode.get("value"); + + if (valuesNode != null) { + lUserGroups + .addAll(StreamSupport.stream(valuesNode.spliterator(), false).filter(this::isMatchingUserGroupKey) + .map(node -> { + final String objectID = node. + get(aadAuthenticationProperties.getUserGroup().getObjectIDKey()).asText(); + final String displayName = node.get("displayName").asText(); + return new UserGroup(objectID, displayName); + }).collect(Collectors.toList())); + + } + + return lUserGroups; + } + + /** + * Checks that the JSON Node is a valid User Group to extract User Groups from + * + * @param node - json node to look for a key/value to equate against the + * {@link AADAuthenticationProperties.UserGroupProperties} + * @return true if the json node contains the correct key, and expected value to identify a user group. + */ + private boolean isMatchingUserGroupKey(final JsonNode node) { + return node.get(aadAuthenticationProperties.getUserGroup().getKey()).asText() + .equals(aadAuthenticationProperties.getUserGroup().getValue()); + } + + public Set getGrantedAuthorities(String graphApiToken) throws IOException { + // Fetch the authority information from the protected resource using accessToken + final List groups = getGroups(graphApiToken); + + // Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities + return convertGroupsToGrantedAuthorities(groups); + } + + + /** + * Converts UserGroup list to Set of GrantedAuthorities + * + * @param groups user groups + * @return granted authorities + */ + public Set convertGroupsToGrantedAuthorities(final List groups) { + // Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities + final Set mappedAuthorities = groups.stream().filter(this::isValidUserGroupToGrantAuthority) + .map(userGroup -> new SimpleGrantedAuthority(DEFAULT_ROLE_PREFIX + userGroup.getDisplayName())) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (mappedAuthorities.isEmpty()) { + mappedAuthorities.add(DEFAULT_AUTHORITY); + } + + return mappedAuthorities; + } + + /** + * Determines if this is a valid {@link UserGroup} to build to a GrantedAuthority. + *

+ * If the {@link AADAuthenticationProperties.UserGroupProperties#getAllowedGroups()} or the {@link + * AADAuthenticationProperties#getActiveDirectoryGroups()} contains the {@link UserGroup#getDisplayName()} return + * true. + * + * @param group - User Group to check if valid to grant an authority to. + * @return true if either of the allowed-groups or active-directory-groups contains the UserGroup display name + */ + private boolean isValidUserGroupToGrantAuthority(final UserGroup group) { + return aadAuthenticationProperties.getUserGroup().getAllowedGroups().contains(group.getDisplayName()) + || aadAuthenticationProperties.getActiveDirectoryGroups().contains(group.getDisplayName()); + } + + public IAuthenticationResult acquireTokenForGraphApi(String idToken, String tenantId) + throws ServiceUnavailableException { + final IClientCredential clientCredential = ClientCredentialFactory.createFromSecret(clientSecret); + final UserAssertion assertion = new UserAssertion(idToken); + + IAuthenticationResult result = null; + ExecutorService service = null; + try { + service = Executors.newFixedThreadPool(1); + + final ConfidentialClientApplication application = ConfidentialClientApplication + .builder(clientId, clientCredential) + .authority(serviceEndpoints.getAadSigninUri() + tenantId + "/") + .build(); + + final Set scopes = new HashSet<>(); + scopes.add(aadMicrosoftGraphApiBool ? MICROSOFT_GRAPH_SCOPE : AAD_GRAPH_API_SCOPE); + + final OnBehalfOfParameters onBehalfOfParameters = OnBehalfOfParameters + .builder(scopes, assertion) + .build(); + + final CompletableFuture future = application.acquireToken(onBehalfOfParameters); + result = future.get(); + } catch (ExecutionException | InterruptedException | MalformedURLException e) { + // handle conditional access policy + final Throwable cause = e.getCause(); + if (cause instanceof MsalServiceException) { + final MsalServiceException exception = (MsalServiceException) cause; + if (exception.claims() != null && !exception.claims().isEmpty()) { + throw exception; + } + } + LOGGER.error("acquire on behalf of token for graph api error", e); + } finally { + if (service != null) { + service.shutdown(); + } + } + + if (result == null) { + throw new ServiceUnavailableException("unable to acquire on-behalf-of token for client " + clientId); + } + return result; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java new file mode 100644 index 000000000000..477a72b98f55 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public final class JacksonObjectMapperFactory { + + private JacksonObjectMapperFactory() { + } + + public static ObjectMapper getInstance() { + return SingletonHelper.INSTANCE; + } + + private static class SingletonHelper { + private static final ObjectMapper INSTANCE = new ObjectMapper(); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java new file mode 100644 index 000000000000..39e3ee43dda1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + + +/** + * Pojo file to store the service urls for different azure services. + */ +public class ServiceEndpoints { + private String aadSigninUri; + private String aadGraphApiUri; + private String aadKeyDiscoveryUri; + private String aadMembershipRestUri; + + public String getAadSigninUri() { + return aadSigninUri; + } + + public void setAadSigninUri(String aadSigninUri) { + this.aadSigninUri = aadSigninUri; + } + + public String getAadGraphApiUri() { + return aadGraphApiUri; + } + + public void setAadGraphApiUri(String aadGraphApiUri) { + this.aadGraphApiUri = aadGraphApiUri; + } + + public String getAadKeyDiscoveryUri() { + return aadKeyDiscoveryUri; + } + + public void setAadKeyDiscoveryUri(String aadKeyDiscoveryUri) { + this.aadKeyDiscoveryUri = aadKeyDiscoveryUri; + } + + public String getAadMembershipRestUri() { + return aadMembershipRestUri; + } + + public void setAadMembershipRestUri(String aadMembershipRestUri) { + this.aadMembershipRestUri = aadMembershipRestUri; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java new file mode 100644 index 000000000000..76f28891e398 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@ConfigurationProperties("azure.service") +public class ServiceEndpointsProperties { + private Map endpoints = new HashMap<>(); + + public Map getEndpoints() { + return endpoints; + } + + /** + * Get ServiceEndpoints data for the given environment. + * + * @param environment the environment of the cloud service + * @return The ServiceEndpoints data for the given azure service environment + */ + public ServiceEndpoints getServiceEndpoints(String environment) { + Assert.notEmpty(endpoints, "No service endpoints found"); + + if (!endpoints.containsKey(environment)) { + throw new IllegalArgumentException(environment + " is not found in the configuration," + + " only following environments are supported: " + endpoints.keySet()); + } + + return endpoints.get(environment); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java new file mode 100644 index 000000000000..5d9163be261d --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import java.io.Serializable; +import java.util.Objects; + +public class UserGroup implements Serializable { + private static final long serialVersionUID = 9064197572478554735L; + + private String objectID; + private String displayName; + + public UserGroup(String objectID, String displayName) { + this.objectID = objectID; + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + public String getObjectID() { + return objectID; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || !(o instanceof UserGroup)) { + return false; + } + final UserGroup group = (UserGroup) o; + return this.getDisplayName().equals(group.getDisplayName()) + && this.getObjectID().equals(group.getObjectID()); + } + + @Override + public int hashCode() { + return Objects.hash(objectID, displayName); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java new file mode 100644 index 000000000000..85b1894da61f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class UserPrincipal implements Serializable { + private static final long serialVersionUID = -3725690847771476854L; + + private JWSObject jwsObject; + private JWTClaimsSet jwtClaimsSet; + private List userGroups = new ArrayList<>(); + + public UserPrincipal(JWSObject jwsObject, JWTClaimsSet jwtClaimsSet) { + this.jwsObject = jwsObject; + this.jwtClaimsSet = jwtClaimsSet; + } + + // claimset + public String getIssuer() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getIssuer(); + } + + public String getSubject() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getSubject(); + } + + public Map getClaims() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaims(); + } + + public Object getClaim() { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim("tid"); + } + + public Object getClaim(String name) { + return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim(name); + } + + public String getUpn() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("upn"); + } + + public String getUniqueName() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("unique_name"); + } + + public String getName() { + return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("name"); + } + + // header + public String getKid() { + return jwsObject == null ? null : jwsObject.getHeader().getKeyID(); + } + + public void setUserGroups(List groups) { + this.userGroups = groups; + } + + public List getUserGroups() { + return this.userGroups; + } + + public boolean isMemberOf(UserGroup group) { + return !(userGroups == null || userGroups.isEmpty()) && userGroups.contains(group); + } +} + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java new file mode 100644 index 000000000000..6f62b7bd326e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.jwk.source.JWKSetCache; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.util.ResourceRetriever; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.BadJWTException; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import com.nimbusds.jwt.proc.JWTClaimsSetVerifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.MalformedURLException; +import java.net.URL; +import java.text.ParseException; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +public class UserPrincipalManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserPrincipalManager.class); + + private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/"; + private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/"; + private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/"; + + private final JWKSource keySource; + private final AADAuthenticationProperties aadAuthProps; + private final Boolean explicitAudienceCheck; + private final Set validAudiences = new HashSet<>(); + + /** + * Creates a new {@link UserPrincipalManager} with a predefined {@link JWKSource}. + *

+ * This is helpful in cases the JWK is not a remote JWKSet or for unit testing. + * + * @param keySource - {@link JWKSource} containing at least one key + */ + public UserPrincipalManager(JWKSource keySource) { + this.keySource = keySource; + this.explicitAudienceCheck = false; + this.aadAuthProps = null; + } + + /** + * Create a new {@link UserPrincipalManager} based of the {@link ServiceEndpoints#getAadKeyDiscoveryUri()} and + * {@link AADAuthenticationProperties#getEnvironment()}. + * + * @param serviceEndpointsProps - used to retrieve the JWKS URL + * @param aadAuthProps - used to retrieve the environment. + * @param resourceRetriever - configures the {@link RemoteJWKSet} call. + * @param explicitAudienceCheck - explicit audience check + */ + public UserPrincipalManager(ServiceEndpointsProperties serviceEndpointsProps, + AADAuthenticationProperties aadAuthProps, + ResourceRetriever resourceRetriever, + boolean explicitAudienceCheck) { + this.aadAuthProps = aadAuthProps; + this.explicitAudienceCheck = explicitAudienceCheck; + if (explicitAudienceCheck) { + // client-id for "normal" check + this.validAudiences.add(this.aadAuthProps.getClientId()); + // app id uri for client credentials flow (server to server communication) + this.validAudiences.add(this.aadAuthProps.getAppIdUri()); + } + try { + keySource = new RemoteJWKSet<>(new URL(serviceEndpointsProps + .getServiceEndpoints(aadAuthProps.getEnvironment()).getAadKeyDiscoveryUri()), resourceRetriever); + } catch (MalformedURLException e) { + LOGGER.error("Failed to parse active directory key discovery uri.", e); + throw new IllegalStateException("Failed to parse active directory key discovery uri.", e); + } + } + + /** + * Create a new {@link UserPrincipalManager} based of the {@link ServiceEndpoints#getAadKeyDiscoveryUri()} and + * {@link AADAuthenticationProperties#getEnvironment()}. + * + * @param serviceEndpointsProps - used to retrieve the JWKS URL + * @param aadAuthProps - used to retrieve the environment. + * @param resourceRetriever - configures the {@link RemoteJWKSet} call. + * @param jwkSetCache - used to cache the JWK set for a finite time, default set to 5 minutes + * which matches constructor above if no jwkSetCache is passed in + * @param explicitAudienceCheck - explicit audience check + */ + public UserPrincipalManager(ServiceEndpointsProperties serviceEndpointsProps, + AADAuthenticationProperties aadAuthProps, + ResourceRetriever resourceRetriever, + boolean explicitAudienceCheck, + JWKSetCache jwkSetCache) { + this.aadAuthProps = aadAuthProps; + this.explicitAudienceCheck = explicitAudienceCheck; + if (explicitAudienceCheck) { + // client-id for "normal" check + this.validAudiences.add(this.aadAuthProps.getClientId()); + // app id uri for client credentials flow (server to server communication) + this.validAudiences.add(this.aadAuthProps.getAppIdUri()); + } + try { + keySource = new RemoteJWKSet<>(new URL(serviceEndpointsProps + .getServiceEndpoints(aadAuthProps.getEnvironment()).getAadKeyDiscoveryUri()), + resourceRetriever, + jwkSetCache); + } catch (MalformedURLException e) { + LOGGER.error("Failed to parse active directory key discovery uri.", e); + throw new IllegalStateException("Failed to parse active directory key discovery uri.", e); + } + } + + public UserPrincipal buildUserPrincipal(String idToken) throws ParseException, JOSEException, BadJOSEException { + final JWSObject jwsObject = JWSObject.parse(idToken); + final ConfigurableJWTProcessor validator = + getAadJwtTokenValidator(jwsObject.getHeader().getAlgorithm()); + final JWTClaimsSet jwtClaimsSet = validator.process(idToken, null); + final JWTClaimsSetVerifier verifier = validator.getJWTClaimsSetVerifier(); + verifier.verify(jwtClaimsSet, null); + + return new UserPrincipal(jwsObject, jwtClaimsSet); + } + + private ConfigurableJWTProcessor getAadJwtTokenValidator(JWSAlgorithm jwsAlgorithm) { + final ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + + final JWSKeySelector keySelector = + new JWSVerificationKeySelector<>(jwsAlgorithm, keySource); + jwtProcessor.setJWSKeySelector(keySelector); + + //TODO: would it make sense to inject it? and make it configurable or even allow to provide own implementation + jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier() { + @Override + public void verify(JWTClaimsSet claimsSet, SecurityContext ctx) throws BadJWTException { + super.verify(claimsSet, ctx); + final String issuer = claimsSet.getIssuer(); + if (issuer == null || !(issuer.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER) + || issuer.startsWith(STS_WINDOWS_ISSUER) + || issuer.startsWith(STS_CHINA_CLOUD_API_ISSUER))) { + throw new BadJWTException("Invalid token issuer"); + } + if (explicitAudienceCheck) { + final Optional matchedAudience = claimsSet.getAudience().stream() + .filter(UserPrincipalManager.this.validAudiences::contains).findFirst(); + if (matchedAudience.isPresent()) { + LOGGER.debug("Matched audience [{}]", matchedAudience.get()); + } else { + throw new BadJWTException("Invalid token audience. Provided value " + claimsSet.getAudience() + + "does not match neither client-id nor AppIdUri."); + } + } + } + }); + return jwtProcessor; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolver.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolver.java new file mode 100644 index 000000000000..7f413832cb0e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolver.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +public class AADB2CAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + + private static final String REQUEST_BASE_URI = + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + + private static final String REGISTRATION_ID_NAME = "registrationId"; + + private static final String PARAMETER_X_CLIENT_SKU = "x-client-SKU"; + + private static final String AAD_B2C_USER_AGENT = "spring-boot-starter"; + + private static final String MATCHER_PATTERN = String.format("%s/{%s}", REQUEST_BASE_URI, REGISTRATION_ID_NAME); + + private static final AntPathRequestMatcher REQUEST_MATCHER = new AntPathRequestMatcher(MATCHER_PATTERN); + + private final OAuth2AuthorizationRequestResolver defaultResolver; + + private final String passwordResetUserFlow; + + public AADB2CAuthorizationRequestResolver(@NonNull ClientRegistrationRepository repository) { + this.passwordResetUserFlow = null; + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repository, REQUEST_BASE_URI); + } + + public AADB2CAuthorizationRequestResolver(@NonNull ClientRegistrationRepository repository, + @Nullable String passwordResetUserFlow) { + this.passwordResetUserFlow = passwordResetUserFlow; + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repository, REQUEST_BASE_URI); + } + + @Override + public OAuth2AuthorizationRequest resolve(@NonNull HttpServletRequest request) { + return resolve(request, getRegistrationId(request)); + } + + @Override + public OAuth2AuthorizationRequest resolve(@NonNull HttpServletRequest request, String registrationId) { + if (StringUtils.hasText(passwordResetUserFlow) && isForgotPasswordAuthorizationRequest(request)) { + final OAuth2AuthorizationRequest authRequest = defaultResolver.resolve(request, passwordResetUserFlow); + return getB2CAuthorizationRequest(authRequest, passwordResetUserFlow); + } + + if (StringUtils.hasText(registrationId) && REQUEST_MATCHER.matches(request)) { + return getB2CAuthorizationRequest(defaultResolver.resolve(request), registrationId); + } + + // Return null may not be the good practice, but we need to align with oauth2.client.web + // DefaultOAuth2AuthorizationRequestResolver. + return null; + } + + private void cleanupSecurityContextAuthentication() { + SecurityContextHolder.getContext().setAuthentication(null); + } + + private OAuth2AuthorizationRequest getB2CAuthorizationRequest(@Nullable OAuth2AuthorizationRequest request, + String userFlow) { + Assert.hasText(userFlow, "User flow should contain text."); + + if (request == null) { + return null; + } + + cleanupSecurityContextAuthentication(); + + final Map parameters = new HashMap<>(request.getAdditionalParameters()); + + parameters.put("p", userFlow); + parameters.put(PARAMETER_X_CLIENT_SKU, AAD_B2C_USER_AGENT); + + return OAuth2AuthorizationRequest.from(request).additionalParameters(parameters).build(); + } + + private String getRegistrationId(HttpServletRequest request) { + if (REQUEST_MATCHER.matches(request)) { + return REQUEST_MATCHER.extractUriTemplateVariables(request).get(REGISTRATION_ID_NAME); + } + + return null; + } + + // Handle the forgot password of sign-up-or-in page cannot redirect user to password-reset page. + // The B2C service will enhance that, and then related code will be removed. + private boolean isForgotPasswordAuthorizationRequest(@NonNull HttpServletRequest request) { + final String error = request.getParameter("error"); + final String description = request.getParameter("error_description"); + + if ("access_denied".equals(error)) { + Assert.hasText(description, "description should contain text."); + return description.startsWith("AADB2C90118:"); + } + + return false; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfiguration.java new file mode 100644 index 000000000000..002d0e565eff --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfiguration.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import com.microsoft.azure.telemetry.TelemetrySender; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CProperties.PREFIX; +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CProperties.USER_FLOW_SIGN_UP_OR_SIGN_IN; +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.TENANT_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; + +@Configuration +@ConditionalOnWebApplication +@ConditionalOnResource(resources = "classpath:aadb2c.enable.config") +@ConditionalOnProperty( + prefix = PREFIX, + value = { + "tenant", + "client-id", + "client-secret", + "reply-url", + USER_FLOW_SIGN_UP_OR_SIGN_IN + } +) +@EnableConfigurationProperties(AADB2CProperties.class) +public class AADB2CAutoConfiguration { + + private final ClientRegistrationRepository repository; + + private final AADB2CProperties properties; + + public AADB2CAutoConfiguration(@NonNull ClientRegistrationRepository repository, + @NonNull AADB2CProperties properties) { + this.repository = repository; + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + public AADB2CAuthorizationRequestResolver b2cOAuth2AuthorizationRequestResolver() { + return new AADB2CAuthorizationRequestResolver(repository, properties.getUserFlows().getPasswordReset()); + } + + @Bean + @ConditionalOnMissingBean + public AADB2CLogoutSuccessHandler b2cLogoutSuccessHandler() { + return new AADB2CLogoutSuccessHandler(properties); + } + + @Bean + @ConditionalOnMissingBean + public AADB2COidcLoginConfigurer b2cLoginConfigurer(AADB2CLogoutSuccessHandler handler, + AADB2CAuthorizationRequestResolver resolver) { + return new AADB2COidcLoginConfigurer(properties, handler, resolver); + } + + @PostConstruct + private void sendTelemetry() { + if (properties.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(AADB2CAutoConfiguration.class)); + events.put(TENANT_NAME, properties.getTenant()); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } + + @Configuration + @ConditionalOnResource(resources = "classpath:aadb2c.enable.config") + @ConditionalOnProperty(prefix = PREFIX, value = "oidc-enabled", havingValue = "true", matchIfMissing = true) + public static class AADB2COidcAutoConfiguration { + + private final AADB2CProperties properties; + + public AADB2COidcAutoConfiguration(@NonNull AADB2CProperties properties) { + this.properties = properties; + } + + private void addB2CClientRegistration(@NonNull List registrations, String userFlow) { + if (StringUtils.hasText(userFlow)) { + registrations.add(b2cClientRegistration(userFlow)); + } + } + + @Bean + @ConditionalOnMissingBean + public ClientRegistrationRepository clientRegistrationRepository() { + final List registrations = new ArrayList<>(); + + addB2CClientRegistration(registrations, properties.getUserFlows().getSignUpOrSignIn()); + addB2CClientRegistration(registrations, properties.getUserFlows().getProfileEdit()); + addB2CClientRegistration(registrations, properties.getUserFlows().getPasswordReset()); + + return new InMemoryClientRegistrationRepository(registrations); + } + + private ClientRegistration b2cClientRegistration(String userFlow) { + Assert.hasText(userFlow, "User flow should contains text."); + + return ClientRegistration.withRegistrationId(userFlow) // Use flow as registration Id. + .clientId(properties.getClientId()) + .clientSecret(properties.getClientSecret()) + .clientAuthenticationMethod(ClientAuthenticationMethod.POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(properties.getReplyUrl()) + .scope(properties.getClientId(), "openid") + .authorizationUri(AADB2CURL.getAuthorizationUrl(properties.getTenant())) + .tokenUri(AADB2CURL.getTokenUrl(properties.getTenant(), userFlow)) + .jwkSetUri(AADB2CURL.getJwkSetUrl(properties.getTenant(), userFlow)) + .userNameAttributeName("name") + .clientName(userFlow) + .build(); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConfigurationException.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConfigurationException.java new file mode 100644 index 000000000000..974e48ac964f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConfigurationException.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +public class AADB2CConfigurationException extends RuntimeException { + + public AADB2CConfigurationException(String message) { + super(message); + } + + public AADB2CConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandler.java new file mode 100644 index 000000000000..194c6ee9d39c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandler.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.springframework.lang.NonNull; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class AADB2CLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { + + private final AADB2CProperties properties; + + public AADB2CLogoutSuccessHandler(@NonNull AADB2CProperties properties) { + this.properties = properties; + + super.setDefaultTargetUrl(getAADB2CEndSessionUrl()); + } + + private String getAADB2CEndSessionUrl() { + final String userFlow = properties.getUserFlows().getSignUpOrSignIn(); + final String logoutSuccessUrl = properties.getLogoutSuccessUrl(); + + return AADB2CURL.getEndSessionUrl(properties.getTenant(), logoutSuccessUrl, userFlow); + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + super.onLogoutSuccess(request, response, authentication); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2COidcLoginConfigurer.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2COidcLoginConfigurer.java new file mode 100644 index 000000000000..4af8bd08c272 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2COidcLoginConfigurer.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + +public class AADB2COidcLoginConfigurer extends AbstractHttpConfigurer { + + private final AADB2CProperties properties; + + private final AADB2CLogoutSuccessHandler handler; + + private final AADB2CAuthorizationRequestResolver resolver; + + public AADB2COidcLoginConfigurer(AADB2CProperties properties, + AADB2CLogoutSuccessHandler handler, AADB2CAuthorizationRequestResolver resolver) { + this.properties = properties; + this.handler = handler; + this.resolver = resolver; + } + + @Override + public void init(HttpSecurity http) throws Exception { + http.logout() + .logoutSuccessHandler(handler) + .and() + .oauth2Login() + .loginProcessingUrl(properties.getLoginProcessingUrl()) + .authorizationEndpoint().authorizationRequestResolver(resolver); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CProperties.java new file mode 100644 index 000000000000..9423789e24ec --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CProperties.java @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.hibernate.validator.constraints.URL; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.lang.NonNull; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; +import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotBlank; +import java.net.MalformedURLException; + +@Validated +@ConfigurationProperties(prefix = AADB2CProperties.PREFIX) +public class AADB2CProperties { + + private static final String USER_FLOWS = "user-flows"; + + /** + * We do not use ${@link String#format(String, Object...)} + * as it's not real constant, which cannot be referenced in annotation. + */ + public static final String USER_FLOW_PASSWORD_RESET = USER_FLOWS + ".password-reset"; + + public static final String USER_FLOW_PROFILE_EDIT = USER_FLOWS + ".profile-edit"; + + public static final String USER_FLOW_SIGN_UP_OR_SIGN_IN = USER_FLOWS + ".sign-up-or-sign-in"; + + public static final String DEFAULT_LOGOUT_SUCCESS_URL = "http://localhost:8080/login"; + + public static final String PREFIX = "azure.activedirectory.b2c"; + + /** + * The name of the b2c tenant. + */ + @NotBlank(message = "tenant name should not be blank") + private String tenant; + + /** + * Use OIDC ${@link OidcAuthorizationCodeAuthenticationProvider} by default. If set to false, + * will use Oauth2 ${@link OAuth2AuthorizationCodeAuthenticationProvider}. + */ + private Boolean oidcEnabled = true; + + /** + * The application ID that registered under b2c tenant. + */ + @NotBlank(message = "client ID should not be blank") + private String clientId; + + /** + * The application secret that registered under b2c tenant. + */ + @NotBlank(message = "client secret should not be blank") + private String clientSecret; + + @URL(message = "reply URL should be valid URL") + private String replyUrl; + + @URL(message = "logout success should be valid URL") + private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL; + + /** + * The all user flows which is created under b2c tenant. + */ + private UserFlows userFlows = new UserFlows(); + + /** + * Telemetry data will be collected if true, or disable data collection. + */ + private boolean allowTelemetry = true; + + private String getReplyURLPath(@URL String replyURL) { + try { + return new java.net.URL(replyURL).getPath(); + } catch (MalformedURLException e) { + throw new AADB2CConfigurationException("Failed to get path of given URL.", e); + } + } + + @NonNull + public String getLoginProcessingUrl() { + return getReplyURLPath(replyUrl); + } + + @Validated + protected static class UserFlows { + + protected UserFlows() { + + } + + /** + * The sign-up-or-sign-in user flow which is created under b2c tenant. + */ + @NotBlank(message = "sign-up-or-in value should not be blank") + private String signUpOrSignIn; + + /** + * The profile-edit user flow which is created under b2c tenant. + */ + private String profileEdit; + + /** + * The password-reset user flow which is created under b2c tenant. + */ + private String passwordReset; + + public String getSignUpOrSignIn() { + return signUpOrSignIn; + } + + public void setSignUpOrSignIn(String signUpOrSignIn) { + this.signUpOrSignIn = signUpOrSignIn; + } + + public String getProfileEdit() { + return profileEdit; + } + + public void setProfileEdit(String profileEdit) { + this.profileEdit = profileEdit; + } + + public String getPasswordReset() { + return passwordReset; + } + + public void setPasswordReset(String passwordReset) { + this.passwordReset = passwordReset; + } + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public Boolean getOidcEnabled() { + return oidcEnabled; + } + + public void setOidcEnabled(Boolean oidcEnabled) { + this.oidcEnabled = oidcEnabled; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getReplyUrl() { + return replyUrl; + } + + public void setReplyUrl(String replyUrl) { + this.replyUrl = replyUrl; + } + + public String getLogoutSuccessUrl() { + return logoutSuccessUrl; + } + + public void setLogoutSuccessUrl(String logoutSuccessUrl) { + this.logoutSuccessUrl = logoutSuccessUrl; + } + + public UserFlows getUserFlows() { + return userFlows; + } + + public void setUserFlows(UserFlows userFlows) { + this.userFlows = userFlows; + } + + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURL.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURL.java new file mode 100644 index 000000000000..0f1bd87824c6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURL.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.springframework.util.Assert; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +public final class AADB2CURL { + + private AADB2CURL() { + + } + + private static final String AUTHORIZATION_URL_PATTERN = + "https://%s.b2clogin.com/%s.onmicrosoft.com/oauth2/v2.0/authorize"; + + private static final String TOKEN_URL_PATTERN = + "https://%s.b2clogin.com/%s.onmicrosoft.com/oauth2/v2.0/token?p=%s"; + + private static final String JWKSET_URL_PATTERN = + "https://%s.b2clogin.com/%s.onmicrosoft.com/discovery/v2.0/keys?p=%s"; + + private static final String END_SESSION_URL_PATTERN = + "https://%s.b2clogin.com/%s.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=%s&p=%s"; + + public static String getAuthorizationUrl(String tenant) { + Assert.hasText(tenant, "tenant should have text."); + + return String.format(AUTHORIZATION_URL_PATTERN, tenant, tenant); + } + + public static String getTokenUrl(String tenant, String userFlow) { + Assert.hasText(tenant, "tenant should have text."); + Assert.hasText(userFlow, "user flow should have text."); + + return String.format(TOKEN_URL_PATTERN, tenant, tenant, userFlow); + } + + public static String getJwkSetUrl(String tenant, String userFlow) { + Assert.hasText(tenant, "tenant should have text."); + Assert.hasText(userFlow, "user flow should have text."); + + return String.format(JWKSET_URL_PATTERN, tenant, tenant, userFlow); + } + + public static String getEndSessionUrl(String tenant, String logoutUrl, String userFlow) { + Assert.hasText(tenant, "tenant should have text."); + Assert.hasText(logoutUrl, "logoutUrl should have text."); + Assert.hasText(userFlow, "user flow should have text."); + + return String.format(END_SESSION_URL_PATTERN, tenant, tenant, getEncodedURL(logoutUrl), userFlow); + } + + private static String getEncodedURL(String url) { + Assert.hasText(url, "url should have text."); + + try { + return URLEncoder.encode(url, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new AADB2CConfigurationException("failed to encode url: " + url, e); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosAutoConfiguration.java new file mode 100644 index 000000000000..8656f20d1b01 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosAutoConfiguration.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import com.azure.data.cosmos.ConnectionPolicy; +import com.azure.data.cosmos.CosmosClient; +import com.microsoft.azure.spring.data.cosmosdb.config.AbstractCosmosConfiguration; +import com.microsoft.azure.spring.data.cosmosdb.config.CosmosDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({ CosmosClient.class, CosmosTemplate.class }) +@ConditionalOnResource(resources = "classpath:cosmosdb.enable.config") +@EnableConfigurationProperties(CosmosDBProperties.class) +public class CosmosAutoConfiguration extends AbstractCosmosConfiguration { + private final CosmosDBProperties properties; + private final ConnectionPolicy policy; + + public CosmosAutoConfiguration(CosmosDBProperties properties, + ObjectProvider connectionPolicyObjectProvider) { + this.properties = properties; + this.policy = connectionPolicyObjectProvider.getIfAvailable(); + configConnectionPolicy(properties, policy); + } + + @Bean + public CosmosDBConfig cosmosDBConfig() { + + return CosmosDBConfig.builder( + properties.getUri(), properties.getKey(), properties.getDatabase()) + .consistencyLevel(properties.getConsistencyLevel()) + .allowTelemetry(properties.isAllowTelemetry()) + .connectionPolicy(properties.getConnectionPolicy()) + .responseDiagnosticsProcessor(properties.getResponseDiagnosticsProcessor()) + .populateQueryMetrics(properties.isPopulateQueryMetrics()) + .build(); + } + + private void configConnectionPolicy(CosmosDBProperties properties, ConnectionPolicy connectionPolicy) { + // This is a temp fix as CosmosDbFactory does not support loading ConnectionPolicy bean from context + final ConnectionPolicy policy = connectionPolicy == null ? ConnectionPolicy.defaultPolicy() : connectionPolicy; + + properties.setConnectionPolicy(policy); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDBProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDBProperties.java new file mode 100644 index 000000000000..7d4ff1a510a3 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDBProperties.java @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import com.azure.data.cosmos.ConnectionPolicy; +import com.azure.data.cosmos.ConsistencyLevel; +import com.microsoft.azure.spring.data.cosmosdb.core.ResponseDiagnosticsProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@Validated +@ConfigurationProperties("azure.cosmosdb") +public class CosmosDBProperties { + + private static final Logger LOGGER = LoggerFactory.getLogger(CosmosDBProperties.class); + /** + * Document DB URI. + */ + @NotEmpty + private String uri; + + /** + * Document DB key. + */ + @NotEmpty + private String key; + + /** + * Document DB consistency level. + */ + private ConsistencyLevel consistencyLevel; + + /** + * Document DB database name. + */ + @NotEmpty + private String database; + + /** + * Populate Diagnostics Strings and Query metrics + */ + private boolean populateQueryMetrics; + + /** + * Whether allow Microsoft to collect telemetry data. + */ + private boolean allowTelemetry = true; + + /** + * Response Diagnostics processor + * Default implementation is to log the response diagnostics string + */ + private ResponseDiagnosticsProcessor responseDiagnosticsProcessor = + responseDiagnostics -> { + if (populateQueryMetrics) { + LOGGER.info("Response Diagnostics {}", responseDiagnostics); + } + }; + + private ConnectionPolicy connectionPolicy = ConnectionPolicy.defaultPolicy(); + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String databaseName) { + this.database = databaseName; + } + + public ConsistencyLevel getConsistencyLevel() { + return consistencyLevel; + } + + public void setConsistencyLevel(ConsistencyLevel consistencyLevel) { + this.consistencyLevel = consistencyLevel; + } + + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } + + public ConnectionPolicy getConnectionPolicy() { + return connectionPolicy; + } + + public void setConnectionPolicy(ConnectionPolicy connectionPolicy) { + this.connectionPolicy = connectionPolicy; + } + + public boolean isPopulateQueryMetrics() { + return populateQueryMetrics; + } + + public void setPopulateQueryMetrics(boolean populateQueryMetrics) { + this.populateQueryMetrics = populateQueryMetrics; + } + + public ResponseDiagnosticsProcessor getResponseDiagnosticsProcessor() { + return responseDiagnosticsProcessor; + } + + public void setResponseDiagnosticsProcessor(ResponseDiagnosticsProcessor responseDiagnosticsProcessor) { + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbReactiveRepositoriesAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbReactiveRepositoriesAutoConfiguration.java new file mode 100644 index 000000000000..4717310c8689 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbReactiveRepositoriesAutoConfiguration.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import com.microsoft.azure.spring.data.cosmosdb.repository.ReactiveCosmosRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.config.ReactiveCosmosRepositoryConfigurationExtension; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.ReactiveCosmosRepositoryFactoryBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + + +@Configuration +@ConditionalOnClass({ ReactiveCosmosRepository.class }) +@ConditionalOnMissingBean({ ReactiveCosmosRepositoryFactoryBean.class, + ReactiveCosmosRepositoryConfigurationExtension.class }) +@ConditionalOnProperty(prefix = "azure.cosmosdb.repositories", + name = "enabled", + havingValue = "true", + matchIfMissing = true) +@Import(CosmosDbReactiveRepositoriesAutoConfigureRegistrar.class) +public class CosmosDbReactiveRepositoriesAutoConfiguration { +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbReactiveRepositoriesAutoConfigureRegistrar.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbReactiveRepositoriesAutoConfigureRegistrar.java new file mode 100644 index 000000000000..82bc09f93847 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbReactiveRepositoriesAutoConfigureRegistrar.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import com.microsoft.azure.spring.data.cosmosdb.repository.config.EnableReactiveCosmosRepositories; +import com.microsoft.azure.spring.data.cosmosdb.repository.config.ReactiveCosmosRepositoryConfigurationExtension; +import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +import java.lang.annotation.Annotation; + +public class CosmosDbReactiveRepositoriesAutoConfigureRegistrar extends AbstractRepositoryConfigurationSourceSupport { + @Override + protected Class getAnnotation() { + return EnableReactiveCosmosRepositories.class; + } + + @Override + protected Class getConfiguration() { + return EnableCosmosDbReactiveRepositoriesConfiguration.class; + } + + @Override + protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { + return new ReactiveCosmosRepositoryConfigurationExtension(); + } + + @EnableReactiveCosmosRepositories + private static class EnableCosmosDbReactiveRepositoriesConfiguration { + + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfiguration.java new file mode 100644 index 000000000000..6b6c675dc80d --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfiguration.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import com.microsoft.azure.spring.data.cosmosdb.repository.config.CosmosRepositoryConfigurationExtension; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.CosmosRepositoryFactoryBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + + +@Configuration +@ConditionalOnClass({ CosmosRepository.class }) +@ConditionalOnMissingBean({ CosmosRepositoryFactoryBean.class, + CosmosRepositoryConfigurationExtension.class }) +@ConditionalOnProperty(prefix = "azure.cosmosdb.repositories", + name = "enabled", + havingValue = "true", + matchIfMissing = true) +@Import(CosmosDbRepositoriesAutoConfigureRegistrar.class) +public class CosmosDbRepositoriesAutoConfiguration { +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfigureRegistrar.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfigureRegistrar.java new file mode 100644 index 000000000000..ca9aa125e0da --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfigureRegistrar.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import com.microsoft.azure.spring.data.cosmosdb.repository.config.CosmosRepositoryConfigurationExtension; +import com.microsoft.azure.spring.data.cosmosdb.repository.config.EnableCosmosRepositories; +import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +import java.lang.annotation.Annotation; + +public class CosmosDbRepositoriesAutoConfigureRegistrar extends AbstractRepositoryConfigurationSourceSupport { + @Override + protected Class getAnnotation() { + return EnableCosmosRepositories.class; + } + + @Override + protected Class getConfiguration() { + return EnableCosmosDbRepositoriesConfiguration.class; + } + + @Override + protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { + return new CosmosRepositoryConfigurationExtension(); + } + + @EnableCosmosRepositories + private static class EnableCosmosDbRepositoriesConfiguration { + + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinAutoConfiguration.java new file mode 100644 index 000000000000..95c1135d36f8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinAutoConfiguration.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin; + +import com.microsoft.azure.telemetry.TelemetrySender; +import com.microsoft.spring.data.gremlin.common.GremlinConfig; +import com.microsoft.spring.data.gremlin.common.GremlinFactory; +import com.microsoft.spring.data.gremlin.conversion.MappingGremlinConverter; +import com.microsoft.spring.data.gremlin.mapping.GremlinMappingContext; +import com.microsoft.spring.data.gremlin.query.GremlinTemplate; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Persistent; +import org.springframework.lang.NonNull; +import org.springframework.util.ClassUtils; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; + +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; + +@Configuration +@ConditionalOnClass({GremlinFactory.class, GremlinTemplate.class, MappingGremlinConverter.class}) +@ConditionalOnResource(resources = "classpath:gremlin.enable.config") +@ConditionalOnProperty(prefix = "gremlin", value = {"endpoint", "port", "username", "password"}) +@EnableConfigurationProperties(GremlinProperties.class) +public class GremlinAutoConfiguration { + + private final GremlinProperties properties; + + private final ApplicationContext applicationContext; + + public GremlinAutoConfiguration(@NonNull GremlinProperties properties, @NonNull ApplicationContext context) { + this.properties = properties; + this.applicationContext = context; + } + + @PostConstruct + private void sendTelemetry() { + if (properties.isTelemetryAllowed()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(GremlinAutoConfiguration.class)); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } + + @Bean + @ConditionalOnMissingBean + public GremlinConfig getGremlinConfig() { + return GremlinConfig.builder(properties.getEndpoint(), properties.getUsername(), properties.getPassword()) + .port(properties.getPort()) + .sslEnabled(properties.isSslEnabled()) + .telemetryAllowed(properties.isTelemetryAllowed()) + .build(); + } + + @Bean + @ConditionalOnMissingBean + public GremlinFactory gremlinFactory() { + return new GremlinFactory(getGremlinConfig()); + } + + @Bean + @ConditionalOnMissingBean + public GremlinTemplate gremlinTemplate(GremlinFactory factory, MappingGremlinConverter converter) { + return new GremlinTemplate(factory, converter); + } + + @Bean + @ConditionalOnMissingBean + public GremlinMappingContext gremlinMappingContext() { + try { + final GremlinMappingContext context = new GremlinMappingContext(); + + context.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + + return context; + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + } + + @Bean + @ConditionalOnMissingBean + public MappingGremlinConverter mappingGremlinConverter(GremlinMappingContext context) { + return new MappingGremlinConverter(context); + } +} + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinProperties.java new file mode 100644 index 000000000000..8be77c7ab241 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinProperties.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@Validated +@ConfigurationProperties("gremlin") +public class GremlinProperties { + + @NotEmpty + private String endpoint; + + private int port; + + @NotEmpty + private String username; + + @NotEmpty + private String password; + + private boolean telemetryAllowed = true; + + private boolean sslEnabled = true; + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isTelemetryAllowed() { + return telemetryAllowed; + } + + public void setTelemetryAllowed(boolean telemetryAllowed) { + this.telemetryAllowed = telemetryAllowed; + } + + public boolean isSslEnabled() { + return sslEnabled; + } + + public void setSslEnabled(boolean sslEnabled) { + this.sslEnabled = sslEnabled; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfiguration.java new file mode 100644 index 000000000000..a2dae5ed7c0d --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfiguration.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin; + +import com.microsoft.spring.data.gremlin.repository.GremlinRepository; +import com.microsoft.spring.data.gremlin.repository.config.GremlinRepositoryConfigurationExtension; +import com.microsoft.spring.data.gremlin.repository.support.GremlinRepositoryFactoryBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@ConditionalOnClass({GremlinRepository.class}) +@ConditionalOnMissingBean({GremlinRepositoryFactoryBean.class, GremlinRepositoryConfigurationExtension.class}) +@ConditionalOnProperty(prefix = "spring.data.gremlin.repositories", name = "enabled", havingValue = "true", + matchIfMissing = true) +@Import(GremlinRepositoriesAutoConfigureRegistrar.class) +public class GremlinRepositoriesAutoConfiguration { +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfigureRegistrar.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfigureRegistrar.java new file mode 100644 index 000000000000..9397c0dab2b5 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfigureRegistrar.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin; + +import com.microsoft.spring.data.gremlin.repository.config.EnableGremlinRepositories; +import com.microsoft.spring.data.gremlin.repository.config.GremlinRepositoryConfigurationExtension; +import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +import java.lang.annotation.Annotation; + +public class GremlinRepositoriesAutoConfigureRegistrar extends AbstractRepositoryConfigurationSourceSupport { + + @Override + protected Class getAnnotation() { + return EnableGremlinRepositories.class; + } + + @Override + protected Class getConfiguration() { + return EnableGremlinRepositoriesConfiguration.class; + } + + @Override + protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { + return new GremlinRepositoryConfigurationExtension(); + } + + @EnableGremlinRepositories + private static class EnableGremlinRepositoriesConfiguration { + + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/AzureServiceBusJMSProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/AzureServiceBusJMSProperties.java new file mode 100644 index 000000000000..3b86e4ca8bc3 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/AzureServiceBusJMSProperties.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.jms; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; + +@Validated +@ConfigurationProperties("spring.jms.servicebus") +public class AzureServiceBusJMSProperties { + + private String connectionString; + + private String topicClientId; + + private int idleTimeout = 1800000; + + public String getConnectionString() { + return connectionString; + } + + public void setConnectionString(String connectionString) { + this.connectionString = connectionString; + } + + public String getTopicClientId() { + return topicClientId; + } + + public void setTopicClientId(String topicClientId) { + this.topicClientId = topicClientId; + } + + public int getIdleTimeout() { + return idleTimeout; + } + + public void setIdleTimeout(int idleTimeout) { + this.idleTimeout = idleTimeout; + } + + @PostConstruct + public void validate() { + + if (!StringUtils.hasText(connectionString)) { + throw new IllegalArgumentException("'spring.jms.servicebus.connection-string' should be provided"); + } + + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ConnectionStringResolver.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ConnectionStringResolver.java new file mode 100644 index 000000000000..5c8c7b097f9a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ConnectionStringResolver.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.jms; + +import java.util.HashMap; + +public class ConnectionStringResolver { + + private static final String ENDPOINT = "Endpoint"; + private static final String HOST = "host"; + private static final String SAS_KEY_NAME = "SharedAccessKeyName"; + private static final String SAS_KEY = "SharedAccessKey"; + + public static ServiceBusKey getServiceBusKey(String connectionString) { + final String[] segments = connectionString.split(";"); + final HashMap hashMap = new HashMap<>(); + + for (final String segment : segments) { + final int indexOfEqualSign = segment.indexOf("="); + final String key = segment.substring(0, indexOfEqualSign); + final String value = segment.substring(indexOfEqualSign + 1); + hashMap.put(key, value); + } + + final String endpoint = hashMap.get(ENDPOINT); + final String[] segmentsOfEndpoint = endpoint.split("/"); + final String host = segmentsOfEndpoint[segmentsOfEndpoint.length - 1]; + hashMap.put(HOST, host); + + return new ServiceBusKey(hashMap.get(HOST), hashMap.get(SAS_KEY_NAME), hashMap.get(SAS_KEY)); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusJMSAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusJMSAutoConfiguration.java new file mode 100644 index 000000000000..461fd5827c19 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusJMSAutoConfiguration.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.jms; + +import org.apache.qpid.jms.JmsConnectionFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; +import org.springframework.jms.connection.CachingConnectionFactory; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.listener.MessageListenerContainer; + +import javax.jms.ConnectionFactory; + +@Configuration +@ConditionalOnClass(JmsConnectionFactory.class) +@ConditionalOnResource(resources = "classpath:servicebusjms.enable.config") +@ConditionalOnProperty(value = "spring.jms.servicebus.enabled", matchIfMissing = true) +@EnableConfigurationProperties(AzureServiceBusJMSProperties.class) +public class ServiceBusJMSAutoConfiguration { + + private static final String AMQP_URI_FORMAT = "amqps://%s?amqp.idleTimeout=%d"; + + @Bean + @ConditionalOnMissingBean + public ConnectionFactory jmsConnectionFactory(AzureServiceBusJMSProperties serviceBusJMSProperties) { + final String connectionString = serviceBusJMSProperties.getConnectionString(); + final String clientId = serviceBusJMSProperties.getTopicClientId(); + final int idleTimeout = serviceBusJMSProperties.getIdleTimeout(); + + final ServiceBusKey serviceBusKey = ConnectionStringResolver.getServiceBusKey(connectionString); + final String host = serviceBusKey.getHost(); + final String sasKeyName = serviceBusKey.getSharedAccessKeyName(); + final String sasKey = serviceBusKey.getSharedAccessKey(); + + final String remoteUri = String.format(AMQP_URI_FORMAT, host, idleTimeout); + final JmsConnectionFactory jmsConnectionFactory = new JmsConnectionFactory(); + jmsConnectionFactory.setRemoteURI(remoteUri); + jmsConnectionFactory.setClientID(clientId); + jmsConnectionFactory.setUsername(sasKeyName); + jmsConnectionFactory.setPassword(sasKey); + return new CachingConnectionFactory(jmsConnectionFactory); + } + + @Bean + @ConditionalOnMissingBean + public JmsTemplate jmsTemplate(ConnectionFactory jmsConnectionFactory) { + final JmsTemplate jmsTemplate = new JmsTemplate(); + jmsTemplate.setConnectionFactory(jmsConnectionFactory); + return jmsTemplate; + } + + @Bean + @ConditionalOnMissingBean + public JmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory) { + final DefaultJmsListenerContainerFactory jmsListenerContainerFactory = new DefaultJmsListenerContainerFactory(); + jmsListenerContainerFactory.setConnectionFactory(connectionFactory); + return jmsListenerContainerFactory; + } + + @Bean + public JmsListenerContainerFactory topicJmsListenerContainerFactory(ConnectionFactory connectionFactory) { + final DefaultJmsListenerContainerFactory jmsListenerContainerFactory = new DefaultJmsListenerContainerFactory(); + jmsListenerContainerFactory.setConnectionFactory(connectionFactory); + jmsListenerContainerFactory.setSubscriptionDurable(Boolean.TRUE); + return jmsListenerContainerFactory; + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusKey.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusKey.java new file mode 100644 index 000000000000..f5668c3c6027 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusKey.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.jms; + +public class ServiceBusKey { + private final String host; + private final String sharedAccessKeyName; + private final String sharedAccessKey; + + ServiceBusKey(String host, String sharedAccessKeyName, String sharedAccessKey) { + this.host = host; + this.sharedAccessKeyName = sharedAccessKeyName; + this.sharedAccessKey = sharedAccessKey; + } + + public String getHost() { + return host; + } + + public String getSharedAccessKeyName() { + return sharedAccessKeyName; + } + + public String getSharedAccessKey() { + return sharedAccessKey; + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesAutoConfiguration.java new file mode 100644 index 000000000000..545b06a30ce9 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesAutoConfiguration.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.mediaservices; + +import com.microsoft.azure.telemetry.TelemetrySender; +import com.microsoft.windowsazure.exception.ServiceException; +import com.microsoft.windowsazure.services.media.MediaConfiguration; +import com.microsoft.windowsazure.services.media.MediaContract; +import com.microsoft.windowsazure.services.media.MediaService; +import com.microsoft.windowsazure.services.media.authentication.AzureAdClientSymmetricKey; +import com.microsoft.windowsazure.services.media.authentication.AzureAdTokenCredentials; +import com.microsoft.windowsazure.services.media.authentication.AzureAdTokenProvider; +import com.microsoft.windowsazure.services.media.authentication.AzureEnvironments; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; +import static com.microsoft.windowsazure.Configuration.PROPERTY_CONNECT_TIMEOUT; +import static com.microsoft.windowsazure.Configuration.PROPERTY_HTTP_PROXY_HOST; +import static com.microsoft.windowsazure.Configuration.PROPERTY_HTTP_PROXY_PORT; +import static com.microsoft.windowsazure.Configuration.PROPERTY_HTTP_PROXY_SCHEME; +import static com.microsoft.windowsazure.Configuration.PROPERTY_READ_TIMEOUT; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +@Configuration +@ConditionalOnResource(resources = "classpath:mediaservices.enable.config") +@ConditionalOnMissingBean(MediaContract.class) +@EnableConfigurationProperties(MediaServicesProperties.class) +@ConditionalOnProperty(prefix = "azure.mediaservices", + value = {"tenant", "client-id", "client-secret", "rest-api-endpoint"}) +public class MediaServicesAutoConfiguration { + private static final Logger LOG = LoggerFactory.getLogger(MediaServicesAutoConfiguration.class); + + private final MediaServicesProperties properties; + + public MediaServicesAutoConfiguration(MediaServicesProperties mediaServicesProperties) { + this.properties = mediaServicesProperties; + } + + @Bean + public MediaContract mediaContract() throws ServiceException, MalformedURLException, URISyntaxException { + LOG.debug("mediaContract called"); + return createMediaContract(); + } + + private MediaContract createMediaContract() throws ServiceException, MalformedURLException, URISyntaxException { + LOG.debug("createMediaContract called"); + + final ExecutorService executorService = Executors.newFixedThreadPool(1); + final AzureAdTokenCredentials credentials = new AzureAdTokenCredentials(properties.getTenant(), + new AzureAdClientSymmetricKey(properties.getClientId(), properties.getClientSecret()), + AzureEnvironments.AZURE_CLOUD_ENVIRONMENT); + + final AzureAdTokenProvider tokenProvider = new AzureAdTokenProvider(credentials, executorService); + + final com.microsoft.windowsazure.Configuration configuration = MediaConfiguration + .configureWithAzureAdTokenProvider(new URI(properties.getRestApiEndpoint()), tokenProvider); + + if (properties.getConnectTimeout() != null) { + configuration.getProperties().put(PROPERTY_CONNECT_TIMEOUT, properties.getConnectTimeout()); + } + if (properties.getReadTimeout() != null) { + configuration.getProperties().put(PROPERTY_READ_TIMEOUT, properties.getReadTimeout()); + } + + if (!StringUtils.isEmpty(properties.getProxyHost()) && nonNull(properties.getProxyPort())) { + configuration.getProperties().put(PROPERTY_HTTP_PROXY_HOST, properties.getProxyHost()); + configuration.getProperties().put(PROPERTY_HTTP_PROXY_PORT, properties.getProxyPort()); + configuration.getProperties().put(PROPERTY_HTTP_PROXY_SCHEME, properties.getProxyScheme()); + } else if (!StringUtils.isEmpty(properties.getProxyHost()) && isNull(properties.getProxyPort())) { + throw new ServiceException("Please configure azure.mediaservices.proxy-port"); + } else if (nonNull(properties.getProxyPort()) && StringUtils.isEmpty(properties.getProxyHost())) { + throw new ServiceException("Please configure azure.mediaservices.proxy-host"); + } + + return MediaService.create(configuration); + } + + @PostConstruct + private void sendTelemetry() { + if (properties.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(MediaServicesAutoConfiguration.class)); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesProperties.java new file mode 100644 index 000000000000..49aa704afe39 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesProperties.java @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.mediaservices; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@Validated +@ConfigurationProperties("azure.mediaservices") +public class MediaServicesProperties { + @NotEmpty(message = "azure.mediaservices.tenant property must be configured.") + private String tenant; + + /** + * Media service Azure Active Directory client-id(application id). + */ + @NotEmpty(message = "azure.mediaservices.client-id property must be configured.") + private String clientId; + + /** + * Media service Azure Active Directory client secret. + */ + @NotEmpty(message = "azure.mediaservices.client-secret property must be configured.") + private String clientSecret; + + /** + * Media service REST API endpoint. + */ + @NotEmpty(message = "azure.mediaservices.rest-api-endpoint property must be configured.") + private String restApiEndpoint; + + /** + * Proxy host if to use proxy. + */ + private String proxyHost; + + /** + * Proxy port if to use proxy. + */ + private Integer proxyPort; + + /** + * Proxy scheme if to use proxy. Default is http. + */ + private String proxyScheme = "http"; + + /** + * Whether allow telemetry collecting. + */ + private boolean allowTelemetry = true; + + /** + * Socket connect timeout + */ + private Integer connectTimeout; + + /** + * Socket read timeout + */ + private Integer readTimeout; + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public String getRestApiEndpoint() { + return restApiEndpoint; + } + + public void setRestApiEndpoint(String restApiEndpoint) { + this.restApiEndpoint = restApiEndpoint; + } + + public String getProxyHost() { + return proxyHost; + } + + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + public Integer getProxyPort() { + return proxyPort; + } + + public void setProxyPort(Integer proxyPort) { + this.proxyPort = proxyPort; + } + + public String getProxyScheme() { + return proxyScheme; + } + + public void setProxyScheme(String proxyScheme) { + this.proxyScheme = proxyScheme; + } + + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } + + public Integer getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Integer getReadTimeout() { + return readTimeout; + } + + public void setReadTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorMetricsExportAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorMetricsExportAutoConfiguration.java new file mode 100644 index 000000000000..612bfa956018 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorMetricsExportAutoConfiguration.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.metrics; + +import com.microsoft.applicationinsights.TelemetryConfiguration; +import io.micrometer.azuremonitor.AzureMonitorConfig; +import io.micrometer.azuremonitor.AzureMonitorMeterRegistry; +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.util.StringUtils; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Auto-Configuration for exporting metrics to Azure Application Insights. + * + * @author Dhaval Doshi + */ +@Configuration +@AutoConfigureBefore({CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class}) +@AutoConfigureAfter(MetricsAutoConfiguration.class) +@ConditionalOnBean(Clock.class) +@ConditionalOnClass(AzureMonitorMeterRegistry.class) +@ConditionalOnResource(resources = "classpath:metrics.enable.config") +@ConditionalOnProperty(prefix = "management.metrics.export.azuremonitor", + name = "enabled", havingValue = "true", matchIfMissing = true) +@EnableConfigurationProperties(AzureMonitorProperties.class) +public class AzureMonitorMetricsExportAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public AzureMonitorConfig azureConfig(AzureMonitorProperties properties) { + return new AzureMonitorPropertiesConfigAdapter(properties); + } + + /** + * This bean is already available when the + * Azure Application Insights starter + * is present. + * + * @param config Azure monitor config + * @return telemetry configuration + */ + @Bean + @ConditionalOnMissingBean + public TelemetryConfiguration telemetryConfiguration(AzureMonitorConfig config) { + // Gets the active instance of TelemetryConfiguration either created by starter or xml + final TelemetryConfiguration telemetryConfiguration = TelemetryConfiguration.getActive(); + if (StringUtils.isEmpty(telemetryConfiguration.getInstrumentationKey())) { + telemetryConfiguration.setInstrumentationKey(config.instrumentationKey()); + } + return telemetryConfiguration; + } + + @Bean + @ConditionalOnMissingBean + public AzureMonitorMeterRegistry azureMeterRegistry(AzureMonitorConfig config, + TelemetryConfiguration configuration, Clock clock) { + return AzureMonitorMeterRegistry.builder(config) + .clock(clock) + .telemetryConfiguration(configuration) + .build(); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorProperties.java new file mode 100644 index 000000000000..3e288e01cf9f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorProperties.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.metrics; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties} for configuring Azure Application Insights metrics export. + * + * @author Dhaval Doshi + */ +@ConfigurationProperties(prefix = "management.metrics.export.azuremonitor") +public class AzureMonitorProperties extends StepRegistryProperties { + private String instrumentationKey; + + public String getInstrumentationKey() { + return this.instrumentationKey; + } + + public void setInstrumentationKey(String instrumentationKey) { + this.instrumentationKey = instrumentationKey; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorPropertiesConfigAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorPropertiesConfigAdapter.java new file mode 100644 index 000000000000..93646aadc7ed --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorPropertiesConfigAdapter.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.metrics; + +import io.micrometer.azuremonitor.AzureMonitorConfig; +import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter; + +/** + * Adapter to convert {@link AzureMonitorProperties} to a {@link AzureMonitorConfig}. + * + * @author Dhaval Doshi + */ +public class AzureMonitorPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter + implements AzureMonitorConfig { + + AzureMonitorPropertiesConfigAdapter(AzureMonitorProperties properties) { + super(properties); + } + + @Override + public String instrumentationKey() { + return get(AzureMonitorProperties::getInstrumentationKey, AzureMonitorConfig.super::instrumentationKey); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusAutoConfiguration.java new file mode 100644 index 000000000000..56076a455b73 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusAutoConfiguration.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.servicebus; + +import com.microsoft.azure.servicebus.QueueClient; +import com.microsoft.azure.servicebus.SubscriptionClient; +import com.microsoft.azure.servicebus.TopicClient; +import com.microsoft.azure.servicebus.primitives.ConnectionStringBuilder; +import com.microsoft.azure.servicebus.primitives.ServiceBusException; +import com.microsoft.azure.telemetry.TelemetrySender; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.util.ClassUtils; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; + +import static com.microsoft.azure.telemetry.TelemetryData.HASHED_NAMESPACE; +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; +import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; + +@Lazy +@Configuration +@ConditionalOnResource(resources = "classpath:servicebus.enable.config") +@EnableConfigurationProperties(ServiceBusProperties.class) +@ConditionalOnProperty(prefix = "azure.servicebus", value = "connection-string") +public class ServiceBusAutoConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBusAutoConfiguration.class); + + private final ServiceBusProperties properties; + + public ServiceBusAutoConfiguration(ServiceBusProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "azure.servicebus", value = {"queue-name", "queue-receive-mode"}) + public QueueClient queueClient() throws InterruptedException, ServiceBusException { + return new QueueClient(new ConnectionStringBuilder(properties.getConnectionString(), + properties.getQueueName()), properties.getQueueReceiveMode()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "azure.servicebus", value = "topic-name") + public TopicClient topicClient() throws InterruptedException, ServiceBusException { + return new TopicClient(new ConnectionStringBuilder(properties.getConnectionString(), + properties.getTopicName())); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "azure.servicebus", + value = {"topic-name", "subscription-name", "subscription-receive-mode"}) + public SubscriptionClient subscriptionClient() throws ServiceBusException, InterruptedException { + return new SubscriptionClient(new ConnectionStringBuilder(properties.getConnectionString(), + properties.getTopicName() + "/subscriptions/" + properties.getSubscriptionName()), + properties.getSubscriptionReceiveMode()); + } + + private String getHashNamespace() { + final String namespace = properties.getConnectionString() + .replaceFirst("^.*//", "") // emit head 'Endpoint=sb://' + .replaceAll("\\..*$", ""); // emit tail '${namespace}.xxx.xxx' + + // Namespace can only be letter, number and hyphen, start with letter, end with letter or number, + // with length of 6-50. + if (!namespace.matches("[a-zA-Z][a-zA-Z-0-9]{4,48}[a-zA-Z0-9]")) { + LOGGER.warn("Unexpected name {}, please check if it's valid name or portal name rule changes.", namespace); + } + + return sha256Hex(namespace); + } + + @PostConstruct + private void sendTelemetry() { + if (properties.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(ServiceBusAutoConfiguration.class)); + events.put(HASHED_NAMESPACE, getHashNamespace()); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } + +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusProperties.java new file mode 100644 index 000000000000..bed9d20782e5 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusProperties.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.servicebus; + +import com.microsoft.azure.servicebus.ReceiveMode; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@Validated +@ConfigurationProperties("azure.servicebus") +public class ServiceBusProperties { + /** + * Service Bus connection string. + */ + @NotEmpty + private String connectionString; + + /** + * Queue name. Entity path of the queue. + */ + private String queueName; + + /** + * Queue receive mode. + */ + private ReceiveMode queueReceiveMode; + + /** + * Topic name. Entity path of the topic. + */ + private String topicName; + + /** + * Subscription name. + */ + private String subscriptionName; + + /** + * Subscription receive mode. + */ + private ReceiveMode subscriptionReceiveMode; + + /** + * allow telemery or not + */ + private boolean allowTelemetry = true; + + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } + + public String getConnectionString() { + return connectionString; + } + + public void setConnectionString(String connectionString) { + this.connectionString = connectionString; + } + + public String getQueueName() { + return queueName; + } + + public void setQueueName(String queueName) { + this.queueName = queueName; + } + + public ReceiveMode getQueueReceiveMode() { + return queueReceiveMode; + } + + public void setQueueReceiveMode(ReceiveMode queueReceiveMode) { + this.queueReceiveMode = queueReceiveMode; + } + + public String getTopicName() { + return topicName; + } + + public void setTopicName(String topicName) { + this.topicName = topicName; + } + + public String getSubscriptionName() { + return subscriptionName; + } + + public void setSubscriptionName(String subscriptionName) { + this.subscriptionName = subscriptionName; + } + + public ReceiveMode getSubscriptionReceiveMode() { + return subscriptionReceiveMode; + } + + public void setSubscriptionReceiveMode(ReceiveMode subscriptionReceiveMode) { + this.subscriptionReceiveMode = subscriptionReceiveMode; + } +} + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/storage/StorageAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/storage/StorageAutoConfiguration.java new file mode 100644 index 000000000000..c28ea31550e7 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/storage/StorageAutoConfiguration.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.storage; + +import com.microsoft.azure.storage.blob.ContainerURL; +import com.microsoft.azure.storage.blob.PipelineOptions; +import com.microsoft.azure.storage.blob.ServiceURL; +import com.microsoft.azure.storage.blob.SharedKeyCredentials; +import com.microsoft.azure.storage.blob.StorageURL; +import com.microsoft.azure.storage.blob.TelemetryOptions; +import com.microsoft.azure.telemetry.TelemetrySender; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ClassUtils; + +import javax.annotation.PostConstruct; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.InvalidKeyException; +import java.util.HashMap; +import java.util.Map; + +import static com.microsoft.azure.telemetry.TelemetryData.HASHED_ACCOUNT_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; +import static org.apache.commons.codec.digest.DigestUtils.sha256Hex; + +@Configuration +@ConditionalOnClass(ServiceURL.class) +@ConditionalOnResource(resources = "classpath:storage.enable.config") +@EnableConfigurationProperties(StorageProperties.class) +@ConditionalOnProperty(prefix = "azure.storage", value = {"account-name", "account-key"}) +public class StorageAutoConfiguration { + private static final Logger LOG = LoggerFactory.getLogger(StorageAutoConfiguration.class); + private static final String BLOB_URL = "http://%s.blob.core.windows.net"; + private static final String BLOB_HTTPS_URL = "https://%s.blob.core.windows.net"; + private static final String USER_AGENT_PREFIX = "spring-storage/"; + + private final StorageProperties properties; + + public StorageAutoConfiguration(StorageProperties properties) { + this.properties = properties; + } + + /** + * @param options PipelineOptions bean, not required. + * @throws MalformedURLException Thrown when the URL is malformed. + * @throws InvalidKeyException Thrown when the accountKey is ill-formatted. + * @return Service URL + */ + @Bean + public ServiceURL createServiceUrl(@Autowired(required = false) PipelineOptions options) throws + MalformedURLException, InvalidKeyException { + LOG.debug("Creating ServiceURL bean..."); + final SharedKeyCredentials credentials = new SharedKeyCredentials(properties.getAccountName(), + properties.getAccountKey()); + final URL blobUrl = getURL(); + final PipelineOptions pipelineOptions = buildOptions(options); + final ServiceURL serviceURL = new ServiceURL(blobUrl, StorageURL.createPipeline(credentials, pipelineOptions)); + + return serviceURL; + } + + private URL getURL() throws MalformedURLException { + if (properties.isUseEmulator()) { + LOG.debug("Using emulator address instead.."); + return new URL(String.format("%s/%s", properties.getEmulatorBlobHost(), properties.getAccountName())); + } + if (properties.isEnableHttps()) { + return new URL(String.format(BLOB_HTTPS_URL, properties.getAccountName())); + } + return new URL(String.format(BLOB_URL, properties.getAccountName())); + } + + private PipelineOptions buildOptions(PipelineOptions fromOptions) { + final PipelineOptions pipelineOptions = fromOptions == null ? new PipelineOptions() : fromOptions; + + pipelineOptions.withTelemetryOptions(new TelemetryOptions(USER_AGENT_PREFIX + + pipelineOptions.telemetryOptions().userAgentPrefix())); + + return pipelineOptions; + } + + @Bean + @ConditionalOnProperty(prefix = "azure.storage", value = "container-name") + public ContainerURL createContainerURL(ServiceURL serviceURL) { + return serviceURL.createContainerURL(properties.getContainerName()); + } + + @PostConstruct + private void sendTelemetry() { + if (properties.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(StorageAutoConfiguration.class)); + events.put(HASHED_ACCOUNT_NAME, sha256Hex(properties.getAccountName())); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/storage/StorageProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/storage/StorageProperties.java new file mode 100644 index 000000000000..0c96859948bd --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/storage/StorageProperties.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.storage; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@Validated +@ConfigurationProperties("azure.storage") +public class StorageProperties { + @NotEmpty + private String accountName; + + @NotEmpty + private String accountKey; + + private boolean useEmulator = false; + + private String emulatorBlobHost; + + private String containerName; + + private boolean allowTelemetry = true; + + private boolean enableHttps = false; + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public String getAccountKey() { + return accountKey; + } + + public void setAccountKey(String accountKey) { + this.accountKey = accountKey; + } + + public boolean isUseEmulator() { + return useEmulator; + } + + public void setUseEmulator(boolean useEmulator) { + this.useEmulator = useEmulator; + } + + public String getEmulatorBlobHost() { + return emulatorBlobHost; + } + + public void setEmulatorBlobHost(String emulatorBlobHost) { + this.emulatorBlobHost = emulatorBlobHost; + } + + public String getContainerName() { + return containerName; + } + + public void setContainerName(String containerName) { + this.containerName = containerName; + } + + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } + + public boolean isEnableHttps() { + return enableHttps; + } + + public void setEnableHttps(boolean enableHttps) { + this.enableHttps = enableHttps; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java new file mode 100644 index 000000000000..5285a745ac04 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* + * Disclaimer: + * This class is copied from https://github.com/Microsoft/azure-tools-for-java/ with minor modification (fixing + * static analysis error). + * Location in the repo: /Utils/azuretools-core/src/com/microsoft/azuretools/azurecommons/util/GetHashMac.java + */ + +package com.microsoft.azure.spring.support; + +import org.apache.commons.io.IOUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class GetHashMac { + public static final String MAC_REGEX = "([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}"; + public static final String MAC_REGEX_ZERO = "([0]{2}[:-]){5}[0]{2}"; + public static final String HASHED_MAC_REGEX = "[0-9a-f]{64}"; + + private GetHashMac() { + super(); + } + + public static boolean isValidHashMacFormat(String hashMac) { + if (hashMac == null || hashMac.isEmpty()) { + return false; + } + + final Pattern hashedMacPattern = Pattern.compile(HASHED_MAC_REGEX); + final Matcher matcher = hashedMacPattern.matcher(hashMac); + return matcher.matches(); + } + + public static String getHashMac() { + final String rawMac = getRawMac(); + if (rawMac == null || rawMac.isEmpty()) { + return null; + } + + final Pattern pattern = Pattern.compile(MAC_REGEX); + final Pattern patternZero = Pattern.compile(MAC_REGEX_ZERO); + final Matcher matcher = pattern.matcher(rawMac); + String mac = ""; + while (matcher.find()) { + mac = matcher.group(0); + if (!patternZero.matcher(mac).matches()) { + break; + } + } + + return hash(mac); + } + + private static String getRawMac() { + final StringBuilder ret = new StringBuilder(); + + final String os = System.getProperty("os.name"); + String[] command = {"ifconfig", "-a"}; + if (os != null && !os.isEmpty() && os.toLowerCase(Locale.US).startsWith("win")) { + command = new String[]{"getmac"}; + } + + try { + final ProcessBuilder builder = new ProcessBuilder(command); + final Process process = builder.start(); + + InputStream inputStream = process.getInputStream(); + InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(inputStreamReader); + try { + String tmp; + while ((tmp = br.readLine()) != null) { + ret.append(tmp); + } + } catch (IOException e) { + IOUtils.closeQuietly(br); + } finally { + IOUtils.closeQuietly(br); + } + } catch (IOException e) { + return null; + } + + return ret.toString(); + } + + private static String hash(String mac) { + if (mac == null || mac.isEmpty()) { + return null; + } + + final String ret; + try { + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + final byte[] bytes = mac.getBytes(StandardCharsets.UTF_8); + md.update(bytes); + final byte[] bytesAfterDigest = md.digest(); + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytesAfterDigest.length; i++) { + sb.append(Integer.toString((bytesAfterDigest[i] & 0xff) + 0x100, 16).substring(1)); + } + + ret = sb.toString(); + } catch (NoSuchAlgorithmException ex) { + return null; + } + + return ret; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java new file mode 100644 index 000000000000..93d744fd886b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.support; + +public class UserAgent { + /** + * Generate UserAgent string for given service. + * + * @param serviceName Name of the service from which called this method. + * @param allowTelemetry Whether allows telemtry + * @return generated UserAgent string + */ + public static String getUserAgent(String serviceName, boolean allowTelemetry) { + String macAddress = "Not Collected"; + if (allowTelemetry) { + macAddress = GetHashMac.getHashMac(); + } + + return String.format(serviceName + " MacAddressHash:%s", macAddress); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java new file mode 100644 index 000000000000..2056a0ad9226 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.telemetry; + +public class TelemetryData { + + public static final String INSTALLATION_ID = "installationId"; + public static final String PROJECT_VERSION = "version"; + public static final String SERVICE_NAME = "serviceName"; + public static final String HASHED_ACCOUNT_NAME = "hashedAccountName"; + public static final String HASHED_NAMESPACE = "hashedNamespace"; + public static final String TENANT_NAME = "tenantName"; + + public static String getClassPackageSimpleName(Class clazz) { + if (clazz == null) { + return "unknown"; + } + + return clazz.getPackage().getName().replaceAll("\\w+\\.", ""); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java new file mode 100644 index 000000000000..bea134be254f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.telemetry; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.azure.utils.PropertyLoader; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.time.Instant; +import java.util.Map; + +public class TelemetryEventData { + + private final String name; + + @JsonProperty("iKey") + private final String instrumentationKey; + + private final Tags tags = new Tags("Spring-on-azure", "Java-maven-plugin"); + + private final EventData data = new EventData("EventData"); + + private final String time; + + public TelemetryEventData(String eventName, @NonNull Map properties) { + Assert.hasText(eventName, "Event name should contain text."); + + name = "Microsoft.ApplicationInsights.Event"; + instrumentationKey = PropertyLoader.getTelemetryInstrumentationKey(); + + data.getBaseData().setName(eventName); + data.getBaseData().setProperties(properties); + time = Instant.now().toString(); + } + + private static class Tags { + + @JsonProperty("ai.cloud.roleInstance") + private final String aiCloudRoleInstance; + + @JsonProperty("ai.internal.sdkVersion") + private final String aiInternalSdkVersion; + + Tags(String instance, String sdkVersion) { + aiCloudRoleInstance = instance; + aiInternalSdkVersion = sdkVersion; + } + + public String getAiCloudRoleInstance() { + return aiCloudRoleInstance; + } + + public String getAiInternalSdkVersion() { + return aiInternalSdkVersion; + } + } + + private static class EventData { + + private final String baseType; + + private final CustomData baseData = new CustomData(); + + EventData(String baseType) { + this.baseType = baseType; + } + + private static class CustomData { + + private final Integer ver = 2; + + private String name; + + private Map properties; + + public Integer getVer() { + return ver; + } + + public String getName() { + return name; + } + + public Map getProperties() { + return properties; + } + + private void setName(String name) { + this.name = name; + } + + private void setProperties(Map properties) { + this.properties = properties; + } + } + + public String getBaseType() { + return baseType; + } + + public CustomData getBaseData() { + return baseData; + } + } + + public String getName() { + return name; + } + + public String getInstrumentationKey() { + return instrumentationKey; + } + + public Tags getTags() { + return tags; + } + + public EventData getData() { + return data; + } + + public String getTime() { + return time; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java new file mode 100644 index 000000000000..2561474615ce --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.telemetry; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("telemetry") +public class TelemetryProperties { + + private String instrumentationKey; + + public String getInstrumentationKey() { + return instrumentationKey; + } + + public void setInstrumentationKey(String instrumentationKey) { + this.instrumentationKey = instrumentationKey; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java new file mode 100644 index 000000000000..3e4c02a26bea --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.telemetry; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.support.GetHashMac; +import com.microsoft.azure.utils.PropertyLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON; + +public class TelemetrySender { + + private static final Logger LOGGER = LoggerFactory.getLogger(TelemetrySender.class); + + private static final String TELEMETRY_TARGET_URL = "https://dc.services.visualstudio.com/v2/track"; + + private static final String PROJECT_INFO = "spring-boot-starter/" + PropertyLoader.getProjectVersion(); + + private static final int RETRY_LIMIT = 3; // Align the retry times with sdk + + private static final RestTemplate REST_TEMPLATE = new RestTemplate(); + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final HttpHeaders HEADERS = new HttpHeaders(); + + static { + HEADERS.add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON.toString()); + } + + private ResponseEntity executeRequest(final TelemetryEventData eventData) { + try { + final HttpEntity body = new HttpEntity<>(MAPPER.writeValueAsString(eventData), HEADERS); + + return REST_TEMPLATE.exchange(TELEMETRY_TARGET_URL, HttpMethod.POST, body, String.class); + } catch (RestClientException | JsonProcessingException e) { + LOGGER.warn("Failed to exchange telemetry request, {}.", e.getMessage()); + } + + return null; + } + + private void sendTelemetryData(@NonNull TelemetryEventData eventData) { + ResponseEntity response = null; + + for (int i = 0; i < RETRY_LIMIT; i++) { + response = executeRequest(eventData); + + if (response != null && response.getStatusCode() == HttpStatus.OK) { + return; + } + } + + if (response != null && response.getStatusCode() != HttpStatus.OK) { + LOGGER.warn("Failed to send telemetry data, response status code {}.", response.getStatusCode().toString()); + } + } + + public void send(String name, @NonNull Map properties) { + Assert.hasText(name, "Event name should contain text."); + + properties.putIfAbsent(TelemetryData.INSTALLATION_ID, GetHashMac.getHashMac()); + properties.putIfAbsent(TelemetryData.PROJECT_VERSION, PROJECT_INFO); + + sendTelemetryData(new TelemetryEventData(name, properties)); + } +} + diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java new file mode 100644 index 000000000000..0229282a7849 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class PropertyLoader { + private static final String PROJECT_PROPERTY_FILE = "/META-INF/project.properties"; + + private static final String TELEMETRY_CONFIG_FILE = "/telemetry.config"; + + private static String getProperty(String file, String property) { + InputStream inputStream = null; + try { + inputStream = PropertyLoader.class.getResourceAsStream(file); + if (inputStream != null) { + final Properties properties = new Properties(); + properties.load(inputStream); + + return properties.getProperty(property); + } + } catch (IOException e) { + // Omitted + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + // Omitted + } + } + } + + return "unknown"; + } + + public static String getProjectVersion() { + return getProperty(PROJECT_PROPERTY_FILE, "project.version"); + } + + public static String getTelemetryInstrumentationKey() { + return getProperty(TELEMETRY_CONFIG_FILE, "telemetry.instrumentationKey"); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties new file mode 100644 index 000000000000..90d42083480f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties @@ -0,0 +1 @@ +project.version=@project.version@ diff --git a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..603d44a26bf0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories @@ -0,0 +1,15 @@ +org.springframework.boot.env.EnvironmentPostProcessor=com.microsoft.azure.spring.cloudfoundry.environment.VcapProcessor,\ +com.microsoft.azure.spring.keyvault.KeyVaultEnvironmentPostProcessor +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosDbRepositoriesAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosDbReactiveRepositoriesAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.gremlin.GremlinAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.gremlin.GremlinRepositoriesAutoConfiguration,\ +com.azure.spring.autoconfigure.mediaservices.MediaServicesAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.servicebus.ServiceBusAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.storage.StorageAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationFilterAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.aad.AADOAuth2AutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.btoc.AADB2CAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.metrics.AzureMonitorMetricsExportAutoConfiguration,\ +com.microsoft.azure.spring.autoconfigure.jms.ServiceBusJMSAutoConfiguration diff --git a/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties b/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties new file mode 100644 index 000000000000..ff355a06c5de --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties @@ -0,0 +1,12 @@ +spring.security.oauth2.client.provider.azure.authorization-uri=https://login.microsoftonline.com/common/oauth2/authorize +spring.security.oauth2.client.provider.azure.token-uri=https://login.microsoftonline.com/common/oauth2/token +spring.security.oauth2.client.provider.azure.user-info-uri=https://login.microsoftonline.com/common/openid/userinfo +spring.security.oauth2.client.provider.azure.jwk-set-uri=https://login.microsoftonline.com/common/discovery/keys +spring.security.oauth2.client.provider.azure.user-name-attribute=name + +spring.security.oauth2.client.registration.azure.client-authentication-method=post +spring.security.oauth2.client.registration.azure.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.azure.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} +spring.security.oauth2.client.registration.azure.scope=openid, https://graph.microsoft.com/user.read +spring.security.oauth2.client.registration.azure.client-name=Azure +spring.security.oauth2.client.registration.azure.provider=azure diff --git a/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties b/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties new file mode 100644 index 000000000000..236d2ff8b308 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties @@ -0,0 +1,16 @@ +azure.service.endpoints.cn.aadSigninUri=https://login.partner.microsoftonline.cn/ +azure.service.endpoints.cn.aadGraphApiUri=https://graph.chinacloudapi.cn/ +azure.service.endpoints.cn.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys +azure.service.endpoints.cn.aadMembershipRestUri=https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6 +azure.service.endpoints.cn-v2-graph.aadSigninUri=https://login.partner.microsoftonline.cn/ +azure.service.endpoints.cn-v2-graph.aadGraphApiUri=https://microsoftgraph.chinacloudapi.cn/ +azure.service.endpoints.cn-v2-graph.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys +azure.service.endpoints.cn-v2-graph.aadMembershipRestUri=https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf +azure.service.endpoints.global.aadSigninUri=https://login.microsoftonline.com/ +azure.service.endpoints.global.aadGraphApiUri=https://graph.windows.net/ +azure.service.endpoints.global.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/ +azure.service.endpoints.global.aadMembershipRestUri=https://graph.windows.net/me/memberOf?api-version=1.6 +azure.service.endpoints.global-v2-graph.aadSigninUri=https://login.microsoftonline.com/ +azure.service.endpoints.global-v2-graph.aadGraphApiUri=https://graph.microsoft.com/ +azure.service.endpoints.global-v2-graph.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/ +azure.service.endpoints.global-v2-graph.aadMembershipRestUri=https://graph.microsoft.com/v1.0/me/memberOf diff --git a/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config b/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config new file mode 100644 index 000000000000..61b615fa0ef1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config @@ -0,0 +1 @@ +telemetry.instrumentationKey=@telemetry.instrumentationKey@ \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java new file mode 100644 index 000000000000..b042cfec863a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/InitializerTest.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.junit.Assert.assertFalse; + +@RunWith(SpringJUnit4ClassRunner.class) +@TestPropertySource(locations = "classpath:application.properties") +public class InitializerTest { + + @Autowired + ApplicationContext context; + + @Test + public void testAzureKvPropertySourceNotInitialized() { + final MutablePropertySources sources = + ((ConfigurableEnvironment) context.getEnvironment()).getPropertySources(); + + assertFalse("PropertySources should not contains azurekv when enabled=false", + sources.contains(Constants.AZURE_KEYVAULT_PROPERTYSOURCE_NAME)); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorTest.java new file mode 100644 index 000000000000..6944c96789da --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultEnvironmentPostProcessorTest.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.keyvault.spring; + +import com.azure.core.credential.TokenCredential; +import com.azure.identity.ClientCertificateCredential; +import com.azure.identity.ClientSecretCredential; +import com.azure.identity.ManagedIdentityCredential; +import org.hamcrest.core.IsInstanceOf; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.mock.env.MockEnvironment; + +import java.util.HashMap; +import java.util.Map; + +import static com.microsoft.azure.keyvault.spring.Constants.AZURE_KEYVAULT_CERTIFICATE_PATH; +import static com.microsoft.azure.keyvault.spring.Constants.AZURE_KEYVAULT_CLIENT_ID; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +public class KeyVaultEnvironmentPostProcessorTest { + private KeyVaultEnvironmentPostProcessorHelper keyVaultEnvironmentPostProcessorHelper; + private MockEnvironment environment; + private MutablePropertySources propertySources; + private Map testProperties = new HashMap<>(); + + @Before + public void setup() { + environment = new MockEnvironment(); + environment.setProperty(Constants.AZURE_KEYVAULT_ALLOW_TELEMETRY, "false"); + testProperties.clear(); + propertySources = environment.getPropertySources(); + } + + @Test + public void testGetCredentialsWhenUsingClientAndKey() { + testProperties.put("azure.keyvault.client-id", "aaaa-bbbb-cccc-dddd"); + testProperties.put("azure.keyvault.client-key", "mySecret"); + testProperties.put("azure.keyvault.tenant-id", "myid"); + propertySources.addLast(new MapPropertySource("Test_Properties", testProperties)); + + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + + assertThat(credentials, IsInstanceOf.instanceOf(ClientSecretCredential.class)); + } + + @Test + public void testGetCredentialsWhenPFXCertConfigured() { + testProperties.put(AZURE_KEYVAULT_CLIENT_ID, "aaaa-bbbb-cccc-dddd"); + testProperties.put("azure.keyvault.tenant-id", "myid"); + testProperties.put(AZURE_KEYVAULT_CERTIFICATE_PATH, "fake-pfx-cert.pfx"); + + propertySources.addLast(new MapPropertySource("Test_Properties", testProperties)); + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + assertThat(credentials, IsInstanceOf.instanceOf(ClientCertificateCredential.class)); + } + + @Test + public void testGetCredentialsWhenMSIEnabledInAppService() { + testProperties.put("MSI_ENDPOINT", "fakeendpoint"); + testProperties.put("MSI_SECRET", "fakesecret"); + propertySources.addLast(new MapPropertySource("Test_Properties", testProperties)); + + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + + assertThat(credentials, IsInstanceOf.instanceOf(ManagedIdentityCredential.class)); + } + + @Test + public void testGetCredentialsWhenMSIEnabledInVMWithClientId() { + testProperties.put("azure.keyvault.client-id", "aaaa-bbbb-cccc-dddd"); + propertySources.addLast(new MapPropertySource("Test_Properties", testProperties)); + + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + + assertThat(credentials, IsInstanceOf.instanceOf(ManagedIdentityCredential.class)); + } + + @Test + public void testGetCredentialsWhenMSIEnabledInVMWithoutClientId() { + keyVaultEnvironmentPostProcessorHelper = new KeyVaultEnvironmentPostProcessorHelper(environment); + + final TokenCredential credentials = keyVaultEnvironmentPostProcessorHelper.getCredentials(); + + assertThat(credentials, IsInstanceOf.instanceOf(ManagedIdentityCredential.class)); + } + + @Test + public void postProcessorHasConfiguredOrder() { + final KeyVaultEnvironmentPostProcessor processor = new KeyVaultEnvironmentPostProcessor(); + assertEquals(processor.getOrder(), KeyVaultEnvironmentPostProcessor.DEFAULT_ORDER); + } + + @Test + public void postProcessorOrderConfigurable() { + final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OrderedProcessConfig.class)) + .withPropertyValues("azure.keyvault.uri=fakeuri", "azure.keyvault.enabled=true"); + + contextRunner.run(context -> { + assertThat("Configured order for KeyVaultEnvironmentPostProcessor is different with default order " + + "value.", + KeyVaultEnvironmentPostProcessor.DEFAULT_ORDER != OrderedProcessConfig.TEST_ORDER); + assertEquals("KeyVaultEnvironmentPostProcessor order should be changed.", + OrderedProcessConfig.TEST_ORDER, + context.getBean(KeyVaultEnvironmentPostProcessor.class).getOrder()); + }); + } +} + +@Configuration +class OrderedProcessConfig { + static final int TEST_ORDER = KeyVaultEnvironmentPostProcessor.DEFAULT_ORDER + 1; + + @Bean + @Primary + public KeyVaultEnvironmentPostProcessor getProcessor() { + final KeyVaultEnvironmentPostProcessor processor = new KeyVaultEnvironmentPostProcessor(); + processor.setOrder(TEST_ORDER); + return processor; + } +} + diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultOperationUnitTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultOperationUnitTest.java new file mode 100644 index 000000000000..09416d80297f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultOperationUnitTest.java @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.keyvault.spring; + +import com.azure.core.http.rest.PagedFlux; +import com.azure.core.http.rest.PagedIterable; +import com.azure.security.keyvault.secrets.SecretClient; +import com.azure.security.keyvault.secrets.models.KeyVaultSecret; +import com.azure.security.keyvault.secrets.models.SecretProperties; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class KeyVaultOperationUnitTest { + + private static final List SECRET_KEYS_CONFIG = Arrays.asList("key1", "key2", "key3"); + + private static final String TEST_PROPERTY_NAME_1 = "testPropertyName1"; + + private static final String SECRET_KEY_1 = "key1"; + + private static final String FAKE_VAULT_URI = "https:fake.vault.com"; + + private static final String TEST_SPRING_RELAXED_BINDING_NAME_0 = "acme.my-project.person.first-name"; + + private static final String TEST_SPRING_RELAXED_BINDING_NAME_1 = "acme.myProject.person.firstName"; + + private static final String TEST_SPRING_RELAXED_BINDING_NAME_2 = "acme.my_project.person.first_name"; + + private static final String TEST_SPRING_RELAXED_BINDING_NAME_3 = "ACME_MYPROJECT_PERSON_FIRSTNAME"; + + private static final String TEST_AZURE_KEYVAULT_NAME = "acme-myproject-person-firstname"; + + private static final List TEST_SPRING_RELAXED_BINDING_NAMES = Arrays.asList( + TEST_SPRING_RELAXED_BINDING_NAME_0, + TEST_SPRING_RELAXED_BINDING_NAME_1, + TEST_SPRING_RELAXED_BINDING_NAME_2, + TEST_SPRING_RELAXED_BINDING_NAME_3 + ); + + @Mock + private SecretClient keyVaultClient; + private KeyVaultOperation keyVaultOperation; + + public void setupSecretBundle(String id, String value, List secretKeysConfig) { + //provision for list + when(keyVaultClient.listPropertiesOfSecrets()).thenReturn(new MockPage(new PagedFlux<>(() -> null), id)); + //provison for get + final KeyVaultSecret secretBundle = new KeyVaultSecret(id, value); + when(keyVaultClient.getSecret(anyString())).thenReturn(secretBundle); + keyVaultOperation = new KeyVaultOperation(keyVaultClient, + FAKE_VAULT_URI, + Constants.TOKEN_ACQUIRE_TIMEOUT_SECS, + secretKeysConfig); + } + + @Test + public void testGet() { + //test get with no specific secret keys + setupSecretBundle(TEST_PROPERTY_NAME_1, TEST_PROPERTY_NAME_1, null); + assertThat(keyVaultOperation.get(TEST_PROPERTY_NAME_1)).isEqualToIgnoringCase(TEST_PROPERTY_NAME_1); + } + + @Test + public void testGetAndMissWhenSecretsProvided() { + //test get with specific secret key configs + setupSecretBundle(TEST_PROPERTY_NAME_1, TEST_PROPERTY_NAME_1, SECRET_KEYS_CONFIG); + assertThat(keyVaultOperation.get(TEST_PROPERTY_NAME_1)).isEqualToIgnoringCase(null); + } + + @Test + public void testGetAndHitWhenSecretsProvided() { + setupSecretBundle(SECRET_KEY_1, SECRET_KEY_1, SECRET_KEYS_CONFIG); + assertThat(keyVaultOperation.get(SECRET_KEY_1)).isEqualToIgnoringCase(SECRET_KEY_1); + } + + @Test + public void testList() { + //test list with no specific secret keys + setupSecretBundle(TEST_PROPERTY_NAME_1, TEST_PROPERTY_NAME_1, null); + final String[] result = keyVaultOperation.list(); + assertThat(result.length).isEqualTo(1); + assertThat(result[0]).isEqualToIgnoringCase(TEST_PROPERTY_NAME_1); + + //test list with specific secret key configs + setupSecretBundle(TEST_PROPERTY_NAME_1, TEST_PROPERTY_NAME_1, SECRET_KEYS_CONFIG); + final String[] specificResult = keyVaultOperation.list(); + assertThat(specificResult.length).isEqualTo(3); + assertThat(specificResult[0]).isEqualTo(SECRET_KEYS_CONFIG.get(0)); + } + + @Test + public void setTestSpringRelaxedBindingNames() { + //test list with no specific secret keys + setupSecretBundle(TEST_AZURE_KEYVAULT_NAME, TEST_AZURE_KEYVAULT_NAME, null); + + TEST_SPRING_RELAXED_BINDING_NAMES.forEach( + n -> assertThat(keyVaultOperation.get(n)).isEqualTo(TEST_AZURE_KEYVAULT_NAME) + ); + + //test list with specific secret key configs + setupSecretBundle(TEST_AZURE_KEYVAULT_NAME, TEST_AZURE_KEYVAULT_NAME, Arrays.asList(TEST_AZURE_KEYVAULT_NAME)); + TEST_SPRING_RELAXED_BINDING_NAMES.forEach( + n -> assertThat(keyVaultOperation.get(n)).isEqualTo(TEST_AZURE_KEYVAULT_NAME) + ); + + setupSecretBundle(TEST_AZURE_KEYVAULT_NAME, TEST_AZURE_KEYVAULT_NAME, SECRET_KEYS_CONFIG); + TEST_SPRING_RELAXED_BINDING_NAMES.forEach( + n -> assertThat(keyVaultOperation.get(n)).isEqualTo(null) + ); + } + + class MockPage extends PagedIterable { + private String name; + + MockPage(PagedFlux pagedFlux, String name) { + super(pagedFlux); + this.name = name; + } + + /** + * Creates instance given {@link PagedFlux}. + * + * @param pagedFlux to use as iterable + */ + MockPage(PagedFlux pagedFlux) { + super(pagedFlux); + } + + @Override + public void forEach(Consumer action) { + action.accept(new MockSecretProperties(name)); + } + } + + class MockSecretProperties extends SecretProperties { + private String name; + + MockSecretProperties(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySourceUnitTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySourceUnitTest.java new file mode 100644 index 000000000000..e536bc21030f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/keyvault/spring/KeyVaultPropertySourceUnitTest.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.keyvault.spring; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class KeyVaultPropertySourceUnitTest { + + private static final String TEST_PROPERTY_NAME_1 = "testPropertyName1"; + @Mock + KeyVaultOperation keyVaultOperation; + KeyVaultPropertySource keyVaultPropertySource; + + @Before + public void setup() { + final String[] propertyNameList = new String[]{TEST_PROPERTY_NAME_1}; + + when(keyVaultOperation.get(anyString())).thenReturn(TEST_PROPERTY_NAME_1); + when(keyVaultOperation.list()).thenReturn(propertyNameList); + + keyVaultPropertySource = new KeyVaultPropertySource(keyVaultOperation); + } + + @Test + public void testGetPropertyNames() { + final String[] result = keyVaultPropertySource.getPropertyNames(); + + assertThat(result.length).isEqualTo(1); + assertThat(result[0]).isEqualTo(TEST_PROPERTY_NAME_1); + } + + @Test + public void testGetProperty() { + final String result = (String) keyVaultPropertySource.getProperty(TEST_PROPERTY_NAME_1); + assertThat(result).isEqualTo(TEST_PROPERTY_NAME_1); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java new file mode 100644 index 000000000000..537ad4e0cf6f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader.Builder; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.BadJWTException; +import java.io.IOException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.minidev.json.JSONArray; +import org.assertj.core.api.Assertions; +import org.hamcrest.CoreMatchers; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +public class AADAppRoleAuthenticationFilterTest { + + public static final String TOKEN = "dummy-token"; + + private final UserPrincipalManager userPrincipalManager; + private final HttpServletRequest request; + private final HttpServletResponse response; + private final SimpleGrantedAuthority roleAdmin; + private final SimpleGrantedAuthority roleUser; + private final AADAppRoleStatelessAuthenticationFilter filter; + + private UserPrincipal createUserPrincipal(Collection roles) { + final JSONArray claims = new JSONArray(); + claims.addAll(roles); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() + .subject("john doe") + .claim("roles", claims) + .build(); + final JWSObject jwsObject = new JWSObject(new Builder(JWSAlgorithm.RS256).build(), + new Payload(jwtClaimsSet.toString())); + return new UserPrincipal(jwsObject, jwtClaimsSet); + } + + public AADAppRoleAuthenticationFilterTest() { + userPrincipalManager = mock(UserPrincipalManager.class); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + roleAdmin = new SimpleGrantedAuthority("ROLE_admin"); + roleUser = new SimpleGrantedAuthority("ROLE_user"); + filter = new AADAppRoleStatelessAuthenticationFilter(userPrincipalManager); + } + + @Test + public void testDoFilterGoodCase() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + final UserPrincipal dummyPrincipal = createUserPrincipal(Arrays.asList("user", "admin")); + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(TOKEN)).thenReturn(dummyPrincipal); + + // Check in subsequent filter that authentication is available! + final FilterChain filterChain = new FilterChain() { + @Override + @SuppressWarnings("unchecked") + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + final Authentication authentication = context.getAuthentication(); + assertNotNull(authentication); + assertTrue("User should be authenticated!", authentication.isAuthenticated()); + assertEquals(dummyPrincipal, authentication.getPrincipal()); + Assertions.assertThat((Collection) authentication.getAuthorities()) + .containsExactlyInAnyOrder(roleAdmin, roleUser); + } + }; + + filter.doFilterInternal(request, response, filterChain); + + verify(userPrincipalManager).buildUserPrincipal(TOKEN); + assertNull("Authentication has not been cleaned up!", SecurityContextHolder.getContext().getAuthentication()); + } + + @Test(expected = ServletException.class) + public void testDoFilterShouldRethrowJWTException() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(any())).thenThrow(new BadJWTException("bad token")); + + filter.doFilterInternal(request, response, mock(FilterChain.class)); + } + + @Test + public void testDoFilterAddsDefaultRole() + throws ParseException, JOSEException, BadJOSEException, ServletException, IOException { + + final UserPrincipal dummyPrincipal = createUserPrincipal(Collections.emptyList()); + + when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN); + when(userPrincipalManager.buildUserPrincipal(TOKEN)).thenReturn(dummyPrincipal); + + // Check in subsequent filter that authentication is available and default roles are filled. + final FilterChain filterChain = new FilterChain() { + @Override + @SuppressWarnings("unchecked") + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + final SecurityContext context = SecurityContextHolder.getContext(); + assertNotNull(context); + final Authentication authentication = context.getAuthentication(); + assertNotNull(authentication); + assertTrue("User should be authenticated!", authentication.isAuthenticated()); + final SimpleGrantedAuthority expectedDefaultRole = new SimpleGrantedAuthority("ROLE_USER"); + Assertions.assertThat((Collection) authentication.getAuthorities()) + .containsExactlyInAnyOrder(expectedDefaultRole); + } + }; + + filter.doFilterInternal(request, response, filterChain); + + verify(userPrincipalManager).buildUserPrincipal(TOKEN); + assertNull("Authentication has not been cleaned up!", SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + public void testRolesToGrantedAuthoritiesShouldConvertRolesAndFilterNulls() { + final JSONArray roles = new JSONArray().appendElement("user").appendElement(null).appendElement("ADMIN"); + final AADAppRoleStatelessAuthenticationFilter filter = new AADAppRoleStatelessAuthenticationFilter(null); + final Set result = filter.rolesToGrantedAuthorities(roles); + assertThat("Set should contain the two granted authority 'ROLE_user' and 'ROLE_ADMIN'", result, + CoreMatchers.hasItems(new SimpleGrantedAuthority("ROLE_user"), + new SimpleGrantedAuthority("ROLE_ADMIN"))); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java new file mode 100644 index 000000000000..7db4b15b3699 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.core.env.Environment; + +import java.util.Map; + +import static com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationProperties.UserGroupProperties; +import static org.assertj.core.api.Assertions.assertThat; + +public class AADAuthenticationAutoConfigurationTest { + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)) + .withPropertyValues("azure.activedirectory.client-id=fake-client-id", + "azure.activedirectory.client-secret=fake-client-secret", + "azure.activedirectory.active-directory-groups=fake-group", + "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri"); + + @Test + public void createAADAuthenticationFilter() { + this.contextRunner.run(context -> { + final AADAuthenticationFilter azureADJwtTokenFilter = context.getBean(AADAuthenticationFilter.class); + assertThat(azureADJwtTokenFilter).isNotNull(); + assertThat(azureADJwtTokenFilter).isExactlyInstanceOf(AADAuthenticationFilter.class); + }); + } + + @Test + public void serviceEndpointsCanBeOverridden() { + this.contextRunner.withPropertyValues("azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/", + "azure.service.endpoints.global.aadSigninUri=https://test/", + "azure.service.endpoints.global.aadGraphApiUri=https://test/", + "azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/", + "azure.service.endpoints.global.aadMembershipRestUri=https://test/") + .run(context -> { + final Environment environment = context.getEnvironment(); + assertThat(environment.getProperty("azure.service.endpoints.global.aadSigninUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadGraphApiUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadKeyDiscoveryUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadMembershipRestUri")) + .isEqualTo("https://test/"); + final ServiceEndpointsProperties serviceEndpointsProperties = + context.getBean(ServiceEndpointsProperties.class); + assertThat(serviceEndpointsProperties).isNotNull(); + assertThat(serviceEndpointsProperties.getEndpoints()).isNotEmpty(); + + final Map endpoints = serviceEndpointsProperties.getEndpoints(); + assertThat(endpoints).hasSize(4); + assertThat(endpoints.get("cn")).isNotNull() + .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri, + ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri) + .containsExactly("https://graph.chinacloudapi.cn/", + "https://login.partner.microsoftonline.cn/common/discovery/keys", + "https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6", + "https://login.partner.microsoftonline.cn/"); + assertThat(endpoints.get("global")).isNotNull() + .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri, + ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri) + .containsExactly("https://test/", "https://test/", "https://test/", "https://test/"); + }); + } + + @Test + public void testUserGroupPropertiesAreOverridden() { + contextRunner.withPropertyValues("azure.activedirectory.user-group.allowed-groups=another_group,third_group", + "azure.activedirectory.user-group.key=key", "azure.activedirectory.user-group.value=value", + "azure.activedirectory.user-group.object-id-key=objidk").run(context -> { + assertThat(context.getBean(AADAuthenticationProperties.class)).isNotNull(); + + final UserGroupProperties userGroupProperties = context + .getBean(AADAuthenticationProperties.class).getUserGroup(); + + assertThat(userGroupProperties).hasNoNullFieldsOrProperties() + .extracting(UserGroupProperties::getKey, UserGroupProperties::getValue, + UserGroupProperties::getObjectIDKey).containsExactly("key", "value", "objidk"); + + assertThat(userGroupProperties.getAllowedGroups()).isNotEmpty() + .hasSize(2).containsExactly("another_group", "third_group"); + } + ); + + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java new file mode 100644 index 000000000000..96ddd09ccc5a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.After; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.ObjectError; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AADAuthenticationFilterPropertiesTest { + @After + public void clearAllProperties() { + System.clearProperty(Constants.SERVICE_ENVIRONMENT_PROPERTY); + System.clearProperty(Constants.CLIENT_ID_PROPERTY); + System.clearProperty(Constants.CLIENT_SECRET_PROPERTY); + System.clearProperty(Constants.TARGETED_GROUPS_PROPERTY); + } + + @Test + public void canSetProperties() { + configureAllRequiredProperties(); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(Config.class); + context.refresh(); + + final AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + + assertThat(properties.getClientId()).isEqualTo(Constants.CLIENT_ID); + assertThat(properties.getClientSecret()).isEqualTo(Constants.CLIENT_SECRET); + assertThat(properties.getActiveDirectoryGroups() + .toString()).isEqualTo(Constants.TARGETED_GROUPS.toString()); + } + } + + @Test + public void defaultEnvironmentIsGlobal() { + configureAllRequiredProperties(); + assertThat(System.getProperty(Constants.SERVICE_ENVIRONMENT_PROPERTY)).isNullOrEmpty(); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(Config.class); + context.refresh(); + + final AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class); + + assertThat(properties.getEnvironment()).isEqualTo(Constants.DEFAULT_ENVIRONMENT); + } + } + + private void configureAllRequiredProperties() { + System.setProperty(Constants.CLIENT_ID_PROPERTY, Constants.CLIENT_ID); + System.setProperty(Constants.CLIENT_SECRET_PROPERTY, Constants.CLIENT_SECRET); + System.setProperty(Constants.TARGETED_GROUPS_PROPERTY, + Constants.TARGETED_GROUPS.toString().replace("[", "").replace("]", "")); + } + + @Test + @Ignore // TODO (wepa) clientId and clientSecret can also be configured in oauth2 config, test to be refactored + public void emptySettingsNotAllowed() { + System.setProperty(Constants.CLIENT_ID_PROPERTY, ""); + System.setProperty(Constants.CLIENT_SECRET_PROPERTY, ""); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + Exception exception = null; + + context.register(Config.class); + + try { + context.refresh(); + } catch (Exception e) { + exception = e; + } + + assertThat(exception).isNotNull(); + assertThat(exception).isExactlyInstanceOf(ConfigurationPropertiesBindException.class); + + final BindValidationException bindException = (BindValidationException) exception.getCause().getCause(); + final List errors = bindException.getValidationErrors().getAllErrors(); + + final List errorStrings = errors.stream().map(e -> e.toString()).collect(Collectors.toList()); + + final List errorStringsExpected = Arrays.asList( + "Field error in object 'azure.activedirectory' on field 'activeDirectoryGroups': " + + "rejected value [null];", + "Field error in object 'azure.activedirectory' on field 'clientId': rejected value [];", + "Field error in object 'azure.activedirectory' on field 'clientSecret': rejected value [];" + ); + + Collections.sort(errorStrings); + + assertThat(errors.size()).isEqualTo(errorStringsExpected.size()); + + for (int i = 0; i < errorStrings.size(); i++) { + assertThat(errorStrings.get(i)).contains(errorStringsExpected.get(i)); + } + } + } + + @Configuration + @EnableConfigurationProperties(AADAuthenticationProperties.class) + static class Config { + } +} + diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java new file mode 100644 index 000000000000..345f97c641d8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AADAuthenticationFilterTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)); + + @Before + @Ignore + public void beforeEveryMethod() { + Assume.assumeTrue(!Constants.CLIENT_ID.contains("real_client_id")); + Assume.assumeTrue(!Constants.CLIENT_SECRET.contains("real_client_secret")); + Assume.assumeTrue(!Constants.BEARER_TOKEN.contains("real_jtw_bearer_token")); + } + + //TODO (Zhou Liu): current test case is out of date, a new test case need to cover here, do it later. + @Test + @Ignore + public void doFilterInternal() { + this.contextRunner.withPropertyValues(Constants.CLIENT_ID_PROPERTY, Constants.CLIENT_ID) + .withPropertyValues(Constants.CLIENT_SECRET_PROPERTY, Constants.CLIENT_SECRET) + .withPropertyValues(Constants.TARGETED_GROUPS_PROPERTY, + Constants.TARGETED_GROUPS.toString() + .replace("[", "").replace("]", "")); + + this.contextRunner.run(context -> { + final HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader(Constants.TOKEN_HEADER)).thenReturn(Constants.BEARER_TOKEN); + + final HttpServletResponse response = mock(HttpServletResponse.class); + final FilterChain filterChain = mock(FilterChain.class); + + + final AADAuthenticationFilter azureADJwtTokenFilter = context.getBean(AADAuthenticationFilter.class); + assertThat(azureADJwtTokenFilter).isNotNull(); + assertThat(azureADJwtTokenFilter).isExactlyInstanceOf(AADAuthenticationFilter.class); + + azureADJwtTokenFilter.doFilterInternal(request, response, filterChain); + + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication.getPrincipal()).isNotNull(); + assertThat(authentication.getPrincipal()).isExactlyInstanceOf(UserPrincipal.class); + assertThat(authentication.getAuthorities()).isNotNull(); + assertThat(authentication.getAuthorities().size()).isEqualTo(2); + assertThat(authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_group1")) + && authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_group2")) + ).isTrue(); + + final UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + assertThat(principal.getIssuer()).isNotNull().isNotEmpty(); + assertThat(principal.getKid()).isNotNull().isNotEmpty(); + assertThat(principal.getSubject()).isNotNull().isNotEmpty(); + + assertThat(principal.getClaims()).isNotNull().isNotEmpty(); + final Map claims = principal.getClaims(); + assertThat(claims.get("iss")).isEqualTo(principal.getIssuer()); + assertThat(claims.get("sub")).isEqualTo(principal.getSubject()); + }); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java new file mode 100644 index 000000000000..17e75d9374c8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import io.jsonwebtoken.lang.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePropertySource; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AADOAuth2ConfigTest { + private static final String AAD_OAUTH2_MINIMUM_PROPS = "aad-backend-oauth2-minimum.properties"; + private Resource testResource; + private ResourcePropertySource testPropResource; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private AnnotationConfigWebApplicationContext testContext; + + @Before + public void setup() throws Exception { + testResource = new ClassPathResource(AAD_OAUTH2_MINIMUM_PROPS); + testPropResource = new ResourcePropertySource("test", testResource); + } + + @After + public void clear() { + if (testContext != null) { + testContext.close(); + } + } + + @Test + public void noOAuth2UserServiceBeanCreatedIfPropsNotConfigured() { + final AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(AADOAuth2AutoConfiguration.class); + context.refresh(); + + exception.expect(NoSuchBeanDefinitionException.class); + context.getBean(OAuth2UserService.class); + } + + @Test + public void testOAuth2UserServiceBeanCreatedIfPropsConfigured() { + testContext = initTestContext(); + Assert.notNull(testContext.getBean(OAuth2UserService.class)); + } + + @Test + public void noOAuth2UserServiceBeanCreatedIfTenantIdNotConfigured() { + testPropResource.getSource().remove(Constants.TENANT_ID_PROPERTY); + testContext = initTestContext(); + + exception.expect(NoSuchBeanDefinitionException.class); + testContext.getBean(OAuth2UserService.class); + } + + @Test + public void testEndpointsPropertiesLoadAndOverridable() { + testContext = initTestContext("azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/", + "azure.service.endpoints.global.aadSigninUri=https://test/", + "azure.service.endpoints.global.aadGraphApiUri=https://test/", + "azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/", + "azure.service.endpoints.global.aadMembershipRestUri=https://test/"); + + + final Environment environment = testContext.getEnvironment(); + assertThat(environment.getProperty("azure.service.endpoints.global.aadSigninUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadGraphApiUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadKeyDiscoveryUri")) + .isEqualTo("https://test/"); + assertThat(environment.getProperty("azure.service.endpoints.global.aadMembershipRestUri")) + .isEqualTo("https://test/"); + final ServiceEndpointsProperties serviceEndpointsProperties = + testContext.getBean(ServiceEndpointsProperties.class); + assertThat(serviceEndpointsProperties).isNotNull(); + assertThat(serviceEndpointsProperties.getEndpoints()).isNotEmpty(); + + final Map endpoints = serviceEndpointsProperties.getEndpoints(); + assertThat(endpoints).hasSize(4); + assertThat(endpoints.get("cn")).isNotNull() + .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri, + ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri) + .containsExactly("https://graph.chinacloudapi.cn/", + "https://login.partner.microsoftonline.cn/common/discovery/keys", + "https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6", + "https://login.partner.microsoftonline.cn/"); + assertThat(endpoints.get("global")).isNotNull() + .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri, + ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri) + .containsExactly("https://test/", "https://test/", "https://test/", "https://test/"); + + } + + private AnnotationConfigWebApplicationContext initTestContext(String... environment) { + final AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + + context.getEnvironment().getPropertySources().addLast(testPropResource); + if (environment.length > 0) { + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, environment); + } + + context.register(AADOAuth2AutoConfiguration.class); + context.refresh(); + + return context; + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java new file mode 100644 index 000000000000..5b12edc5a1ca --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThatCode; + +public class AADUserGroupsPropertyValidatorTest { + + private AADAuthenticationProperties aadAuthenticationProperties; + + @Before + public void setUp() { + aadAuthenticationProperties = new AADAuthenticationProperties(); + } + + @Test + public void isValidNoGroupsDefined() { + assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()) + .isInstanceOf(IllegalStateException.class).hasMessage( + "One of the User Group Properties must be populated. " + + "Please populate azure.activedirectory.user-group.allowed-groups"); + } + + @Test + public void isValidDeprecatedPropertySet() { + aadAuthenticationProperties.setActiveDirectoryGroups(Collections.singletonList("user-group")); + assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException(); + } + + @Test + public void isValidUserGroupPropertySet() { + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("user-group")); + assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException(); + } + + @Test + public void isValidBothUserGroupPropertiesSet() { + aadAuthenticationProperties.setActiveDirectoryGroups(Collections.singletonList("user-group")); + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("user-group")); + assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException(); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java new file mode 100644 index 000000000000..36635f6e1203 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.GrantedAuthority; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class AzureADGraphClientTest { + + private AzureADGraphClient adGraphClient; + + private AADAuthenticationProperties aadAuthProps; + + @Mock + private ServiceEndpointsProperties endpointsProps; + + @Before + public void setup() { + final List activeDirectoryGroups = new ArrayList<>(); + activeDirectoryGroups.add("Test_Group"); + aadAuthProps = new AADAuthenticationProperties(); + aadAuthProps.setActiveDirectoryGroups(activeDirectoryGroups); + adGraphClient = new AzureADGraphClient("client", "pass", aadAuthProps, endpointsProps); + } + + @Test + public void testConvertGroupToGrantedAuthorities() { + + final List userGroups = Collections.singletonList( + new UserGroup("testId", "Test_Group")); + + final Set authorities = adGraphClient.convertGroupsToGrantedAuthorities(userGroups); + assertThat(authorities).hasSize(1).extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_Test_Group"); + } + + @Test + public void testConvertGroupToGrantedAuthoritiesUsingAllowedGroups() { + final List userGroups = Arrays + .asList(new UserGroup("testId", "Test_Group"), + new UserGroup("testId", "Another_Group")); + aadAuthProps.getUserGroup().getAllowedGroups().add("Another_Group"); + final Set authorities = adGraphClient.convertGroupsToGrantedAuthorities(userGroups); + assertThat(authorities).hasSize(2).extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_Test_Group", "ROLE_Another_Group"); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java new file mode 100644 index 000000000000..9dab47388ef8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import java.util.Arrays; +import java.util.List; + +public class Constants { + public static final String SERVICE_ENVIRONMENT_PROPERTY = "azure.activedirectory.environment"; + public static final String CLIENT_ID_PROPERTY = "azure.activedirectory.client-id"; + public static final String CLIENT_SECRET_PROPERTY = "azure.activedirectory.client-secret"; + public static final String TARGETED_GROUPS_PROPERTY = "azure.activedirectory.active-directory-groups"; + public static final String TENANT_ID_PROPERTY = "azure.activedirectory.tenant-id"; + + public static final String DEFAULT_ENVIRONMENT = "global"; + public static final String CLIENT_ID = "real_client_id"; + public static final String CLIENT_SECRET = "real_client_secret"; + public static final List TARGETED_GROUPS = Arrays.asList("group1", "group2", "group3"); + + public static final String TOKEN_HEADER = "Authorization"; + public static final String BEARER_TOKEN = "Bearer real_jtw_bearer_token"; + + public static final String USERGROUPS_JSON = "{\n" + + " \"odata.metadata\": \"https://graph.windows.net/myorganization/$metadata#directoryObjects\",\n" + + " \"value\": [\n" + + " {\n" + + " \"odata.type\": \"Microsoft.DirectoryServices.Group\",\n" + + " \"objectType\": \"Group\",\n" + + " \"objectId\": \"12345678-7baf-48ce-96f4-a2d60c26391e\",\n" + + " \"deletionTimestamp\": null,\n" + + " \"description\": \"this is group1\",\n" + + " \"dirSyncEnabled\": true,\n" + + " \"displayName\": \"group1\",\n" + + " \"lastDirSyncTime\": \"2017-08-02T12:54:37Z\",\n" + + " \"mail\": null,\n" + + " \"mailNickname\": \"something\",\n" + + " \"mailEnabled\": false,\n" + + " \"onPremisesDomainName\": null,\n" + + " \"onPremisesNetBiosName\": null,\n" + + " \"onPremisesSamAccountName\": null,\n" + + " \"onPremisesSecurityIdentifier\": \"S-1-5-21-1234567885-903363285-719344707-285039\",\n" + + " \"provisioningErrors\": [],\n" + + " \"proxyAddresses\": [],\n" + + " \"securityEnabled\": true\n" + + " },\n" + + " {\n" + + " \"odata.type\": \"Microsoft.DirectoryServices.Group\",\n" + + " \"objectType\": \"Group\",\n" + + " \"objectId\": \"12345678-e757-4474-b9c4-3f00a9ac17a0\",\n" + + " \"deletionTimestamp\": null,\n" + + " \"description\": null,\n" + + " \"dirSyncEnabled\": true,\n" + + " \"displayName\": \"group2\",\n" + + " \"lastDirSyncTime\": \"2017-08-09T13:45:03Z\",\n" + + " \"mail\": null,\n" + + " \"mailNickname\": \"somethingelse\",\n" + + " \"mailEnabled\": false,\n" + + " \"onPremisesDomainName\": null,\n" + + " \"onPremisesNetBiosName\": null,\n" + + " \"onPremisesSamAccountName\": null,\n" + + " \"onPremisesSecurityIdentifier\": \"S-1-5-21-1234567885-903363285-719344707-28565\",\n" + + " \"provisioningErrors\": [],\n" + + " \"proxyAddresses\": [],\n" + + " \"securityEnabled\": true\n" + + " },\n" + + " {\n" + + " \"odata.type\": \"Microsoft.DirectoryServices.Group\",\n" + + " \"objectType\": \"Group\",\n" + + " \"objectId\": \"12345678-86a4-4237-aeb0-60bad29c1de0\",\n" + + " \"deletionTimestamp\": null,\n" + + " \"description\": \"this is group3\",\n" + + " \"dirSyncEnabled\": true,\n" + + " \"displayName\": \"group3\",\n" + + " \"lastDirSyncTime\": \"2017-08-09T05:41:43Z\",\n" + + " \"mail\": null,\n" + + " \"mailNickname\": \"somethingelse\",\n" + + " \"mailEnabled\": false,\n" + + " \"onPremisesDomainName\": null,\n" + + " \"onPremisesNetBiosName\": null,\n" + + " \"onPremisesSamAccountName\": null,\n" + + " \"onPremisesSecurityIdentifier\": \"S-1-5-21-1234567884-1604012920-1887927527-14401381\",\n" + + " \"provisioningErrors\": [],\n" + + " \"proxyAddresses\": [],\n" + + " \"securityEnabled\": true\n" + + " }" + + "],\n" + + " \"odata.nextLink\": \"directoryObjects/$/Microsoft.DirectoryServices.User/" + + "12345678-2898-434a-a370-8ec974c2fb57/memberOf?$skiptoken=X'4453707407000100000000" + + "00000000100000009D29CBA7B45D854A84FF7F9B636BD9DC000000000000000000000017312E322E3" + + "834302E3131333535362E312E342E3233333100000000'\"\n" + + "}"; + + /** Token from https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-id-and-access-tokens */ + public static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1" + + "iYTlnb0VLWSJ9.eyJhdWQiOiI2NzMxZGU3Ni0xNGE2LTQ5YWUtOTdiYy02ZWJhNjkxNDM5MWUiLCJpc3MiOiJodHRwczovL2xvZ2lu" + + "Lm1pY3Jvc29mdG9ubGluZS5jb20vYjk0MTk4MTgtMDlhZi00OWMyLWIwYzMtNjUzYWRjMWYzNzZlL3YyLjAiLCJpYXQiOjE0NTIyOD" + + "UzMzEsIm5iZiI6MTQ1MjI4NTMzMSwiZXhwIjoxNDUyMjg5MjMxLCJuYW1lIjoiQmFiZSBSdXRoIiwibm9uY2UiOiIxMjM0NSIsIm9p" + + "ZCI6ImExZGJkZGU4LWU0ZjktNDU3MS1hZDkzLTMwNTllMzc1MGQyMyIsInByZWZlcnJlZF91c2VybmFtZSI6InRoZWdyZWF0YmFtYm" + + "lub0BueXkub25taWNyb3NvZnQuY29tIiwic3ViIjoiTUY0Zi1nZ1dNRWppMTJLeW5KVU5RWnBoYVVUdkxjUXVnNWpkRjJubDAxUSIs" + + "InRpZCI6ImI5NDE5ODE4LTA5YWYtNDljMi1iMGMzLTY1M2FkYzFmMzc2ZSIsInZlciI6IjIuMCJ9.p_rYdrtJ1oCmgDBggNHB9O38K" + + "TnLCMGbMDODdirdmZbmJcTHiZDdtTc-hguu3krhbtOsoYM2HJeZM3Wsbp_YcfSKDY--X_NobMNsxbT7bqZHxDnA2jTMyrmt5v2EKUn" + + "EeVtSiJXyO3JWUq9R0dO-m4o9_8jGP6zHtR62zLaotTBYHmgeKpZgTFB9WtUq8DVdyMn_HSvQEfz-LWqckbcTwM_9RNKoGRVk38KCh" + + "VJo4z5LkksYRarDo8QgQ7xEKmYmPvRr_I7gvM2bmlZQds2OeqWLB1NSNbFZqyFOCgYn3bAQ-nEQSKwBaA36jYGPOVG2r2Qv1uKcpSO" + + "xzxaQybzYpQ"; +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java new file mode 100644 index 000000000000..1550066179e5 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import java.util.Arrays; +import java.util.List; + +public class MicrosoftGraphConstants { + public static final String SERVICE_ENVIRONMENT_PROPERTY = "azure.activedirectory.environment"; + public static final String CLIENT_ID_PROPERTY = "azure.activedirectory.client-id"; + public static final String CLIENT_SECRET_PROPERTY = "azure.activedirectory.client-secret"; + public static final String TARGETED_GROUPS_PROPERTY = "azure.activedirectory.active-directory-groups"; + public static final String TENANT_ID_PROPERTY = "azure.activedirectory.tenant-id"; + + public static final String DEFAULT_ENVIRONMENT = "global"; + public static final String CLIENT_ID = "real_client_id"; + public static final String CLIENT_SECRET = "real_client_secret"; + public static final List TARGETED_GROUPS = Arrays.asList("group1", "group2", "group3"); + + public static final String TOKEN_HEADER = "Authorization"; + public static final String BEARER_TOKEN = "Bearer real_jtw_bearer_token"; + + public static final String USERGROUPS_JSON = "{\n" + + " \"odata.metadata\": \"https://graph.windows.net/myorganization/$metadata#directoryObjects\",\n" + + " \"value\": [\n" + + " {\n" + + " \"@odata.type\": \"#microsoft.graph.group\",\n" + + " \"id\": \"12345678-7baf-48ce-96f4-a2d60c26391e\",\n" + + " \"deletedDateTime\": null,\n" + + " \"classification\": null,\n" + + " \"createdDateTime\": \"2017-08-02T12:54:37Z\",\n" + + " \"creationOptions\": [],\n" + + " \"description\": \"this is group1\",\n" + + " \"displayName\": \"group1\",\n" + + " \"groupTypes\": [],\n" + + " \"mail\": null,\n" + + " \"mailEnabled\": false,\n" + + " \"mailNickname\": \"something\",\n" + + " \"onPremisesLastSyncDateTime\": null,\n" + + " \"onPremisesSecurityIdentifier\": null,\n" + + " \"onPremisesSyncEnabled\": null,\n" + + " \"preferredDataLocation\": null,\n" + + " \"proxyAddresses\": [],\n" + + " \"renewedDateTime\": \"2017-08-02T12:54:37Z\",\n" + + " \"resourceBehaviorOptions\": [],\n" + + " \"resourceProvisioningOptions\": [],\n" + + " \"securityEnabled\": true,\n" + + " \"visibility\": null,\n" + + " \"onPremisesProvisioningErrors\": []\n" + + " },\n" + + " {\n" + + " \"@odata.type\": \"#microsoft.graph.group\",\n" + + " \"id\": \"12345678-e757-4474-b9c4-3f00a9ac17a0\",\n" + + " \"deletedDateTime\": null,\n" + + " \"classification\": null,\n" + + " \"createdDateTime\": \"2017-08-09T13:45:03Z\",\n" + + " \"creationOptions\": [],\n" + + " \"description\": \"this is group2\",\n" + + " \"displayName\": \"group2\",\n" + + " \"groupTypes\": [],\n" + + " \"mail\": null,\n" + + " \"mailEnabled\": false,\n" + + " \"mailNickname\": \"somethingelse\",\n" + + " \"onPremisesLastSyncDateTime\": null,\n" + + " \"onPremisesSecurityIdentifier\": null,\n" + + " \"onPremisesSyncEnabled\": null,\n" + + " \"preferredDataLocation\": null,\n" + + " \"proxyAddresses\": [],\n" + + " \"renewedDateTime\": \"2017-08-09T13:45:03Z\",\n" + + " \"resourceBehaviorOptions\": [],\n" + + " \"resourceProvisioningOptions\": [],\n" + + " \"securityEnabled\": true,\n" + + " \"visibility\": null,\n" + + " \"onPremisesProvisioningErrors\": []\n" + + " },\n" + + " {\n" + + " \"@odata.type\": \"#microsoft.graph.group\",\n" + + " \"id\": \"12345678-86a4-4237-aeb0-60bad29c1de0\",\n" + + " \"deletedDateTime\": null,\n" + + " \"classification\": null,\n" + + " \"createdDateTime\": \"2017-08-09T05:41:43Z\",\n" + + " \"creationOptions\": [],\n" + + " \"description\": \"this is group3\",\n" + + " \"displayName\": \"group3\",\n" + + " \"groupTypes\": [],\n" + + " \"mail\": null,\n" + + " \"mailEnabled\": false,\n" + + " \"mailNickname\": \"somethingelse\",\n" + + " \"onPremisesLastSyncDateTime\": null,\n" + + " \"onPremisesSecurityIdentifier\": null,\n" + + " \"onPremisesSyncEnabled\": null,\n" + + " \"preferredDataLocation\": null,\n" + + " \"proxyAddresses\": [],\n" + + " \"renewedDateTime\": \"2017-08-09T05:41:43Z\",\n" + + " \"resourceBehaviorOptions\": [],\n" + + " \"resourceProvisioningOptions\": [],\n" + + " \"securityEnabled\": true,\n" + + " \"visibility\": null,\n" + + " \"onPremisesProvisioningErrors\": []\n" + + " }" + + "],\n" + + " \"odata.nextLink\": \"directoryObjects/$/Microsoft.DirectoryServices.User/" + + "12345678-2898-434a-a370-8ec974c2fb57/memberOf?$skiptoken=X'4453707407000100000000" + + "00000000100000009D29CBA7B45D854A84FF7F9B636BD9DC000000000000000000000017312E322E3" + + "834302E3131333535362E312E342E3233333100000000'\"\n" + + "}"; + + /** Token from https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-id-and-access-tokens */ + public static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1" + + "iYTlnb0VLWSJ9.eyJhdWQiOiI2NzMxZGU3Ni0xNGE2LTQ5YWUtOTdiYy02ZWJhNjkxNDM5MWUiLCJpc3MiOiJodHRwczovL2xvZ2lu" + + "Lm1pY3Jvc29mdG9ubGluZS5jb20vYjk0MTk4MTgtMDlhZi00OWMyLWIwYzMtNjUzYWRjMWYzNzZlL3YyLjAiLCJpYXQiOjE0NTIyOD" + + "UzMzEsIm5iZiI6MTQ1MjI4NTMzMSwiZXhwIjoxNDUyMjg5MjMxLCJuYW1lIjoiQmFiZSBSdXRoIiwibm9uY2UiOiIxMjM0NSIsIm9p" + + "ZCI6ImExZGJkZGU4LWU0ZjktNDU3MS1hZDkzLTMwNTllMzc1MGQyMyIsInByZWZlcnJlZF91c2VybmFtZSI6InRoZWdyZWF0YmFtYm" + + "lub0BueXkub25taWNyb3NvZnQuY29tIiwic3ViIjoiTUY0Zi1nZ1dNRWppMTJLeW5KVU5RWnBoYVVUdkxjUXVnNWpkRjJubDAxUSIs" + + "InRpZCI6ImI5NDE5ODE4LTA5YWYtNDljMi1iMGMzLTY1M2FkYzFmMzc2ZSIsInZlciI6IjIuMCJ9.p_rYdrtJ1oCmgDBggNHB9O38K" + + "TnLCMGbMDODdirdmZbmJcTHiZDdtTc-hguu3krhbtOsoYM2HJeZM3Wsbp_YcfSKDY--X_NobMNsxbT7bqZHxDnA2jTMyrmt5v2EKUn" + + "EeVtSiJXyO3JWUq9R0dO-m4o9_8jGP6zHtR62zLaotTBYHmgeKpZgTFB9WtUq8DVdyMn_HSvQEfz-LWqckbcTwM_9RNKoGRVk38KCh" + + "VJo4z5LkksYRarDo8QgQ7xEKmYmPvRr_I7gvM2bmlZQds2OeqWLB1NSNbFZqyFOCgYn3bAQ-nEQSKwBaA36jYGPOVG2r2Qv1uKcpSO" + + "xzxaQybzYpQ"; +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java new file mode 100644 index 000000000000..c513fc475ebf --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.ResourceRetriever; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ResourceRetrieverTest { + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class)) + .withPropertyValues("azure.activedirectory.client-id=fake-client-id", + "azure.activedirectory.client-secret=fake-client-secret", + "azure.activedirectory.active-directory-groups=fake-group", + "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri"); + + @Test + public void resourceRetrieverDefaultConfig() { + this.contextRunner.run(context -> { + assertThat(context).hasSingleBean(ResourceRetriever.class); + final ResourceRetriever retriever = context.getBean(ResourceRetriever.class); + assertThat(retriever).isInstanceOf(DefaultResourceRetriever.class); + + final DefaultResourceRetriever defaultRetriever = (DefaultResourceRetriever) retriever; + assertThat(defaultRetriever.getConnectTimeout()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT); + assertThat(defaultRetriever.getReadTimeout()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT); + assertThat(defaultRetriever.getSizeLimit()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT); + }); + } + + @Test + public void resourceRetriverIsConfigurable() { + this.contextRunner.withPropertyValues("azure.activedirectory.jwt-connect-timeout=1234", + "azure.activedirectory.jwt-read-timeout=1234", + "azure.activedirectory.jwt-size-limit=123400", + "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri") + .run(context -> { + assertThat(context).hasSingleBean(ResourceRetriever.class); + final ResourceRetriever retriever = context.getBean(ResourceRetriever.class); + assertThat(retriever).isInstanceOf(DefaultResourceRetriever.class); + + final DefaultResourceRetriever defaultRetriever = (DefaultResourceRetriever) retriever; + assertThat(defaultRetriever.getConnectTimeout()).isEqualTo(1234); + assertThat(defaultRetriever.getReadTimeout()).isEqualTo(1234); + assertThat(defaultRetriever.getSizeLimit()).isEqualTo(123400); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java new file mode 100644 index 000000000000..3defb2d8247e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import org.junit.Assert; +import org.junit.Test; + +public class UserGroupTest { + private static final UserGroup GROUP_1 = new UserGroup("12345", "test"); + + @Test + public void getDisplayName() { + Assert.assertEquals("test", GROUP_1.getDisplayName()); + } + + @Test + public void getObjectID() { + Assert.assertEquals("12345", GROUP_1.getObjectID()); + } + + @Test + public void equals() { + final UserGroup group2 = new UserGroup("12345", "test"); + Assert.assertEquals(GROUP_1, group2); + } + + @Test + public void hashCodeTest() { + final UserGroup group2 = new UserGroup("12345", "test"); + Assert.assertEquals(GROUP_1.hashCode(), group2.hashCode()); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java new file mode 100644 index 000000000000..e790cbb70b0e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Resource; +import com.nimbusds.jose.util.ResourceRetriever; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; + +public class UserPrincipalManagerAudienceTest { + + private static final String FAKE_CLIENT_ID = "dsflkjsdflkjsdf"; + private static final String FAKE_APPLICATION_URI = "https://oihiugjuzfvbhg"; + + private JWSSigner signer; + private String jwkString; + private ResourceRetriever resourceRetriever; + + private ServiceEndpointsProperties serviceEndpointsProperties; + private AADAuthenticationProperties aadAuthenticationProperties; + private UserPrincipalManager userPrincipalManager; + + @Before + public void setupKeys() throws NoSuchAlgorithmException, IOException { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + + final KeyPair kp = kpg.genKeyPair(); + final RSAPrivateKey privateKey = (RSAPrivateKey) kp.getPrivate(); + + signer = new RSASSASigner(privateKey); + + final RSAKey rsaJWK = new RSAKey.Builder((RSAPublicKey) kp.getPublic()) + .privateKey((RSAPrivateKey) kp.getPrivate()) + .keyID("1") + .build(); + final JWKSet jwkSet = new JWKSet(rsaJWK); + jwkString = jwkSet.toString(); + + resourceRetriever = url -> new Resource(jwkString, "application/json"); + + serviceEndpointsProperties = mock(ServiceEndpointsProperties.class); + aadAuthenticationProperties = new AADAuthenticationProperties(); + aadAuthenticationProperties.setClientId(FAKE_CLIENT_ID); + aadAuthenticationProperties.setAppIdUri(FAKE_APPLICATION_URI); + final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); + serviceEndpoints.setAadKeyDiscoveryUri("file://dummy"); + when(serviceEndpointsProperties.getServiceEndpoints(anyString())).thenReturn(serviceEndpoints); + } + + @Test + public void allowApplicationUriAsAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_CLIENT_ID) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .doesNotThrowAnyException(); + } + + @Test + public void allowClientIdAsAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_APPLICATION_URI) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .doesNotThrowAnyException(); + } + + @Test + public void failWithUnkownAudience() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience("unknown audience") + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo)) + .hasMessageContaining("Invalid token audience."); + } + + @Test + public void failOnInvalidSiganture() throws JOSEException { + final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder() + .subject("foo") + .issueTime(Date.from(Instant.now().minusSeconds(60))) + .issuer("https://sts.windows.net/") + .audience(FAKE_APPLICATION_URI) + .build(); + final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne); + signedJWT.sign(signer); + + final String orderTwo = signedJWT.serialize(); + final String invalidToken = orderTwo.substring(0, orderTwo.length() - 5); + + userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties, + resourceRetriever, true); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(invalidToken)) + .hasMessageContaining("JWT rejected: Invalid signature"); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java new file mode 100644 index 000000000000..5364aeb0b8d6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.BadJWTException; +import junitparams.FileParameters; +import junitparams.JUnitParamsRunner; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@RunWith(JUnitParamsRunner.class) +public class UserPrincipalManagerTest { + + private static ImmutableJWKSet immutableJWKSet; + + @BeforeClass + public static void setupClass() throws Exception { + final X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(Files.newInputStream(Paths.get("src/test/resources/test-public-key.txt"))); + immutableJWKSet = new ImmutableJWKSet<>(new JWKSet(JWK.parse( + cert))); + } + + private UserPrincipalManager userPrincipalManager; + + + @Test + public void testAlgIsTakenFromJWT() throws Exception { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + final UserPrincipal userPrincipal = userPrincipalManager.buildUserPrincipal( + new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-signed.txt")), StandardCharsets.UTF_8)); + assertThat(userPrincipal).isNotNull().extracting(UserPrincipal::getIssuer, UserPrincipal::getSubject) + .containsExactly("https://sts.windows.net/test", "test@example.com"); + } + + @Test + public void invalidIssuer() { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal( + new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-bad-issuer.txt")), StandardCharsets.UTF_8))) + .isInstanceOf(BadJWTException.class); + } + + @Test + //TODO: add more generated tokens with other valid issuers to this file. Didn't manage to generate them + @FileParameters("src/test/resources/jwt-valid-issuer.txt") + public void validIssuer(final String token) { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal(token)) + .doesNotThrowAnyException(); + } + + @Test + public void nullIssuer() { + userPrincipalManager = new UserPrincipalManager(immutableJWKSet); + assertThatCode(() -> userPrincipalManager.buildUserPrincipal( + new String(Files.readAllBytes( + Paths.get("src/test/resources/jwt-null-issuer.txt")), StandardCharsets.UTF_8))) + .isInstanceOf(BadJWTException.class); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java new file mode 100644 index 000000000000..05047a3a4985 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.Assertions.assertThat; + + +public class UserPrincipalMicrosoftGraphTest { + private AzureADGraphClient graphClientMock; + + @Rule + public WireMockRule wireMockRule = new WireMockRule(9519); + + private String clientId; + private String clientSecret; + private AADAuthenticationProperties aadAuthProps; + private ServiceEndpointsProperties endpointsProps; + private String accessToken; + + + @Before + public void setup() { + accessToken = MicrosoftGraphConstants.BEARER_TOKEN; + aadAuthProps = new AADAuthenticationProperties(); + aadAuthProps.setEnvironment("global-v2-graph"); + aadAuthProps.getUserGroup().setKey("@odata.type"); + aadAuthProps.getUserGroup().setValue("#microsoft.graph.group"); + aadAuthProps.getUserGroup().setObjectIDKey("id"); + endpointsProps = new ServiceEndpointsProperties(); + final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); + serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf"); + endpointsProps.getEndpoints().put("global-v2-graph", serviceEndpoints); + clientId = "client"; + clientSecret = "pass"; + } + + + @Test + public void getAuthoritiesByUserGroups() throws Exception { + + aadAuthProps.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); + this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps); + + stubFor(get(urlEqualTo("/memberOf")).withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON_VALUE)) + .willReturn(aResponse().withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(MicrosoftGraphConstants.USERGROUPS_JSON))); + + assertThat(graphClientMock.getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN)).isNotEmpty() + .extracting(GrantedAuthority::getAuthority).containsExactly("ROLE_group1"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(HttpHeaders.AUTHORIZATION, + equalTo(String.format("%s %s", OAuth2AccessToken.TokenType.BEARER.getValue(), accessToken))) + .withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON_VALUE))); + } + + @Test + public void getGroups() throws Exception { + + aadAuthProps.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3")); + this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps); + + stubFor(get(urlEqualTo("/memberOf")).withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON_VALUE)) + .willReturn(aResponse().withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(MicrosoftGraphConstants.USERGROUPS_JSON))); + final Collection authorities = graphClientMock + .getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN); + + assertThat(authorities).isNotEmpty().extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_group1", "ROLE_group2", "ROLE_group3"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(HttpHeaders.AUTHORIZATION, + equalTo(String.format("%s %s", OAuth2AccessToken.TokenType.BEARER.getValue(), accessToken))) + .withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON_VALUE))); + } + + @Test + public void userPrinciplaIsSerializable() throws ParseException, IOException, ClassNotFoundException { + final File tmpOutputFile = File.createTempFile("test-user-principal", "txt"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpOutputFile); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + FileInputStream fileInputStream = new FileInputStream(tmpOutputFile); + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { + + final JWSObject jwsObject = JWSObject.parse(MicrosoftGraphConstants.JWT_TOKEN); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("fake-subject").build(); + final UserPrincipal principal = new UserPrincipal(jwsObject, jwtClaimsSet); + + objectOutputStream.writeObject(principal); + + final UserPrincipal serializedPrincipal = (UserPrincipal) objectInputStream.readObject(); + + Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal); + Assert.assertTrue("Serialized UserPrincipal kid not empty", + !StringUtils.isEmpty(serializedPrincipal.getKid())); + Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims()); + Assert.assertTrue("Serialized UserPrincipal claims not empty.", + serializedPrincipal.getClaims().size() > 0); + } finally { + Files.deleteIfExists(tmpOutputFile.toPath()); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalTest.java new file mode 100644 index 000000000000..e1ca1f95d89c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalTest.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.aad; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jwt.JWTClaimsSet; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.Assertions.assertThat; + + +public class UserPrincipalTest { + private AzureADGraphClient graphClientMock; + + @Rule + public WireMockRule wireMockRule = new WireMockRule(9519); + + private String clientId; + private String clientSecret; + private AADAuthenticationProperties aadAuthProps; + private ServiceEndpointsProperties endpointsProps; + private String accessToken; + + + @Before + public void setup() { + accessToken = Constants.BEARER_TOKEN; + aadAuthProps = new AADAuthenticationProperties(); + endpointsProps = new ServiceEndpointsProperties(); + final ServiceEndpoints serviceEndpoints = new ServiceEndpoints(); + serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf"); + endpointsProps.getEndpoints().put("global", serviceEndpoints); + clientId = "client"; + clientSecret = "pass"; + } + + + @Test + public void getAuthoritiesByUserGroups() throws Exception { + + aadAuthProps.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); + this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps); + + stubFor(get(urlEqualTo("/memberOf")).withHeader(HttpHeaders.ACCEPT, + equalTo("application/json;odata=minimalmetadata")) + .willReturn(aResponse().withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(Constants.USERGROUPS_JSON))); + + assertThat(graphClientMock.getGrantedAuthorities(Constants.BEARER_TOKEN)).isNotEmpty() + .extracting(GrantedAuthority::getAuthority).containsExactly("ROLE_group1"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(HttpHeaders.AUTHORIZATION, + equalTo(String.format("%s", accessToken))) + .withHeader(HttpHeaders.ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .withHeader("api-version", equalTo("1.6"))); + } + + @Test + public void getGroups() throws Exception { + + aadAuthProps.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3")); + this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps); + + stubFor(get(urlEqualTo("/memberOf")).withHeader(HttpHeaders.ACCEPT, + equalTo("application/json;odata=minimalmetadata")) + .willReturn(aResponse().withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(Constants.USERGROUPS_JSON))); + final Collection authorities = graphClientMock + .getGrantedAuthorities(Constants.BEARER_TOKEN); + + assertThat(authorities).isNotEmpty().extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_group1", "ROLE_group2", "ROLE_group3"); + + verify(getRequestedFor(urlMatching("/memberOf")) + .withHeader(HttpHeaders.AUTHORIZATION, + equalTo(String.format("%s", accessToken))) + .withHeader(HttpHeaders.ACCEPT, equalTo("application/json;odata=minimalmetadata")) + .withHeader("api-version", equalTo("1.6"))); + } + + @Test + public void userPrinciplaIsSerializable() throws ParseException, IOException, ClassNotFoundException { + final File tmpOutputFile = File.createTempFile("test-user-principal", "txt"); + + try (FileOutputStream fileOutputStream = new FileOutputStream(tmpOutputFile); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); + FileInputStream fileInputStream = new FileInputStream(tmpOutputFile); + ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) { + + final JWSObject jwsObject = JWSObject.parse(Constants.JWT_TOKEN); + final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("fake-subject").build(); + final UserPrincipal principal = new UserPrincipal(jwsObject, jwtClaimsSet); + + objectOutputStream.writeObject(principal); + + final UserPrincipal serializedPrincipal = (UserPrincipal) objectInputStream.readObject(); + + Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal); + Assert.assertTrue("Serialized UserPrincipal kid not empty", + !StringUtils.isEmpty(serializedPrincipal.getKid())); + Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims()); + Assert.assertTrue("Serialized UserPrincipal claims not empty.", + serializedPrincipal.getClaims().size() > 0); + } finally { + Files.deleteIfExists(tmpOutputFile.toPath()); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolverTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolverTest.java new file mode 100644 index 000000000000..367c69e719ca --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolverTest.java @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; + +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CConstants.*; +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class AADB2CAuthorizationRequestResolverTest { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADB2CAutoConfiguration.class)) + .withPropertyValues( + String.format("%s=%s", TENANT, TEST_TENANT), + String.format("%s=%s", CLIENT_ID, TEST_CLIENT_ID), + String.format("%s=%s", CLIENT_SECRET, TEST_CLIENT_SECRET), + String.format("%s=%s", REPLY_URL, TEST_REPLY_URL), + String.format("%s=%s", LOGOUT_SUCCESS_URL, TEST_LOGOUT_SUCCESS_URL), + String.format("%s=%s", SIGN_UP_OR_SIGN_IN, TEST_SIGN_UP_OR_IN_NAME) + ); + + private HttpServletRequest getHttpServletRequest(String uri) { + Assert.hasText(uri, "uri must contain text."); + + final MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.toString(), uri); + + request.setServletPath(uri); + + return request; + } + + @Test + public void testAutoConfigurationBean() { + this.contextRunner.run(c -> { + String requestUri = "/fake-url"; + HttpServletRequest request = getHttpServletRequest(requestUri); + final String registrationId = TEST_SIGN_UP_OR_IN_NAME; + final AADB2CAuthorizationRequestResolver resolver = c.getBean(AADB2CAuthorizationRequestResolver.class); + + assertThat(resolver).isNotNull(); + assertThat(resolver.resolve(request)).isNull(); + assertThat(resolver.resolve(request, registrationId)).isNull(); + + requestUri = "/oauth2/authorization/" + TEST_SIGN_UP_OR_IN_NAME; + request = getHttpServletRequest(requestUri); + + assertThat(resolver.resolve(request)).isNotNull(); + assertThat(resolver.resolve(request, registrationId)).isNotNull(); + + assertThat(resolver.resolve(request).getAdditionalParameters().get("p")).isEqualTo(TEST_SIGN_UP_OR_IN_NAME); + assertThat(resolver.resolve(request).getClientId()).isEqualTo(TEST_CLIENT_ID); + assertThat(resolver.resolve(request).getRedirectUri()).isEqualTo(TEST_REPLY_URL); + assertThat(resolver.resolve(request).getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(resolver.resolve(request).getScopes()).contains("openid", TEST_CLIENT_ID); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfigurationTest.java new file mode 100644 index 000000000000..60114aad61bf --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfigurationTest.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CConstants.*; +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class AADB2CAutoConfigurationTest { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADB2CAutoConfiguration.class)) + .withPropertyValues( + String.format("%s=%s", TENANT, TEST_TENANT), + String.format("%s=%s", CLIENT_ID, TEST_CLIENT_ID), + String.format("%s=%s", CLIENT_SECRET, TEST_CLIENT_SECRET), + String.format("%s=%s", REPLY_URL, TEST_REPLY_URL), + String.format("%s=%s", LOGOUT_SUCCESS_URL, TEST_LOGOUT_SUCCESS_URL), + String.format("%s=%s", SIGN_UP_OR_SIGN_IN, TEST_SIGN_UP_OR_IN_NAME) + ); + + @Test + public void testAutoConfigurationBean() { + this.contextRunner.run(c -> { + final AADB2CAutoConfiguration config = c.getBean(AADB2CAutoConfiguration.class); + + assertThat(config).isNotNull(); + }); + } + + @Test + public void testPropertiesBean() { + this.contextRunner.run(c -> { + final AADB2CProperties properties = c.getBean(AADB2CProperties.class); + + assertThat(properties).isNotNull(); + assertThat(properties.getTenant()).isEqualTo(TEST_TENANT); + assertThat(properties.getClientId()).isEqualTo(TEST_CLIENT_ID); + assertThat(properties.getClientSecret()).isEqualTo(TEST_CLIENT_SECRET); + assertThat(properties.getReplyUrl()).isEqualTo(TEST_REPLY_URL); + + final String signUpOrSignIn = properties.getUserFlows().getSignUpOrSignIn(); + + assertThat(signUpOrSignIn).isEqualTo(TEST_SIGN_UP_OR_IN_NAME); + }); + } + + @Test + public void testAADB2CAuthorizationRequestResolverBean() { + this.contextRunner.run(c -> { + final AADB2CAuthorizationRequestResolver resolver = c.getBean(AADB2CAuthorizationRequestResolver.class); + + assertThat(resolver).isNotNull(); + }); + } + + @Test + public void testLogoutSuccessHandlerBean() { + this.contextRunner.run(c -> { + final AADB2CLogoutSuccessHandler handler = c.getBean(AADB2CLogoutSuccessHandler.class); + + assertThat(handler).isNotNull(); + }); + } + + @Test + public void testFilterBean() { + this.contextRunner.run(c -> { + final ClientRegistrationRepository repository = c.getBean(ClientRegistrationRepository.class); + + assertThat(repository).isNotNull(); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConstants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConstants.java new file mode 100644 index 000000000000..c68f2449aaa0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConstants.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + + +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CProperties.PREFIX; +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CProperties.USER_FLOW_SIGN_UP_OR_SIGN_IN; + +public class AADB2CConstants { + + public static final String TEST_TENANT = "fake-tenant"; + + public static final String TEST_CLIENT_ID = "fake-client-id"; + + public static final String TEST_CLIENT_SECRET = "fake-client-secret"; + + public static final String TEST_REPLY_URL = "http://localhost:8080/index"; + + public static final String TEST_SIGN_UP_OR_IN_NAME = "fake-sign-in-or-up"; + + public static final String TEST_LOGOUT_SUCCESS_URL = "https://fake-logout-success-url"; + + public static final String TENANT = String.format("%s.%s", PREFIX, "tenant"); + + public static final String CLIENT_ID = String.format("%s.%s", PREFIX, "client-id"); + + public static final String CLIENT_SECRET = String.format("%s.%s", PREFIX, "client-secret"); + + public static final String REPLY_URL = String.format("%s.%s", PREFIX, "reply-url"); + + public static final String LOGOUT_SUCCESS_URL = String.format("%s.%s", PREFIX, "logout-success-url"); + + public static final String SIGN_UP_OR_SIGN_IN = String.format("%s.%s", PREFIX, USER_FLOW_SIGN_UP_OR_SIGN_IN); +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandlerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandlerTest.java new file mode 100644 index 000000000000..0262f0ced51e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandlerTest.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AADB2CLogoutSuccessHandlerTest { + + private static final String TEST_TENANT = "test-tenant"; + + private static final String TEST_LOGOUT_SUCCESS_URL = "http://localhost:8080/login"; + + private static final String TEST_USER_FLOW_SIGN_UP_OR_IN = "my-sign-up-or-in"; + + private AADB2CProperties properties; + + @Before + public void setUp() { + properties = new AADB2CProperties(); + + properties.setTenant(TEST_TENANT); + properties.setLogoutSuccessUrl(TEST_LOGOUT_SUCCESS_URL); + properties.getUserFlows().setSignUpOrSignIn(TEST_USER_FLOW_SIGN_UP_OR_IN); + } + + @Test + public void testDefaultTargetUrl() { + final MyLogoutSuccessHandler handler = new MyLogoutSuccessHandler(properties); + final String tenant = properties.getTenant(); + final String url = properties.getLogoutSuccessUrl(); + final String userFlow = properties.getUserFlows().getSignUpOrSignIn(); + + assertThat(handler.getTargetUrl()).isEqualTo(AADB2CURL.getEndSessionUrl(tenant, url, userFlow)); + } + + private static class MyLogoutSuccessHandler extends AADB2CLogoutSuccessHandler { + + MyLogoutSuccessHandler(AADB2CProperties properties) { + super(properties); + } + + public String getTargetUrl() { + return super.getDefaultTargetUrl(); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURLTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURLTest.java new file mode 100644 index 000000000000..7d55ffa859c3 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURLTest.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.junit.Test; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class AADB2CURLTest { + + /** + * Reference pattern see AUTHORIZATION_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetAuthorizationUrl() { + final String expect = "https://fake-tenant.b2clogin.com/fake-tenant.onmicrosoft.com/oauth2/v2.0/authorize"; + + assertThat(AADB2CURL.getAuthorizationUrl("fake-tenant")).isEqualTo(expect); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetAuthorizationUrlException() { + AADB2CURL.getAuthorizationUrl(""); + } + + /** + * Reference pattern see TOKEN_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetTokenUrl() { + final String expect = "https://fake-tenant.b2clogin.com/fake-tenant.onmicrosoft.com/oauth2/v2.0/token?p=fake-p"; + + assertThat(AADB2CURL.getTokenUrl("fake-tenant", "fake-p")).isEqualTo(expect); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetTokenUrlException() { + AADB2CURL.getTokenUrl("", ""); + } + + /** + * Reference pattern see JWKSET_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetJwkSetUrl() { + final String expect = "https://new-tenant.b2clogin.com/new-tenant.onmicrosoft.com/discovery/v2.0/keys?p=new-p"; + + assertThat(AADB2CURL.getJwkSetUrl("new-tenant", "new-p")).isEqualTo(expect); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetJwkSetUrlException() { + AADB2CURL.getJwkSetUrl("", ""); + } + + /** + * Reference pattern see END_SESSION_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetEndSessionUrl() { + final String expect = "https://my-tenant.b2clogin.com/my-tenant.onmicrosoft.com/oauth2/v2.0/logout?" + + "post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fhome&p=my-p"; + + assertThat(AADB2CURL.getEndSessionUrl("my-tenant", "http://localhost:8080/home", + "my-p")).isEqualTo(expect); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetEndSessionUrlException() { + AADB2CURL.getJwkSetUrl("", ""); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosAutoConfigurationTest.java new file mode 100644 index 000000000000..2e5d1f7ce8a1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosAutoConfigurationTest.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + + +import com.azure.data.cosmos.ConnectionPolicy; +import com.azure.data.cosmos.RetryOptions; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Ignore +public class CosmosAutoConfigurationTest { + @BeforeClass + public static void beforeClass() { + PropertySettingUtil.setProperties(); + } + + @AfterClass + public static void afterClass() { + PropertySettingUtil.unsetProperties(); + } + +// @Test +// public void canSetAllPropertiesToDocumentClient() { +// try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { +// context.register(CosmosAutoConfiguration.class); +// context.refresh(); +// final CosmosClient cosmosClient = context.getBean(CosmosClient.class); +// +// // No way to verify the setting of key value and ConsistencyLevel. +// final URI uri = cosmosClient.getServiceEndpoint(); +// assertThat(uri.toString()).isEqualTo(PropertySettingUtil.URI); +// +// assertThat(cosmosClient.getConnectionPolicy()).isEqualTo(ConnectionPolicy.GetDefault()); +// } +// } +// +// @Test +// public void canSetConnectionPolicyToDocumentClient() { +// try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { +// context.register(CosmosAutoConfiguration.class, ConnectionPolicyConfig.class); +// context.refresh(); +// final CosmosClient cosmosClient = context.getBean(CosmosClient.class); +// +// final ConnectionPolicy connectionPolicy = cosmosClient; +// assertThat(connectionPolicy.requestTimeoutInMillis()).isEqualTo(PropertySettingUtil.REQUEST_TIMEOUT); +// assertThat(connectionPolicy.getMediaRequestTimeout()). +// isEqualTo(PropertySettingUtil.MEDIA_REQUEST_TIMEOUT); +// assertThat(connectionPolicy.getConnectionMode()).isEqualTo(PropertySettingUtil.CONNECTION_MODE); +// assertThat(connectionPolicy.getMediaReadMode()).isEqualTo(PropertySettingUtil.MEDIA_READ_MODE); +// assertThat(connectionPolicy.getMaxPoolSize()).isEqualTo(PropertySettingUtil.MAX_POOL_SIZE); +// assertThat(connectionPolicy.getIdleConnectionTimeout()). +// isEqualTo(PropertySettingUtil.IDLE_CONNECTION_TIMEOUT); +// // TODO (data) User agent from configured ConnectionPolicy is not taken +// // assertThat(connectionPolicy.getUserAgentSuffix()).contains(PropertySettingUtil.USER_AGENT_SUFFIX); +// assertThat(connectionPolicy.getUserAgentSuffix()).contains(PropertySettingUtil.DEFAULT_USER_AGENT_SUFFIX); +// assertThat(connectionPolicy.getRetryOptions().getMaxRetryAttemptsOnThrottledRequests()). +// isEqualTo(PropertySettingUtil.RETRY_OPTIONS_MAX_RETRY_ATTEMPTS_ON_THROTTLED_REQUESTS); +// assertThat(connectionPolicy.getRetryOptions().getMaxRetryWaitTimeInSeconds()). +// isEqualTo(PropertySettingUtil.RETRY_OPTIONS_MAX_RETRY_WAIT_TIME_IN_SECONDS); +// assertThat(connectionPolicy.getEnableEndpointDiscovery()). +// isEqualTo(PropertySettingUtil.ENABLE_ENDPOINT_DISCOVERY); +// assertThat(connectionPolicy.getPreferredLocations().toString()). +// isEqualTo(PropertySettingUtil.PREFERRED_LOCATIONS.toString()); +// } +// } +// +// @Test +// public void canSetAllowTelemetryFalse() { +// PropertySettingUtil.setAllowTelemetryFalse(); +// +// try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { +// context.register(CosmosAutoConfiguration.class, ConnectionPolicyConfig.class); +// context.refresh(); +// final DocumentClient documentClient = context.getBean(DocumentClient.class); +// +// final ConnectionPolicy connectionPolicy = documentClient.getConnectionPolicy(); +// // TODO (data) User agent from configured ConnectionPolicy is not taken +// // assertThat(connectionPolicy.getUserAgentSuffix()).contains(PropertySettingUtil.USER_AGENT_SUFFIX); +// assertThat(connectionPolicy.getUserAgentSuffix()).contains( +// PropertySettingUtil.DEFAULT_USER_AGENT_SUFFIX); +// } +// PropertySettingUtil.unsetAllowTelemetry(); +// } + + @Configuration + static class ConnectionPolicyConfig { + @Bean + public ConnectionPolicy connectionPolicy() { + final ConnectionPolicy connectionPolicy = ConnectionPolicy.defaultPolicy(); + + connectionPolicy.requestTimeoutInMillis(PropertySettingUtil.REQUEST_TIMEOUT); + connectionPolicy.connectionMode(PropertySettingUtil.CONNECTION_MODE); + connectionPolicy.maxPoolSize(PropertySettingUtil.MAX_POOL_SIZE); + connectionPolicy.idleConnectionTimeoutInMillis(PropertySettingUtil.IDLE_CONNECTION_TIMEOUT); + // TODO (data) User agent from configured ConnectionPolicy is not taken + connectionPolicy.userAgentSuffix(PropertySettingUtil.USER_AGENT_SUFFIX); + + final RetryOptions retryOptions = new RetryOptions(); + retryOptions.maxRetryAttemptsOnThrottledRequests( + PropertySettingUtil.RETRY_OPTIONS_MAX_RETRY_ATTEMPTS_ON_THROTTLED_REQUESTS); + retryOptions.maxRetryWaitTimeInSeconds( + PropertySettingUtil.RETRY_OPTIONS_MAX_RETRY_WAIT_TIME_IN_SECONDS); + connectionPolicy.retryOptions(retryOptions); + + connectionPolicy.enableEndpointDiscovery(PropertySettingUtil.ENABLE_ENDPOINT_DISCOVERY); + connectionPolicy.preferredLocations(PropertySettingUtil.PREFERRED_LOCATIONS); + + return connectionPolicy; + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDBPropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDBPropertiesTest.java new file mode 100644 index 000000000000..73f628b1cbc2 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDBPropertiesTest.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + + +import org.junit.Test; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.ObjectError; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CosmosDBPropertiesTest { + @Test + public void canSetAllProperties() { + PropertySettingUtil.setProperties(); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(Config.class); + context.refresh(); + final CosmosDBProperties properties = context.getBean(CosmosDBProperties.class); + + assertThat(properties.getUri()).isEqualTo(PropertySettingUtil.URI); + assertThat(properties.getKey()).isEqualTo(PropertySettingUtil.KEY); + assertThat(properties.getConsistencyLevel()).isEqualTo(PropertySettingUtil.CONSISTENCY_LEVEL); + assertThat(properties.isAllowTelemetry()).isEqualTo(PropertySettingUtil.ALLOW_TELEMETRY_TRUE); + assertThat(properties.isPopulateQueryMetrics()).isEqualTo(PropertySettingUtil.POPULATE_QUERY_METRICS); + } + + PropertySettingUtil.unsetProperties(); + } + + @Test + public void canSetAllowTelemetryFalse() { + PropertySettingUtil.setAllowTelemetryFalse(); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(Config.class); + context.refresh(); + final CosmosDBProperties properties = context.getBean(CosmosDBProperties.class); + + assertThat(properties.isAllowTelemetry()).isEqualTo(PropertySettingUtil.ALLOW_TELEMETRY_FALSE); + } + + PropertySettingUtil.unsetProperties(); + } + + @Test + public void emptySettingNotAllowed() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + Exception exception = null; + + context.register(Config.class); + + try { + context.refresh(); + } catch (Exception e) { + exception = e; + } + + assertThat(exception).isNotNull(); + assertThat(exception).isExactlyInstanceOf(ConfigurationPropertiesBindException.class); + + final BindValidationException bindException = (BindValidationException) exception.getCause().getCause(); + final List errors = bindException.getValidationErrors().getAllErrors(); + final List errorStrings = errors.stream().map(e -> e.toString()).collect(Collectors.toList()); + + Collections.sort(errorStrings); + + final List errorStringsExpected = Arrays.asList( + "Field error in object 'azure.cosmosdb' on field 'database': rejected value [null];", + "Field error in object 'azure.cosmosdb' on field 'key': rejected value [null];", + "Field error in object 'azure.cosmosdb' on field 'uri': rejected value [null];" + ); + + assertThat(errorStrings.size()).isEqualTo(errorStringsExpected.size()); + + for (int i = 0; i < errorStrings.size(); i++) { + assertThat(errorStrings.get(i)).contains(errorStringsExpected.get(i)); + } + } + } + + @Configuration + @EnableConfigurationProperties(CosmosDBProperties.class) + static class Config { + } +} + diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfigurationUnitTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfigurationUnitTest.java new file mode 100644 index 000000000000..1801dc1d300e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/CosmosDbRepositoriesAutoConfigurationUnitTest.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import com.azure.data.cosmos.CosmosClient; +import com.microsoft.azure.spring.autoconfigure.cosmosdb.domain.Person; +import com.microsoft.azure.spring.autoconfigure.cosmosdb.domain.PersonRepository; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.core.CosmosTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingCosmosConverter; +import com.microsoft.azure.spring.data.cosmosdb.repository.config.EnableCosmosRepositories; +import org.junit.After; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +@Ignore +public class CosmosDbRepositoriesAutoConfigurationUnitTest { + + private AnnotationConfigApplicationContext context; + + @InjectMocks + private CosmosTemplate cosmosTemplate; + + @Mock + private CosmosDbFactory cosmosDbFactory; + + @Mock + private MappingCosmosConverter mappingCosmosConverter; + + @Mock + private CosmosClient cosmosClient; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void testDefaultRepositoryConfiguration() throws Exception { + prepareApplicationContext(TestConfiguration.class); + + assertThat(this.context.getBean(PersonRepository.class)).isNotNull(); + } + + @Test(expected = NoSuchBeanDefinitionException.class) + public void autConfigNotKickInIfManualConfigDidNotCreateRepositories() throws Exception { + prepareApplicationContext(InvalidCustomConfiguration.class); + this.context.getBean(PersonRepository.class); + } + + private void prepareApplicationContext(Class... configurationClasses) { + this.context = new AnnotationConfigApplicationContext(); + this.context.register(configurationClasses); + this.context.register(CosmosDbRepositoriesAutoConfiguration.class); + this.context.getBeanFactory().registerSingleton(CosmosTemplate.class.getName(), cosmosTemplate); + this.context.refresh(); + } + + @Configuration + @TestAutoConfigurationPackage(Person.class) + protected static class TestConfiguration { + } + + @Configuration + @EnableCosmosRepositories("foo.bar") + @TestAutoConfigurationPackage(CosmosDbRepositoriesAutoConfigurationUnitTest.class) + protected static class InvalidCustomConfiguration { + + } + +} + diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/PropertySettingUtil.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/PropertySettingUtil.java new file mode 100644 index 000000000000..dda01d1f90fe --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/PropertySettingUtil.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import com.azure.data.cosmos.ConnectionMode; +import com.azure.data.cosmos.ConsistencyLevel; +import com.microsoft.azure.utils.PropertyLoader; + +import java.util.Arrays; +import java.util.List; + +public class PropertySettingUtil { + public static final String URI = "https://test.documents.azure.com:443/"; + public static final String KEY = "KeyString"; + public static final String DATABASE_NAME = "test"; + public static final boolean ALLOW_TELEMETRY_TRUE = true; + public static final boolean ALLOW_TELEMETRY_FALSE = false; + public static final boolean POPULATE_QUERY_METRICS = true; + public static final ConsistencyLevel CONSISTENCY_LEVEL = ConsistencyLevel.STRONG; + public static final int REQUEST_TIMEOUT = 4; + public static final int MEDIA_REQUEST_TIMEOUT = 3; + public static final ConnectionMode CONNECTION_MODE = ConnectionMode.DIRECT; + public static final int MAX_POOL_SIZE = 1; + public static final int IDLE_CONNECTION_TIMEOUT = 2; + public static final String USER_AGENT_SUFFIX = "suffix"; + public static final String DEFAULT_USER_AGENT_SUFFIX = "spring-data/" + PropertyLoader.getProjectVersion(); + public static final int RETRY_OPTIONS_MAX_RETRY_ATTEMPTS_ON_THROTTLED_REQUESTS = 5; + public static final int RETRY_OPTIONS_MAX_RETRY_WAIT_TIME_IN_SECONDS = 6; + public static final boolean ENABLE_ENDPOINT_DISCOVERY = false; + public static final List PREFERRED_LOCATIONS = Arrays.asList("East US", "West US", "North Europe"); + private static final String PROPERTY_URI = "azure.cosmosdb.uri"; + private static final String PROPERTY_KEY = "azure.cosmosdb.key"; + private static final String PROPERTY_DBNAME = "azure.cosmosdb.database"; + private static final String PROPERTY_CONSISTENCY_LEVEL = "azure.cosmosdb.consistency-level"; + private static final String PROPERTY_ALLOW_TELEMETRY = "azure.cosmosdb.allow-telemetry"; + private static final String PROPERTY_POPULATE_QUERY_METRICS = "azure.cosmosdb.populateQueryMetrics"; + + public static void setProperties() { + System.setProperty(PROPERTY_URI, URI); + System.setProperty(PROPERTY_KEY, KEY); + System.setProperty(PROPERTY_DBNAME, DATABASE_NAME); + System.setProperty(PROPERTY_CONSISTENCY_LEVEL, CONSISTENCY_LEVEL.name()); + System.setProperty(PROPERTY_ALLOW_TELEMETRY, Boolean.toString(ALLOW_TELEMETRY_TRUE)); + System.setProperty(PROPERTY_POPULATE_QUERY_METRICS, Boolean.toString(POPULATE_QUERY_METRICS)); + } + + public static void setAllowTelemetryFalse() { + setProperties(); + System.setProperty(PROPERTY_ALLOW_TELEMETRY, Boolean.toString(ALLOW_TELEMETRY_FALSE)); + } + + public static void unsetProperties() { + System.clearProperty(PROPERTY_URI); + System.clearProperty(PROPERTY_KEY); + System.clearProperty(PROPERTY_DBNAME); + System.clearProperty(PROPERTY_CONSISTENCY_LEVEL); + System.clearProperty(PROPERTY_ALLOW_TELEMETRY); + System.clearProperty(PROPERTY_POPULATE_QUERY_METRICS); + } + + public static void unsetAllowTelemetry() { + System.clearProperty(PROPERTY_ALLOW_TELEMETRY); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/TestAutoConfigurationPackage.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/TestAutoConfigurationPackage.java new file mode 100644 index 000000000000..a697fa5d3b4c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/TestAutoConfigurationPackage.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(TestAutoConfigurationPackageRegistrar.class) +public @interface TestAutoConfigurationPackage { + + Class value(); + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/TestAutoConfigurationPackageRegistrar.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/TestAutoConfigurationPackageRegistrar.java new file mode 100644 index 000000000000..bf87e7abbc8a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/TestAutoConfigurationPackageRegistrar.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; + +public class TestAutoConfigurationPackageRegistrar + implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + final AnnotationAttributes attributes = AnnotationAttributes + .fromMap(metadata.getAnnotationAttributes( + TestAutoConfigurationPackage.class.getName(), true)); + AutoConfigurationPackages.register(registry, + ClassUtils.getPackageName(attributes.getString("value"))); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/domain/Person.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/domain/Person.java new file mode 100644 index 000000000000..8032a7c7f8a6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/domain/Person.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb.domain; + + +public class Person { + private String firstName; + private String lastName; + + private String id; + + public Person() { + this(null, null, null); + } + + public Person(String id, String fname, String lname) { + this.firstName = fname; + this.lastName = lname; + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String fname) { + this.firstName = fname; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/domain/PersonRepository.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/domain/PersonRepository.java new file mode 100644 index 000000000000..cc4d0c39647c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/cosmosdb/domain/PersonRepository.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PersonRepository extends CosmosRepository { +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinAutoConfigurationUnitTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinAutoConfigurationUnitTest.java new file mode 100644 index 000000000000..8a94a8d10e33 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinAutoConfigurationUnitTest.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin; + +import com.microsoft.spring.data.gremlin.common.GremlinFactory; +import com.microsoft.spring.data.gremlin.conversion.MappingGremlinConverter; +import com.microsoft.spring.data.gremlin.mapping.GremlinMappingContext; +import com.microsoft.spring.data.gremlin.query.GremlinTemplate; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static com.microsoft.azure.spring.autoconfigure.gremlin.PropertiesUtil.*; + +public class GremlinAutoConfigurationUnitTest { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GremlinAutoConfiguration.class)); + + @Test + public void testAllBeanCreated() { + this.contextRunner + .withPropertyValues(GREMLIN_ENDPOINT_CONFIG) + .withPropertyValues(GREMLIN_PORT_CONFIG) + .withPropertyValues(GREMLIN_USERNAME_CONFIG) + .withPropertyValues(GREMLIN_PASSWORD_CONFIG) + .withPropertyValues(GREMLIN_TELEMETRY_CONFIG_ALLOWED) + .run(context -> { + Assert.assertNotNull(context.getBean(GremlinFactory.class)); + Assert.assertNotNull(context.getBean(GremlinFactory.class).getGremlinClient()); + Assert.assertNotNull(context.getBean(GremlinTemplate.class)); + Assert.assertNotNull(context.getBean(GremlinMappingContext.class)); + Assert.assertNotNull(context.getBean(MappingGremlinConverter.class)); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinPropertiesUnitTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinPropertiesUnitTest.java new file mode 100644 index 000000000000..0159466cdd70 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinPropertiesUnitTest.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static com.microsoft.azure.spring.autoconfigure.gremlin.PropertiesUtil.*; + +public class GremlinPropertiesUnitTest { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GremlinAutoConfiguration.class)); + + @Test + public void testAllProperties() { + this.contextRunner + .withPropertyValues(GREMLIN_ENDPOINT_CONFIG) + .withPropertyValues(GREMLIN_PORT_CONFIG) + .withPropertyValues(GREMLIN_USERNAME_CONFIG) + .withPropertyValues(GREMLIN_PASSWORD_CONFIG) + .withPropertyValues(GREMLIN_TELEMETRY_CONFIG_NOT_ALLOWED) + .run(context -> { + Assert.assertEquals(context.getBean(GremlinProperties.class).getEndpoint(), ENDPOINT); + Assert.assertEquals(context.getBean(GremlinProperties.class).getPort(), PORT); + Assert.assertEquals(context.getBean(GremlinProperties.class).getUsername(), USERNAME); + Assert.assertEquals(context.getBean(GremlinProperties.class).getPassword(), PASSWORD); + Assert.assertFalse(context.getBean(GremlinProperties.class).isTelemetryAllowed()); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfigurationUnitTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfigurationUnitTest.java new file mode 100644 index 000000000000..ae3fe095e3f2 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/GremlinRepositoriesAutoConfigurationUnitTest.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin; + +import com.microsoft.azure.spring.autoconfigure.cosmosdb.TestAutoConfigurationPackage; +import com.microsoft.azure.spring.autoconfigure.gremlin.domain.User; +import com.microsoft.azure.spring.autoconfigure.gremlin.domain.UserRepository; +import com.microsoft.spring.data.gremlin.common.GremlinFactory; +import com.microsoft.spring.data.gremlin.conversion.MappingGremlinConverter; +import com.microsoft.spring.data.gremlin.query.GremlinTemplate; +import com.microsoft.spring.data.gremlin.repository.config.EnableGremlinRepositories; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +@RunWith(MockitoJUnitRunner.class) +public class GremlinRepositoriesAutoConfigurationUnitTest { + + private AnnotationConfigApplicationContext applicationContext; + + @InjectMocks + private GremlinTemplate template; + + @Mock + private GremlinFactory factory; + + @Mock + private MappingGremlinConverter converter; + + @After + public void cleanup() { + if (this.applicationContext != null) { + this.applicationContext.close(); + } + } + + private void initializeApplicationContext(Class... classes) { + this.applicationContext = new AnnotationConfigApplicationContext(); + this.applicationContext.register(classes); + this.applicationContext.register(GremlinRepositoriesAutoConfiguration.class); + this.applicationContext.getBeanFactory().registerSingleton(GremlinTemplate.class.getName(), this.template); + this.applicationContext.refresh(); + } + + @Test + public void testDefaultRepositoryConfiguration() { + initializeApplicationContext(TestConfiguration.class); + + Assert.assertNotNull(this.applicationContext.getBean(UserRepository.class)); + } + + @Test(expected = NoSuchBeanDefinitionException.class) + public void testInvalidRepositoryConfiguration() { + initializeApplicationContext(InvalidConfiguration.class); + + this.applicationContext.getBean(UserRepository.class); + } + + @Configuration + @TestAutoConfigurationPackage(User.class) + protected static class TestConfiguration { + + } + + @Configuration + @EnableGremlinRepositories("fake.repository") + @TestAutoConfigurationPackage(GremlinRepositoriesAutoConfigurationUnitTest.class) + protected static class InvalidConfiguration { + + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/PropertiesUtil.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/PropertiesUtil.java new file mode 100644 index 000000000000..b606679bb867 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/PropertiesUtil.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin; + +public class PropertiesUtil { + private static final String PROPERTY_ENDPOINT = "gremlin.endpoint"; + private static final String PROPERTY_PORT = "gremlin.port"; + private static final String PROPERTY_USERNAME = "gremlin.username"; + private static final String PROPERTY_PASSWORD = "gremlin.password"; + private static final String PROPERTY_TELEMETRY = "gremlin.telemetryAllowed"; + + public static final String ENDPOINT = "localhost"; + public static final int PORT = 8090; + public static final String USERNAME = "fake-username"; + public static final String PASSWORD = "fake-passowrd"; + + public static final String GREMLIN_ENDPOINT_CONFIG = PROPERTY_ENDPOINT + "=" + ENDPOINT; + public static final String GREMLIN_PORT_CONFIG = PROPERTY_PORT + "=" + String.valueOf(PORT); + public static final String GREMLIN_USERNAME_CONFIG = PROPERTY_USERNAME + "=" + USERNAME; + public static final String GREMLIN_PASSWORD_CONFIG = PROPERTY_PASSWORD + "=" + PASSWORD; + public static final String GREMLIN_TELEMETRY_CONFIG_NOT_ALLOWED = PROPERTY_TELEMETRY + "=" + "false"; + public static final String GREMLIN_TELEMETRY_CONFIG_ALLOWED = PROPERTY_TELEMETRY + "=" + "true"; +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/domain/User.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/domain/User.java new file mode 100644 index 000000000000..05a6b7c87b8c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/domain/User.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin.domain; + +import com.microsoft.spring.data.gremlin.annotation.Vertex; +import org.springframework.data.annotation.Id; + +@Vertex +public class User { + + @Id + private String id; + + private String name; + + private boolean enabled; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/domain/UserRepository.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/domain/UserRepository.java new file mode 100644 index 000000000000..272222c169ad --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/gremlin/domain/UserRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.gremlin.domain; + +import com.microsoft.spring.data.gremlin.repository.GremlinRepository; + +import java.util.List; + +public interface UserRepository extends GremlinRepository { + + List findByNameAndEnabled(String name, Boolean enabled); +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/jms/ConnectionStringResolverTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/jms/ConnectionStringResolverTest.java new file mode 100644 index 000000000000..d6eaa34ae05a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/jms/ConnectionStringResolverTest.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.jms; + +import org.junit.Assert; +import org.junit.Test; + +public class ConnectionStringResolverTest { + @Test + public void testConnectionStringResolver() { + final String connectionString = "Endpoint=sb://host/;SharedAccessKeyName=sasKeyName;SharedAccessKey=sasKey"; + + final ServiceBusKey serviceBusKey = ConnectionStringResolver.getServiceBusKey(connectionString); + final String host = serviceBusKey.getHost(); + final String sasKeyName = serviceBusKey.getSharedAccessKeyName(); + final String sasKey = serviceBusKey.getSharedAccessKey(); + + Assert.assertEquals("host", host); + Assert.assertEquals("sasKeyName", sasKeyName); + Assert.assertEquals("sasKey", sasKey); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusJMSAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusJMSAutoConfigurationTest.java new file mode 100644 index 000000000000..f9f2e03acb03 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/jms/ServiceBusJMSAutoConfigurationTest.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.jms; + +import org.apache.qpid.jms.JmsConnectionFactory; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ServiceBusJMSAutoConfigurationTest { + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ServiceBusJMSAutoConfiguration.class)); + + @Test + public void testAzureServiceBusDisabled() { + this.contextRunner.withPropertyValues("spring.jms.servicebus.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean(AzureServiceBusJMSProperties.class)); + } + + @Test + public void testWithoutServiceBusJMSNamespace() { + this.contextRunner.withClassLoader(new FilteredClassLoader(JmsConnectionFactory.class)) + .run(context -> assertThat(context).doesNotHaveBean(AzureServiceBusJMSProperties.class)); + } + + @Test(expected = IllegalStateException.class) + public void testAzureServiceBusJMSPropertiesValidation() { + this.contextRunner.run(context -> context.getBean(AzureServiceBusJMSProperties.class)); + } + + @Test + public void testAzureServiceBusJMSPropertiesConfigured() { + + final String connectionString = "Endpoint=sb://host/;SharedAccessKeyName=sasKeyName;SharedAccessKey=sasKey"; + + this.contextRunner = this.contextRunner.withPropertyValues( + "spring.jms.servicebus.connection-string=" + connectionString + ); + + this.contextRunner = this.contextRunner.withPropertyValues( + "spring.jms.servicebus.topic-client-id=cid" + ); + + this.contextRunner = this.contextRunner.withPropertyValues( + "spring.jms.servicebus.idle-timeout=123" + ); + + this.contextRunner.run( + context -> { + assertThat(context).hasSingleBean(AzureServiceBusJMSProperties.class); + assertThat(context.getBean(AzureServiceBusJMSProperties.class).getConnectionString()).isEqualTo( + connectionString); + assertThat(context.getBean(AzureServiceBusJMSProperties.class).getTopicClientId()).isEqualTo("cid"); + assertThat(context.getBean(AzureServiceBusJMSProperties.class).getIdleTimeout()).isEqualTo(123); + } + ); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/Constants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/Constants.java new file mode 100644 index 000000000000..6d7a34cfb264 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/Constants.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.mediaservices; + +public class Constants { + public static final String TENANT_PROP = "azure.mediaservices.tenant"; + public static final String TENANT = "fake.microsoft.media.service.tenant"; + public static final String CLIENT_ID_PROP = "azure.mediaservices.client-id"; + public static final String CLIENT_ID = "fake-application-id-123456"; + public static final String CLIENT_SECRET_PROP = "azure.mediaservices.client-secret"; + public static final String CLIENT_SECRET = "fake-client-secret"; + public static final String REST_API_ENDPOINT_PROP = "azure.mediaservices.rest-api-endpoint"; + public static final String REST_API_ENDPOINT = "https://fake.media.service.media.azure.net/api/"; + public static final String PROXY_HOST = "localhost"; + public static final String PROXY_HOST_PROP = "azure.mediaservices.proxy-host"; + public static final String PROXY_PORT = "8080"; + public static final String PROXY_PORT_PROP = "azure.mediaservices.proxy-port"; +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesAutoConfigurationTest.java new file mode 100644 index 000000000000..0721e768d2ec --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesAutoConfigurationTest.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.mediaservices; + +import com.microsoft.windowsazure.exception.ServiceException; +import com.microsoft.windowsazure.services.media.MediaContract; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.junit.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static com.microsoft.azure.spring.autoconfigure.mediaservices.Constants.*; +import static com.microsoft.azure.utils.TestUtils.propPair; +import static org.assertj.core.api.Assertions.assertThat; + +public class MediaServicesAutoConfigurationTest { + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MediaServicesAutoConfiguration.class)) + .withPropertyValues(propPair(TENANT_PROP, TENANT), + propPair(CLIENT_ID_PROP, CLIENT_ID), + propPair(CLIENT_SECRET_PROP, CLIENT_SECRET), + propPair(REST_API_ENDPOINT_PROP, REST_API_ENDPOINT)); + + @Test + public void mediaContractBeanCanBeCreated() { + contextRunner.run(context -> assertThat(context).hasSingleBean(MediaContract.class)); + } + + @Test(expected = NoSuchBeanDefinitionException.class) + public void byDefaultMediaContractBeanNotCreated() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MediaServicesAutoConfiguration.class)) + .withPropertyValues().run(context -> context.getBean(MediaContract.class)); + } + + @Test + public void createMediaServiceAccountWithProxyHostMissing() { + contextRunner.withPropertyValues(propPair(PROXY_PORT_PROP, PROXY_PORT)).run(context -> { + try { + context.getBean(MediaContract.class); + } catch (IllegalStateException e) { + assertThat(ExceptionUtils.indexOfThrowable(e, ServiceException.class)).isGreaterThan(-1); + } + }); + } + + @Test + public void createMediaServiceAccountWithProxyPortMissing() { + contextRunner.withPropertyValues(propPair(PROXY_HOST_PROP, PROXY_HOST)).run(context -> { + try { + context.getBean(MediaContract.class); + } catch (IllegalStateException e) { + assertThat(ExceptionUtils.indexOfThrowable(e, ServiceException.class)).isGreaterThan(-1); + } + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesPropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesPropertiesTest.java new file mode 100644 index 000000000000..d3b37e21fb8f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/mediaservices/MediaServicesPropertiesTest.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.spring.autoconfigure.mediaservices; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static com.microsoft.azure.spring.autoconfigure.mediaservices.Constants.*; +import static com.microsoft.azure.utils.TestUtils.propPair; +import static org.assertj.core.api.Assertions.assertThat; + +public class MediaServicesPropertiesTest { + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MediaServicesAutoConfiguration.class)) + .withPropertyValues(propPair(TENANT_PROP, TENANT), + propPair(CLIENT_ID_PROP, CLIENT_ID), + propPair(CLIENT_SECRET_PROP, CLIENT_SECRET), + propPair(REST_API_ENDPOINT_PROP, REST_API_ENDPOINT)); + + @Test + public void canSetProperties() { + contextRunner.run(context -> { + final MediaServicesProperties properties = context.getBean(MediaServicesProperties.class); + assertThat(properties).isNotNull(); + assertThat(properties.getTenant()).isEqualTo(TENANT); + assertThat(properties.getClientId()).isEqualTo(CLIENT_ID); + assertThat(properties.getClientSecret()).isEqualTo(CLIENT_SECRET); + assertThat(properties.getRestApiEndpoint()).isEqualTo(REST_API_ENDPOINT); + }); + } + + + @Test + public void emptyTenantNotAllowed() { + contextRunner.withPropertyValues(propPair(TENANT_PROP, ""), propPair(CLIENT_ID_PROP, CLIENT_ID), + propPair(CLIENT_SECRET_PROP, CLIENT_SECRET), propPair(REST_API_ENDPOINT_PROP, REST_API_ENDPOINT)) + .run(context -> { + try { + context.getBean(MediaServicesProperties.class); + } catch (IllegalStateException e) { + assertThat(e.getCause().getCause()).isInstanceOf(ConfigurationPropertiesBindException.class); + } + }); + } + + @Test + public void emptyClientIdtNotAllowed() { + contextRunner.withPropertyValues(propPair(TENANT_PROP, TENANT), propPair(CLIENT_ID_PROP, ""), + propPair(CLIENT_SECRET_PROP, CLIENT_SECRET), propPair(REST_API_ENDPOINT_PROP, REST_API_ENDPOINT)) + .run(context -> { + try { + context.getBean(MediaServicesProperties.class); + } catch (IllegalStateException e) { + assertThat(e.getCause().getCause()).isInstanceOf(ConfigurationPropertiesBindException.class); + } + }); + } + + @Test + public void emptyClientSecretNotAllowed() { + contextRunner.withPropertyValues(propPair(TENANT_PROP, ""), propPair(CLIENT_ID_PROP, CLIENT_ID), + propPair(CLIENT_SECRET_PROP, ""), propPair(REST_API_ENDPOINT_PROP, REST_API_ENDPOINT)) + .run(context -> { + try { + context.getBean(MediaServicesProperties.class); + } catch (IllegalStateException e) { + assertThat(e.getCause().getCause()).isInstanceOf(ConfigurationPropertiesBindException.class); + } + }); + } + + @Test + public void emptyRestApiEndpointNotAllowed() { + contextRunner.withPropertyValues(propPair(TENANT_PROP, ""), propPair(CLIENT_ID_PROP, CLIENT_ID), + propPair(CLIENT_SECRET_PROP, CLIENT_SECRET), propPair(REST_API_ENDPOINT_PROP, "")) + .run(context -> { + try { + context.getBean(MediaServicesProperties.class); + } catch (IllegalStateException e) { + assertThat(e.getCause().getCause()).isInstanceOf(ConfigurationPropertiesBindException.class); + } + }); + } +} + diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorMetricsExportAutoConfigurationTests.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorMetricsExportAutoConfigurationTests.java new file mode 100644 index 000000000000..73cf9ae05eb0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorMetricsExportAutoConfigurationTests.java @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.metrics; + +import io.micrometer.azuremonitor.AzureMonitorConfig; +import io.micrometer.azuremonitor.AzureMonitorMeterRegistry; +import io.micrometer.core.instrument.Clock; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Tests for autoconfiguration of {@link AzureMonitorMetricsExportAutoConfiguration} + * + * @author Dhaval Doshi + */ +public class AzureMonitorMetricsExportAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations + .of(AzureMonitorMetricsExportAutoConfiguration.class)); + + @Test + public void backsOffWithoutAClock() { + this.contextRunner.run((context) -> assertThat(context) + .doesNotHaveBean(AzureMonitorMeterRegistry.class)); + } + + @Test + @Ignore("Somewhere in the class path there is xml from where the config is picked for AI") + public void failsWithoutAnApiKey() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).hasFailed()); + } + + @Test + public void autoConfiguresConfigAndMeterRegistry() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues( + "management.metrics.export.azuremonitor.instrumentation-key=fakekey") + .run((context) -> assertThat(context) + .hasSingleBean(AzureMonitorMeterRegistry.class) + .hasSingleBean(AzureMonitorConfig.class)); + } + + @Test + public void autoConfigurationCanBeDisabled() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues( + "management.metrics.export.azuremonitor.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(AzureMonitorMeterRegistry.class) + .doesNotHaveBean(AzureMonitorConfig.class)); + } + + @Test + public void allowsCustomConfigToBeUsed() { + this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) + .run((context) -> assertThat(context) + .hasSingleBean(AzureMonitorMeterRegistry.class) + .hasSingleBean(AzureMonitorConfig.class).hasBean("customConfig")); + } + + @Test + public void allowsCustomRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class) + .withPropertyValues( + "management.metrics.export.azuremonitor.instrumentation-key=fakekey") + .run((context) -> assertThat(context) + .hasSingleBean(AzureMonitorMeterRegistry.class) + .hasBean("customRegistry") + .hasSingleBean(AzureMonitorConfig.class)); + } + + @Test + public void stopsMeterRegistryWhenContextIsClosed() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues( + "management.metrics.export.azuremonitor.instrumentation-key=fakekey") + .run((context) -> { + final AzureMonitorMeterRegistry registry = spyOnDisposableBean( + AzureMonitorMeterRegistry.class, context); + context.close(); + verify(registry).stop(); + }); + } + + @SuppressWarnings("unchecked") + private T spyOnDisposableBean(Class type, + AssertableApplicationContext context) { + final String[] names = context.getBeanNamesForType(type); + assertThat(names).hasSize(1); + final String registryBeanName = names[0]; + final Map disposableBeans = (Map) ReflectionTestUtils + .getField(context.getAutowireCapableBeanFactory(), "disposableBeans"); + final Object registryAdapter = disposableBeans.get(registryBeanName); + final T registry = (T) spy(ReflectionTestUtils.getField(registryAdapter, "bean")); + ReflectionTestUtils.setField(registryAdapter, "bean", registry); + return registry; + } + + @Configuration + static class BaseConfiguration { + + @Bean + public Clock clock() { + return Clock.SYSTEM; + } + + } + + @Configuration + @Import(BaseConfiguration.class) + static class CustomConfigConfiguration { + + @Bean + public AzureMonitorConfig customConfig() { + return new AzureMonitorConfig() { + + @Override + public String get(String k) { + if ("azuremonitor.instrumentation-key".equals(k)) { + return "12345"; + } + return null; + } + + }; + } + + } + + @Configuration + @Import(BaseConfiguration.class) + static class CustomRegistryConfiguration { + + @Bean + public AzureMonitorMeterRegistry customRegistry(AzureMonitorConfig config, + Clock clock) { + return AzureMonitorMeterRegistry.builder(config) + .clock(clock) + .build(); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorPropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorPropertiesTest.java new file mode 100644 index 000000000000..eb3585a4a479 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/metrics/AzureMonitorPropertiesTest.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.metrics; + +import io.micrometer.azuremonitor.AzureMonitorConfig; +import io.micrometer.core.instrument.step.StepRegistryConfig; +import org.junit.Test; +import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AzureMonitorProperties}. + * + * @author Dhaval Doshi + */ + +public class AzureMonitorPropertiesTest { + + @SuppressWarnings("depreciation") + private void assertStepRegistryDefaultValues(StepRegistryProperties properties, + StepRegistryConfig config) { + + assertThat(properties.getStep()).isEqualTo(config.step()); + assertThat(properties.isEnabled()).isEqualTo(config.enabled()); + assertThat(properties.getConnectTimeout()).isEqualTo(config.connectTimeout()); + assertThat(properties.getReadTimeout()).isEqualTo(config.readTimeout()); + assertThat(properties.getNumThreads()).isEqualTo(config.numThreads()); + assertThat(properties.getBatchSize()).isEqualTo(config.batchSize()); + } + + @Test + public void defaultValuesAreConsistent() { + final AzureMonitorProperties properties = new AzureMonitorProperties(); + final AzureMonitorConfig config = (key) -> null; + assertStepRegistryDefaultValues(properties, config); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/Constants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/Constants.java new file mode 100644 index 000000000000..b5f399657eb5 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/Constants.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.servicebus; + +import com.microsoft.azure.servicebus.ReceiveMode; + +public class Constants { + public static final String CONNECTION_STRING_PROPERTY = "azure.servicebus.connection-string"; + public static final String QUEUE_NAME_PROPERTY = "azure.servicebus.queue-name"; + public static final String QUEUE_RECEIVE_MODE_PROPERTY = "azure.servicebus.queue-receive-mode"; + public static final String TOPIC_NAME_PROPERTY = "azure.servicebus.topic-name"; + public static final String SUBSCRIPTION_NAME_PROPERTY = "azure.servicebus.subscription-name"; + public static final String SUBSCRIPTION_RECEIVE_MODE_PROPERTY = "azure.servicebus.subscription-receive-mode"; + + public static final String INVALID_CONNECTION_STRING = "connection string"; + public static final String CONNECTION_STRING = "Endpoint=sb://test.servicebus.windows.net/;" + + "SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=dummy-key"; + public static final String QUEUE_NAME = "queue name"; + public static final ReceiveMode QUEUE_RECEIVE_MODE = ReceiveMode.PEEKLOCK; + public static final String TOPIC_NAME = "topic name"; + public static final String SUBSCRIPTION_NAME = "subscription name"; + public static final ReceiveMode SUBSCRIPTION_RECEIVE_MODE = ReceiveMode.PEEKLOCK; +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusAutoConfigurationTest.java new file mode 100644 index 000000000000..91edf1c85043 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusAutoConfigurationTest.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.servicebus; + +import com.microsoft.azure.servicebus.QueueClient; +import com.microsoft.azure.servicebus.SubscriptionClient; +import com.microsoft.azure.servicebus.TopicClient; +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ServiceBusAutoConfigurationTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ServiceBusAutoConfiguration.class)); + + @Test + public void returnNullIfSetConnectionStringOnly() { + this.contextRunner.withPropertyValues(Constants.CONNECTION_STRING_PROPERTY, + Constants.INVALID_CONNECTION_STRING); + + this.contextRunner.run((context) -> { + assertThat(context).doesNotHaveBean(QueueClient.class); + assertThat(context).doesNotHaveBean(TopicClient.class); + assertThat(context).doesNotHaveBean(SubscriptionClient.class); + }); + } + + @Test + public void contextInitialisesWithInvalidConfigurationWhenNoBeansReferenced() { + this.contextRunner.withPropertyValues(Constants.CONNECTION_STRING_PROPERTY, Constants.INVALID_CONNECTION_STRING) + .withPropertyValues(Constants.QUEUE_NAME_PROPERTY, Constants.QUEUE_NAME) + .withPropertyValues(Constants.QUEUE_RECEIVE_MODE_PROPERTY, Constants.QUEUE_RECEIVE_MODE.name()) + .withPropertyValues(Constants.TOPIC_NAME_PROPERTY, Constants.TOPIC_NAME) + .withPropertyValues(Constants.SUBSCRIPTION_NAME_PROPERTY, Constants.SUBSCRIPTION_NAME) + .withPropertyValues(Constants.SUBSCRIPTION_RECEIVE_MODE_PROPERTY, + Constants.SUBSCRIPTION_RECEIVE_MODE.name()); + + this.contextRunner.run(context -> assertThat(context).isNotNull()); + } + + @Test + public void cannotAutowireQueueClientWithInvalidConnectionString() { + this.contextRunner.withPropertyValues(Constants.CONNECTION_STRING_PROPERTY, Constants.INVALID_CONNECTION_STRING) + .withPropertyValues(Constants.QUEUE_NAME_PROPERTY, Constants.QUEUE_NAME) + .withPropertyValues(Constants.QUEUE_RECEIVE_MODE_PROPERTY, Constants.QUEUE_RECEIVE_MODE.name()); + + this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(QueueClient.class)); + } + + @Test + public void cannotAutowireTopicClientWithInvalidConnectionString() { + this.contextRunner.withPropertyValues(Constants.CONNECTION_STRING_PROPERTY, Constants.INVALID_CONNECTION_STRING) + .withPropertyValues(Constants.TOPIC_NAME_PROPERTY, Constants.TOPIC_NAME); + + this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(TopicClient.class)); + } + + @Test + public void cannotAutowireSubscriptionClientWithInvalidConnectionString() { + this.contextRunner.withPropertyValues(Constants.CONNECTION_STRING_PROPERTY, Constants.INVALID_CONNECTION_STRING) + .withPropertyValues(Constants.TOPIC_NAME_PROPERTY, Constants.TOPIC_NAME) + .withPropertyValues(Constants.SUBSCRIPTION_NAME_PROPERTY, Constants.SUBSCRIPTION_NAME) + .withPropertyValues(Constants.SUBSCRIPTION_RECEIVE_MODE_PROPERTY, + Constants.SUBSCRIPTION_RECEIVE_MODE.name()); + + this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(SubscriptionClient.class)); + } + + @Test + public void cannotAutowireSubscriptionClientWithInvalidCredential() { + this.contextRunner.withPropertyValues(Constants.CONNECTION_STRING_PROPERTY, Constants.CONNECTION_STRING) + .withPropertyValues(Constants.TOPIC_NAME_PROPERTY, Constants.TOPIC_NAME) + .withPropertyValues(Constants.SUBSCRIPTION_NAME_PROPERTY, Constants.SUBSCRIPTION_NAME) + .withPropertyValues(Constants.SUBSCRIPTION_RECEIVE_MODE_PROPERTY, + Constants.SUBSCRIPTION_RECEIVE_MODE.name()); + + this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(SubscriptionClient.class)); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusPropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusPropertiesTest.java new file mode 100644 index 000000000000..38d7f55fdcde --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/servicebus/ServiceBusPropertiesTest.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.servicebus; + +import org.junit.Test; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.ObjectError; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ServiceBusPropertiesTest { + @Test + public void canSetQueueProperties() { + System.setProperty(Constants.CONNECTION_STRING_PROPERTY, Constants.INVALID_CONNECTION_STRING); + System.setProperty(Constants.QUEUE_NAME_PROPERTY, Constants.QUEUE_NAME); + System.setProperty(Constants.QUEUE_RECEIVE_MODE_PROPERTY, Constants.QUEUE_RECEIVE_MODE.name()); + System.setProperty(Constants.TOPIC_NAME_PROPERTY, Constants.TOPIC_NAME); + System.setProperty(Constants.SUBSCRIPTION_NAME_PROPERTY, Constants.SUBSCRIPTION_NAME); + System.setProperty(Constants.SUBSCRIPTION_RECEIVE_MODE_PROPERTY, Constants.SUBSCRIPTION_RECEIVE_MODE.name()); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(Config.class); + context.refresh(); + final ServiceBusProperties properties = context.getBean(ServiceBusProperties.class); + + assertThat(properties.getConnectionString()).isEqualTo(Constants.INVALID_CONNECTION_STRING); + assertThat(properties.getQueueName()).isEqualTo(Constants.QUEUE_NAME); + assertThat(properties.getQueueReceiveMode()).isEqualTo(Constants.QUEUE_RECEIVE_MODE); + assertThat(properties.getTopicName()).isEqualTo(Constants.TOPIC_NAME); + assertThat(properties.getSubscriptionName()).isEqualTo(Constants.SUBSCRIPTION_NAME); + assertThat(properties.getSubscriptionReceiveMode()).isEqualTo(Constants.SUBSCRIPTION_RECEIVE_MODE); + } + + System.clearProperty(Constants.CONNECTION_STRING_PROPERTY); + System.clearProperty(Constants.QUEUE_NAME_PROPERTY); + System.clearProperty(Constants.QUEUE_RECEIVE_MODE_PROPERTY); + System.clearProperty(Constants.TOPIC_NAME_PROPERTY); + System.clearProperty(Constants.SUBSCRIPTION_NAME_PROPERTY); + System.clearProperty(Constants.SUBSCRIPTION_RECEIVE_MODE_PROPERTY); + } + + @Test + public void connectionStringIsNull() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + Exception exception = null; + + context.register(Config.class); + + try { + context.refresh(); + } catch (Exception e) { + exception = e; + } + + assertThat(exception).isNotNull(); + assertThat(exception).isExactlyInstanceOf(ConfigurationPropertiesBindException.class); + + final BindValidationException bindException = (BindValidationException) exception.getCause().getCause(); + final List errors = bindException.getValidationErrors().getAllErrors(); + final List errorStrings = errors.stream().map(e -> e.toString()).collect(Collectors.toList()); + + Collections.sort(errorStrings); + + final List errorStringsExpected = Arrays.asList( + "Field error in object 'azure.servicebus' on field 'connectionString': rejected value [null];" + ); + + assertThat(errorStrings.size()).isEqualTo(errorStringsExpected.size()); + + for (int i = 0; i < errorStrings.size(); i++) { + assertThat(errorStrings.get(i)).contains(errorStringsExpected.get(i)); + } + } + } + + @Configuration + @EnableConfigurationProperties(ServiceBusProperties.class) + static class Config { + } +} + diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/storage/StorageAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/storage/StorageAutoConfigurationTest.java new file mode 100644 index 000000000000..38918900c732 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/storage/StorageAutoConfigurationTest.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.storage; + +import com.microsoft.azure.storage.blob.ContainerURL; +import com.microsoft.azure.storage.blob.ServiceURL; +import org.junit.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StorageAutoConfigurationTest { + private static final String BLOB_HTTP_URL = "http://%s.blob.core.windows.net"; + private static final String BLOB_HTTPS_URL = "https://%s.blob.core.windows.net"; + private static final String ACCOUNT_KEY = "ZmFrZUFjY291bnRLZXk="; /* Base64 encoded for string fakeAccountKey */ + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(StorageAutoConfiguration.class)); + + @Test(expected = NoSuchBeanDefinitionException.class) + public void serviceUrlBeanNotCreatedByDefault() { + contextRunner.run(context -> context.getBean(ServiceURL.class)); + } + + @Test + public void serviceUrlBeanCreatedCorrectly() { + contextRunner.withPropertyValues("azure.storage.account-name=fakeStorageAccountName", + "azure.storage.account-key=" + ACCOUNT_KEY) + .run(context -> { + final ServiceURL serviceURL = context.getBean(ServiceURL.class); + final String blobUrl = String.format(BLOB_HTTP_URL, "fakeStorageAccountName"); + assertThat(serviceURL).isNotNull(); + assertThat(serviceURL.toURL().toString()).isEqualTo(blobUrl); + }); + + contextRunner.withPropertyValues("azure.storage.account-name=fakeStorageAccountName", + "azure.storage.account-key=" + ACCOUNT_KEY, "azure.storage.enable-https=true") + .run(context -> { + final ServiceURL serviceURL = context.getBean(ServiceURL.class); + final String blobUrl = String.format(BLOB_HTTPS_URL, "fakeStorageAccountName"); + assertThat(serviceURL).isNotNull(); + assertThat(serviceURL.toURL().toString()).isEqualTo(blobUrl); + }); + } + + @Test(expected = NoSuchBeanDefinitionException.class) + public void containerUrlNotCreatedIfNotConfigured() { + contextRunner.withPropertyValues("azure.storage.account-name=fakeStorageAccountName", + "azure.storage.account-key=" + ACCOUNT_KEY) + .run(context -> context.getBean(ContainerURL.class)); + } + + @Test + public void containerUrlCreatedIfConfigured() { + contextRunner.withPropertyValues("azure.storage.account-name=fakeStorageAccountName", + "azure.storage.account-key=" + ACCOUNT_KEY, + "azure.storage.container-name=fakestoragecontainername") + .run(context -> { + final ContainerURL containerURL = context.getBean(ContainerURL.class); + assertThat(containerURL).isNotNull(); + assertThat(containerURL.toURL().toString()).contains("fakestoragecontainername"); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/storage/StoragePropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/storage/StoragePropertiesTest.java new file mode 100644 index 000000000000..26230b1b2390 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/storage/StoragePropertiesTest.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.storage; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.ConfigurationPropertiesBindException; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StoragePropertiesTest { + private static final String ACCOUNT_NAME_PROP = "azure.storage.account-name"; + private static final String ACCOUNT_KEY_PROP = "azure.storage.account-key"; + private static final String CONTAINER_NAME_PROP = "azure.storage.container-name"; + private static final String USE_EMULATOR_PROP = "azure.storage.use-emulator"; + private static final String EMULATOR_BLOB_HOST_PROP = "azure.storage.emulator-blob-host"; + + private static final String ACCOUNT_NAME = "fakeStorageAccountName"; + private static final String ACCOUNT_KEY = "ZmFrZUFjY291bnRLZXk="; /* Base64 encoded for string fakeAccountKey */ + private static final String CONTAINER_NAME = "fakestoragecontainername"; + private static final boolean USE_EMULATOR = true; + private static final String EMULATOR_BLOB_HOST = "http://127.0.0.1:1000"; + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(StorageAutoConfiguration.class)); + + @Test + public void canSetProperties() { + contextRunner.withPropertyValues(propValuePair(ACCOUNT_NAME_PROP, ACCOUNT_NAME), + propValuePair(ACCOUNT_KEY_PROP, ACCOUNT_KEY), propValuePair(CONTAINER_NAME_PROP, CONTAINER_NAME)) + .run(context -> { + final StorageProperties properties = context.getBean(StorageProperties.class); + assertThat(properties.getAccountName()).isEqualTo(ACCOUNT_NAME); + assertThat(properties.getAccountKey()).isEqualTo(ACCOUNT_KEY); + assertThat(properties.getContainerName()).isEqualTo(CONTAINER_NAME); + }); + } + + @Test + public void canSetEmulatorProperties() { + contextRunner.withPropertyValues(propValuePair(ACCOUNT_NAME_PROP, ACCOUNT_NAME), + propValuePair(ACCOUNT_KEY_PROP, ACCOUNT_KEY), propValuePair(CONTAINER_NAME_PROP, CONTAINER_NAME), + propValuePair(USE_EMULATOR_PROP, Boolean.toString(USE_EMULATOR)), + propValuePair(EMULATOR_BLOB_HOST_PROP, EMULATOR_BLOB_HOST)) + .run(context -> { + final StorageProperties properties = context.getBean(StorageProperties.class); + assertThat(properties.isUseEmulator()).isEqualTo(USE_EMULATOR); + assertThat(properties.getEmulatorBlobHost()).isEqualTo(EMULATOR_BLOB_HOST); + }); + } + + @Test + public void emptySettingNotAllowed() { + try { + contextRunner.withPropertyValues(propValuePair(ACCOUNT_NAME_PROP, ""), + propValuePair(ACCOUNT_KEY_PROP, "")) + .run(context -> context.getBean(StorageProperties.class)); + } catch (IllegalStateException e) { + assertThat(e.getCause().getCause()).isInstanceOf(ConfigurationPropertiesBindException.class); + } + } + + private static String propValuePair(String propName, String propValue) { + return propName + "=" + propValue; + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/ApplicationIdTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/ApplicationIdTest.java new file mode 100644 index 000000000000..649347d5614f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/ApplicationIdTest.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.azure.utils; + +import org.junit.Assert; +import org.junit.Test; + +import static com.microsoft.azure.keyvault.spring.Constants.SPRINGBOOT_KEY_VAULT_APPLICATION_ID; + +public class ApplicationIdTest { + + @Test + public void maxLength() { + Assert.assertTrue(SPRINGBOOT_KEY_VAULT_APPLICATION_ID.length() <= 24); + } + +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java new file mode 100644 index 000000000000..aea87a056c05 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.utils; + +public final class TestUtils { + + private TestUtils() { + } + + /** + * + * @param propName property name + * @param propValue value of property + * @return property name and value pair. e.g., prop.name=prop.value + */ + public static String propPair(String propName, String propValue) { + return propName + "=" + propValue; + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties b/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties new file mode 100644 index 000000000000..0931172601a1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties @@ -0,0 +1,5 @@ +spring.security.oauth2.client.registration.azure.client-id=abcd +spring.security.oauth2.client.registration.azure.client-secret=password + +azure.activedirectory.tenant-id=my-tanant-id +azure.activedirectory.active-directory-groups=my-aad-group1, my-aad-group2 diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/application.properties b/sdk/spring/azure-spring-boot/src/test/resources/application.properties new file mode 100644 index 000000000000..fe52c1ff068c --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/application.properties @@ -0,0 +1,4 @@ +azure.client-id=testid +azure.client-key=testkey +azure.keyvault.uri=fakeuri +azure.keyvault.enabled=false diff --git a/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx b/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx new file mode 100644 index 000000000000..ea44792776c8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx @@ -0,0 +1 @@ +for test file extension only \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt new file mode 100644 index 000000000000..28afc8898855 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3dyb25naXNzdWVyLm5ldHQvdGVzdCIsInN1YiI6InRlc3RAZXhhbXBsZS5jb20iLCJuYmYiOjE1NDUwMDg5MDYsImV4cCI6OTk5OTk5OTk5OTksImlhdCI6MTU0NTAwODkwNiwianRpIjoidGVzdGlkIiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.lK-pSsNMHHui8IxSCYs7e4gH9I29ug7CyNnT8GIJBWEZCeLs3IRnlkLG923Zn7eT_4A0aWFyEnjCBYIKtqX7AoaLBwCa08yB9x7c0DbJQvdKwFjGzc5zkpNxnzBZxXLJr7D9nMAjeQrYLkgoy4XXHL_m_Z6PTf9Jwl6tTYqUS06gd5ZokV1DtBTTPeDJj7KKzNhY3PQ1Hh_-RLoCspqIiZFZ8dfPgDCc2OXVCsH8_2tUFCktuPuVYD11Ws7_hFG6sq8AF1jyugrtYnwMhbzpMCtkL-SoZsmBtmAUFW20vTNYV6Vri-VEqz5VkHef9ZqZmlNPR0vH8hcZVj0IX8t7yA \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt new file mode 100644 index 000000000000..ac1382b4f5eb --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmJmIjoxNTQ1MDA5MjQ5LCJleHAiOjk5OTk5OTk5OTk5LCJpYXQiOjE1NDUwMDkyNDksImp0aSI6InRlc3RpZCIsInR5cCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcmVnaXN0ZXIifQ.2rHU8-_UWjE0U2Vq9KDmtt1ztlj_G9OW777O-kZ_di7dOBZPt6H2eMba34Qf5wILfs1bHBubMNIs64B9mLffJzXp_FKyMdcCsYecJAOaSscrSLjHYdnZqhRIETOloz-nbxiH_AhaJP6Hb482Hu7It4XhcxWU_tZ9kRD1brfoyb_-8Qh4vmrR4eddtfLZDlr3xFfTSD9FKDeECDWu59wGLBVS_32Y42XYV82f5PD1FsAG62vC-t2XdVS-y6aQIT1QElsKcc66xY21XgXq4fkFGxyoYPB1hCLIPz_QMJxRXql7AnVoxkueQxMzH4NCT64i1Aj7texhHbZh4-_jG29-zg \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt new file mode 100644 index 000000000000..0ca89f3afca7 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC90ZXN0Iiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsIm5iZiI6MTU0NTAwODE5NiwiZXhwIjo5OTk5OTk5OTk5OSwiaWF0IjoxNTQ1MDA4MTk2LCJqdGkiOiJ0ZXN0aWQiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0.ZQceiSqNKiEHrNaPhKCKW2EVEnhGbyh4TjbhqB-P7E70NRS3Ad89ISBaSyhpwRS6lwdpMrwNEETFloGm8H6nv623gcWzTCnb7bqaOWKCNTV9TjvhecjIe69AkNHfvkqyopbyRktKosWm89e2nAgiGtp-Y1Pyrt1_iiwOtvahtGyaWqs82-WkFY61DFI1e4iRBI6WSIGLUUpc4vXCGdQ33OyN6wAQ2IYeHCURmB-stVT-GcoMcDZKJBqnerQsu5WDbSwkZfcVTWDK-l_sz1WSdFGTdSWATZJ_LKvxa8IPX--s0-JRmZf-0dwadjcbCNLwYtYDvtaZyczouZKGGBoWZA \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt new file mode 100644 index 000000000000..0ca89f3afca7 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC90ZXN0Iiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsIm5iZiI6MTU0NTAwODE5NiwiZXhwIjo5OTk5OTk5OTk5OSwiaWF0IjoxNTQ1MDA4MTk2LCJqdGkiOiJ0ZXN0aWQiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0.ZQceiSqNKiEHrNaPhKCKW2EVEnhGbyh4TjbhqB-P7E70NRS3Ad89ISBaSyhpwRS6lwdpMrwNEETFloGm8H6nv623gcWzTCnb7bqaOWKCNTV9TjvhecjIe69AkNHfvkqyopbyRktKosWm89e2nAgiGtp-Y1Pyrt1_iiwOtvahtGyaWqs82-WkFY61DFI1e4iRBI6WSIGLUUpc4vXCGdQ33OyN6wAQ2IYeHCURmB-stVT-GcoMcDZKJBqnerQsu5WDbSwkZfcVTWDK-l_sz1WSdFGTdSWATZJ_LKvxa8IPX--s0-JRmZf-0dwadjcbCNLwYtYDvtaZyczouZKGGBoWZA \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml b/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..47e7acf72ba1 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000000..1f0955d450f0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx b/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx new file mode 100644 index 000000000000..fd42b8ccd78e Binary files /dev/null and b/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx differ diff --git a/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/test-private-key.txt b/sdk/spring/azure-spring-boot/src/test/resources/test-private-key.txt new file mode 100644 index 000000000000..582119f8d0f0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/test-private-key.txt @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,2E65118E6C7B5207 + +7cYUTW4ZBdmVZ4ILB08hcTdm5ib0E0zcy+I7pHpNQfJHtI7BJ4omys5S19ufJPBJ +IzYjeO7oTVqI37F6EUmjZqG4WVE2UQbQDkosZbZN82O4Ipu1lFAPEbwjqePMKufz +snSQHKfnbyyDPEVNlJbs19NXC8v6g+pQay5rH/I6N2iBxgsTmuemZ54EhNQMZyEN +R/CiheArWEH9H8/4hd2gc9Tb2s0MwGHILL4kbbNm5tp3xw4ik7OYWNrj3m+nG6Xb +vKXh2xEanAZAyMXTqDJTHdn7/CEqusQPJjZGV+Mf1kjKu7p4qcXFnIXP5ILnTW7b +lHoWC4eweDzKOMRzXmbABEVSUvx2SmPl4TcoC5L1SCAHEmZaKbaY7S5l53u6gl0f +ULuQbt7Hr3THznlNFKkGT1/yVNt2QOm1emZd55LaNe8E7XsNSlhl0grYQ+Ue8Jba +x85OapltVjxM9wVCwbgFyi04ihdKHo9e+uYKeTGKv0hU5O7HEH1ev6t/s2u/UG6h +TqEsYrVp0CMHpt5uAF6nZyK6GZ/CHTxh/rz1hADMofem59+e6tVtjnPGA3EjnJT8 +BMOw/D2QIDxjxj2GUzz+YJp50ENhWrL9oSDkG2nzv4NVL77QIy+T/2/f4PgokUDO +QJjIfxPWE40cHGHpnQtZvEPoxP0H3T0YhmEVwuJxX3uaWOY/8Fa1c7Ln0SwWdfV5 +gYvJV8o6c3sumcq1O3agPDlHC5O4IxG7AZQ8CHRDyASogzfkY6P579ZOGYaO4al7 +WA1YIpsHs3/1f4SByMuWe0NVkFfvXckjpqGrBQpTmqQzk6baa0VQ0cwU3XlkwHac +WB/fQ4jylwFzZDcp5JAo53n6aU72zgNvDlGTNKwdXXZI5U3JPocH0AiZgFFWYJLd +63PJLDnjyE3i6XMVlxifXKkXVv0RYSz+ByS7Oz9aCgnQhNU8ycv+UxtfkPQih5zE +/0Y2EEFknajmFJpNXczzF8OEzaswmR0AOjcCiklZKRf61rf5faJxJhhqKEEBJuL6 +oodDVRk3OGU1yQSBazT8nK3V+e6FMo3tWkra2BXFCD+pKxTy014Cp59S1w6F1Fjt +WX7eMWSLWfQ56j2kLMBHq5gb2arqlqH3fsYOTD3TNjCYF3Sgx309kVPuOK5vw61P +pnL/LN3iGY42WR+9lfAyNN2qj9zvwKwscyYs5+DPQoPmcPcVGc3v/u66bLcOGbEU +OlGa/6gdD4GCp5E4fP/7GbnEY/PW2abquFhGB+pVdl3/4+1U/8kItlfWNZoG4FhE +gjMd7glmrdFiNJFFpf5ks1lVXGqJ4mZxqtEZrxUEwciZjm4V27a+E2KyV9NnksZ6 +xF4tGPKIPsvNTV5o8ZqjiacxgbYmr2ywqDXKCgpU/RWSh1sLapqSQqbH/w0MquUj +VhVX0RMYH/foKtjagZf/KO1/mnCITl86treIdachGgR4wr/qqMjrpPUaPLCRY3JQ +00XUP1Mu6YPE0SnMYAVxZheqKHly3a1pg4Xp7YWlM671oUORs3+VENfnbIxgr+2D +TiJT9PxwpfK53Oh7RBSWHJZRuAdLUXE8DG+bl0N/QkJM6pFUxTI1AQ== +-----END RSA PRIVATE KEY----- diff --git a/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt b/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt new file mode 100644 index 000000000000..458bcc5fe8cc --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIBATANBgkqhkiG9w0BAQUFADAaMQswCQYDVQQGEwJVUzEL +MAkGA1UECgwCWjQwHhcNMTMwODI4MTgyODM0WhcNMjMwODI4MTgyODM0WjAaMQsw +CQYDVQQGEwJVUzELMAkGA1UECgwCWjQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDfdOqotHd55SYO0dLz2oXengw/tZ+q3ZmOPeVmMuOMIYO/Cv1wk2U0 +OK4pug4OBSJPhl09Zs6IwB8NwPOU7EDTgMOcQUYB/6QNCI1J7Zm2oLtuchzz4pIb ++o4ZAhVprLhRyvqi8OTKQ7kfGfs5Tuwmn1M/0fQkfzMxADpjOKNgf0uy6lN6utjd +TrPKKFUQNdc6/Ty8EeTnQEwUlsT2LAXCfEKxTn5RlRljDztS7Sfgs8VL0FPy1Qi8 +B+dFcgRYKFrcpsVaZ1lBmXKsXDRu5QR/Rg3f9DRq4GR1sNH8RLY9uApMl2SNz+sR +4zRPG85R/se5Q06Gu0BUQ3UPm67ETVZLAgMBAAGjUDBOMB0GA1UdDgQWBBQHZPTE +yQVu/0I/3QWhlTyW7WoTzTAfBgNVHSMEGDAWgBQHZPTEyQVu/0I/3QWhlTyW7WoT +zTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQDHxqJ9y8alTH7agVMW +Zfic/RbrdvHwyq+IOrgDToqyo0w+IZ6BCn9vjv5iuhqu4ForOWDAFpQKZW0DLBJE +Qy/7/0+9pk2DPhK1XzdOovlSrkRt+GcEpGnUXnzACXDBbO0+Wrk+hcjEkQRRK1bW +2rknARIEJG9GS+pShP9Bq/0BmNsMepdNcBa0z3a5B0fzFyCQoUlX6RTqxRw1h1Qt +5F00pfsp7SjXVIvYcewHaNASbto1n5hrSz1VY9hLba11ivL1N4WoWbmzAL6BWabs +C2D/MenST2/X6hTKyGXpg3Eg2h3iLvUtwcNny0hRKstc73Jl9xR3qXfXKJH0ThTl +q0gq +-----END CERTIFICATE----- diff --git a/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx b/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx new file mode 100644 index 000000000000..7754333d91e0 Binary files /dev/null and b/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx differ diff --git a/sdk/spring/azure-spring-boot/src/test/resources/vcap1.json b/sdk/spring/azure-spring-boot/src/test/resources/vcap1.json new file mode 100644 index 000000000000..8854cb826242 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/vcap1.json @@ -0,0 +1,21 @@ +{ + "azure-storage": [ + { + "credentials": { + "primary_access_key": "pak", + "secondary_access_key": "sak", + "storage_account_name": "sam" + }, + "label": "azure-storage", + "name": "azure-storage-service", + "plan": "standard", + "provider": "provider", + "syslog_drain_url": null, + "tags": [ + "Azure", + "Storage" + ], + "volume_mounts": [] + } + ] +} diff --git a/sdk/spring/azure-spring-boot/src/test/resources/vcap2.json b/sdk/spring/azure-spring-boot/src/test/resources/vcap2.json new file mode 100644 index 000000000000..454f12c3c4bc --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/vcap2.json @@ -0,0 +1,20 @@ +{ + "azure-documentdb": [ + { + "credentials": { + "documentdb_database_id": "docdb123mj", + "documentdb_database_link": "dbs/ZFxCAA==/", + "documentdb_host_endpoint": "https://hostname:443/", + "documentdb_master_key": "3becR7JFnWamMvGwWYWWTV4WpeNhN8tOzJ74yjAxPKDpx65q2lYz60jt8WXU6HrIKrAIwhs0Hglf0123456789==" + }, + "label": "azure-documentdb", + "name": "mydocumentdb", + "plan": "standard", + "provider": null, + "syslog_drain_url": null, + "tags": [], + "volume_mounts": [] + } + ] +} + \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/test/resources/vcap3.json b/sdk/spring/azure-spring-boot/src/test/resources/vcap3.json new file mode 100644 index 000000000000..563d4e3da258 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/resources/vcap3.json @@ -0,0 +1,20 @@ +{ + "user-provided": [ + { + "credentials": { + "azure-service-broker-name": "azure-documentdb", + "azure-service-plan": "standard", + "documentdb_database_id": "docdb123mj", + "documentdb_database_link": "dbs/ZFxCAA==/", + "documentdb_host_endpoint": "https://hostname:443/", + "documentdb_master_key": "3becR7JFnWamMvGwWYWWTV4WpeNhN8tOzJ74yjAxPKDpx65q2lYz60jt8WXU6HrIKrAIwhs0Hglf0123456789==" + }, + "label": "user-provided", + "name": "mydocumentdb", + "syslog_drain_url": null, + "tags": [], + "volume_mounts": [] + } + ] +} + \ No newline at end of file diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index 9c31a0e88d2c..1fd5c6270dac 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -10,11 +10,12 @@ resources: type: github name: Azure/azure-sdk-tools endpoint: azure - + trigger: branches: include: - master + - feature/* - hotfix/* - release/* paths: @@ -31,11 +32,50 @@ pr: paths: include: - sdk/spring/ - + stages: - template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml parameters: ServiceDirectory: spring Artifacts: - - name: azure-spring-something - safeName: azurespringsomething + - name: azure-spring-boot + groupId: com.microsoft.azure + safeName: azurespringboot + - name: azure-spring-boot-starter + groupId: com.microsoft.azure + safeName: azurespringbootstarter + - name: azure-spring-boot-starter-active-directory + groupId: com.microsoft.azure + safeName: azurespringbootstarteractivedirectory + - name: azure-spring-boot-starter-active-directory-b2c + groupId: com.microsoft.azure + safeName: azurespringbootstarteractivedirectoryb2c + - name: azure-spring-boot-starter-cosmosdb + groupId: com.microsoft.azure + safeName: azurespringbootstartercosmosdb + - name: azure-spring-boot-starter-data-gremlin + groupId: com.microsoft.azure + safeName: azurespringbootstarterdatagremlin + - name: azure-spring-boot-starter-keyvault-secrets + groupId: com.microsoft.azure + safeName: azurespringbootstarterkeyvaultsecrets + - name: azure-spring-boot-starter-mediaservices + groupId: com.microsoft.azure + safeName: azurespringbootstartermediaservices + - name: azure-spring-boot-starter-metrics + groupId: com.microsoft.azure + safeName: azurespringbootstartermetrics + - name: azure-spring-boot-starter-servicebus + groupId: com.microsoft.azure + safeName: azurespringbootstarterservicebus + - name: azure-spring-boot-starter-servicebus-jms + groupId: com.microsoft.azure + safeName: azurespringbootstarterservicebusjms + - name: azure-spring-boot-starter-storage + groupId: com.microsoft.azure + safeName: azurespringbootstarterstorage + + + + + diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml new file mode 100644 index 000000000000..0c62359b4ac8 --- /dev/null +++ b/sdk/spring/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + com.azure + azure-spring-boot-service + pom + 1.0.0 + + azure-spring-boot + azure-spring-boot-bom + azure-spring-boot-starter + azure-spring-boot-starter-active-directory + azure-spring-boot-starter-active-directory-b2c + azure-spring-boot-starter-cosmosdb + azure-spring-boot-starter-data-gremlin + azure-spring-boot-starter-keyvault-secrets + azure-spring-boot-starter-metrics + azure-spring-boot-starter-mediaservices + azure-spring-boot-starter-servicebus-jms + azure-spring-boot-starter-servicebus + azure-spring-boot-starter-storage + + + +