Skip to content

Commit

Permalink
feat: User managed identity support added. (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoniotarricone authored Oct 17, 2024
1 parent 038ba74 commit 4757709
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* AzureSystemManagedIdentityClient.java
*
* 7 ago 2024
*/
package it.pagopa.swclient.mil.azureservices.identity.client.usermanaged;

import java.net.URI;
import java.util.Optional;

import org.eclipse.microprofile.config.inject.ConfigProperty;

import io.quarkus.logging.Log;
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import io.smallrye.mutiny.Uni;
import it.pagopa.swclient.mil.azureservices.identity.bean.AccessToken;
import it.pagopa.swclient.mil.azureservices.identity.client.AzureIdentityClient;
import jakarta.enterprise.context.ApplicationScoped;

/**
* <p>
* Reactive client (it's a proxy of REST client) to get access token from Microsoft Entra ID by
* means of User Managed Identity.
* </p>
* <p>
* To use this method, the environment variable {@code IDENTITY_CLIENT_ID} must be set.
* </p>
*
* @author Antonio Tarricone
*/
@ApplicationScoped
public class AzureUserManagedIdentityClient implements AzureIdentityClient {
/**
* <p>
* Reactive REST client to get access token from Microsoft Entra ID by means of User Managed
* Identity.
* </p>
*
* @see it.pagopa.swclient.mil.azureservices.identity.client.usermanaged.AzureUserManagedIdentityRestClient
* AzureSystemManagedIdentityRestClient
*/
private AzureUserManagedIdentityRestClient restClient;

/**
* <p>
* Constructor.
* </p>
*
* @param identityEndpoint Endpoint to get access token by means of user managed identity
*/
AzureUserManagedIdentityClient(@ConfigProperty(name = "IDENTITY_ENDPOINT") Optional<String> identityEndpoint) {
Log.trace("Azure User Managed Identity client initialization");
restClient = QuarkusRestClientBuilder.newBuilder()
.baseUri(URI.create(identityEndpoint.orElseThrow()))
.build(AzureUserManagedIdentityRestClient.class);
}

/**
* @see it.pagopa.swclient.mil.azureservices.identity.client.systemmanaged.AzureSystemManagedIdentityRestClient#getAccessToken(String)
*/
@Override
public Uni<AccessToken> getAccessToken(String scope) {
Log.tracef("Get access token with System Managed Identity for %s", scope);
return restClient.getAccessToken(scope);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* AzureUserManagedIdentityRestClient.java
*
* 17 ott 2024
*/
package it.pagopa.swclient.mil.azureservices.identity.client.usermanaged;

import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;

import io.quarkus.rest.client.reactive.ClientQueryParam;
import io.smallrye.mutiny.Uni;
import it.pagopa.swclient.mil.azureservices.identity.bean.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

/**
* <p>
* Reactive REST client to get access token from Microsoft Entra ID by means of User Managed
* Identity.
* </p>
* <p>
* To use this method, the environment variable {@code IDENTITY_CLIENT_ID} must be set.
* </p>
*
* @author Antonio Tarricone
*/
public interface AzureUserManagedIdentityRestClient {
/**
* <p>
* Retrieves an access token for an Azure resource.
* </p>
*
* @param scope {@link it.pagopa.swclient.mil.azureservices.identity.bean.Scope Scope}
* @return {@link it.pagopa.swclient.mil.azureservices.identity.bean.AccessToken AccessToken}
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@ClientQueryParam(name = "api-version", value = "2019-08-01")
@ClientQueryParam(name = "client_id", value = "${IDENTITY_CLIENT_ID}")
@ClientHeaderParam(name = "x-identity-header", value = "${IDENTITY_HEADER}")
Uni<AccessToken> getAccessToken(@QueryParam("resource") String scope);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import it.pagopa.swclient.mil.azureservices.identity.bean.AccessToken;
import it.pagopa.swclient.mil.azureservices.identity.client.AzureIdentityClient;
import it.pagopa.swclient.mil.azureservices.identity.client.systemmanaged.AzureSystemManagedIdentityClient;
import it.pagopa.swclient.mil.azureservices.identity.client.usermanaged.AzureUserManagedIdentityClient;
import it.pagopa.swclient.mil.azureservices.identity.client.workload.AzureWorkloadIdentityClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
Expand Down Expand Up @@ -54,7 +55,8 @@ public class AzureIdentityReactiveService {
* Constructor.
* </p>
*
* @param identityEndpoint Endpoint to get access token by means of system managed identity
* @patam identityClientId Client ID to get access token by means of user managed identity
* @param identityEndpoint Endpoint to get access token by means of system/user managed identity
* @param identityHeader Value to use to set x-identity-header
* @param authorityHost Endpoint to get access token by means of workload identity
* @param tenantId Tenant ID
Expand All @@ -64,6 +66,7 @@ public class AzureIdentityReactiveService {
*/
@Inject
AzureIdentityReactiveService(
@ConfigProperty(name = "IDENTITY_CLIENT_ID") Optional<String> identityClientId,
@ConfigProperty(name = "IDENTITY_ENDPOINT") Optional<String> identityEndpoint,
@ConfigProperty(name = "IDENTITY_HEADER") Optional<String> identityHeader,
@ConfigProperty(name = "AZURE_AUTHORITY_HOST") Optional<String> authorityHost,
Expand All @@ -74,15 +77,18 @@ public class AzureIdentityReactiveService {
/*
* Initialize identity client.
*/
if (identityEndpoint.isPresent() && identityHeader.isPresent()) {
if (identityEndpoint.isPresent() && identityHeader.isPresent() && identityClientId.isPresent()) {
Log.debug("Azure User Managed Identity will be use");
identityClient = anyIdentityClient.select(AzureUserManagedIdentityClient.class).get();
} else if (identityEndpoint.isPresent() && identityHeader.isPresent()) {
Log.debug("Azure System Managed Identity will be use");
identityClient = anyIdentityClient.select(AzureSystemManagedIdentityClient.class).get();
} else if (authorityHost.isPresent() && tenantId.isPresent() && clientId.isPresent() && federatedTokenFile.isPresent()) {
Log.debug("Azure Workload Identity will be use");
identityClient = anyIdentityClient.select(AzureWorkloadIdentityClient.class).get();
} else {
Log.fatal("IDENTITY_ENDPOINT and IDENTITY_HEADER must not be null or AZURE_AUTHORITY_HOST and AZURE_TENANT_ID and AZURE_CLIENT_ID and AZURE_FEDERATED_TOKEN_FILE must not be null");
throw new DeploymentException("IDENTITY_ENDPOINT and IDENTITY_HEADER must not be null or AZURE_AUTHORITY_HOST and AZURE_TENANT_ID and AZURE_CLIENT_ID and AZURE_FEDERATED_TOKEN_FILE must not be null");
Log.fatal("IDENTITY_CLIENT_ID and IDENTITY_ENDPOINT and IDENTITY_HEADER must not be null or IDENTITY_ENDPOINT and IDENTITY_HEADER must not be null or AZURE_AUTHORITY_HOST and AZURE_TENANT_ID and AZURE_CLIENT_ID and AZURE_FEDERATED_TOKEN_FILE must not be null");
throw new DeploymentException("IDENTITY_CLIENT_ID and IDENTITY_ENDPOINT and IDENTITY_HEADER must not be null or IDENTITY_ENDPOINT and IDENTITY_HEADER must not be null or AZURE_AUTHORITY_HOST and AZURE_TENANT_ID and AZURE_CLIENT_ID and AZURE_FEDERATED_TOKEN_FILE must not be null");
}

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* AzureUserManagedIdentityClientTest.java
*
* 17 ott 2024
*/
package it.pagopa.swclient.mil.azureservices.identity.client.usermanaged;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

import java.net.URI;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.mockito.MockedStatic;

import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
import it.pagopa.swclient.mil.azureservices.identity.bean.AccessToken;
import it.pagopa.swclient.mil.azureservices.identity.bean.Scope;

/**
*
* @author Antonio.tarricone
*/
@QuarkusTest
class AzureUserManagedIdentityClientTest {
/**
*
* @param testInfo
*/
@BeforeEach
void init(TestInfo testInfo) {
String frame = "*".repeat(testInfo.getDisplayName().length() + 11);
System.out.println(frame);
System.out.printf("* %s: START *%n", testInfo.getDisplayName());
System.out.println(frame);
}

/**
*
*/
@Test
void given_requestToGetAccessToken_when_requestIsDone_then_returnAccessToken() {
/*
* Mocking of REST client.
*/
Instant now = Instant.now();
AccessToken accessToken = new AccessToken()
.setExpiresOn(now.plus(5, ChronoUnit.MINUTES).getEpochSecond())
.setValue("access_token_string");

AzureUserManagedIdentityRestClient restClient = mock(AzureUserManagedIdentityRestClient.class);
when(restClient.getAccessToken(Scope.STORAGE))
.thenReturn(Uni.createFrom()
.item(accessToken));

/*
* Mocking of QuarkusRestClientBuilder.
*/
QuarkusRestClientBuilder clientBuilder = mock(QuarkusRestClientBuilder.class);

when(clientBuilder.build(AzureUserManagedIdentityRestClient.class))
.thenReturn(restClient);

when(clientBuilder.baseUri(any(URI.class)))
.thenReturn(clientBuilder);

/*
* Mocking of QuarkusRestClientBuilder factory.
*/
try (MockedStatic<QuarkusRestClientBuilder> restClientBuilderFactory = mockStatic(QuarkusRestClientBuilder.class)) {
restClientBuilderFactory.when(() -> QuarkusRestClientBuilder.newBuilder())
.thenReturn(clientBuilder);

/*
* Test.
*/
AzureUserManagedIdentityClient client = new AzureUserManagedIdentityClient(Optional.of("https://login.microsoftonline.com/"));
client.getAccessToken(Scope.STORAGE)
.subscribe()
.withSubscriber(UniAssertSubscriber.create())
.awaitItem()
.assertItem(accessToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import it.pagopa.swclient.mil.azureservices.identity.bean.Scope;
import it.pagopa.swclient.mil.azureservices.identity.client.AzureIdentityClient;
import it.pagopa.swclient.mil.azureservices.identity.client.systemmanaged.AzureSystemManagedIdentityClient;
import it.pagopa.swclient.mil.azureservices.identity.client.usermanaged.AzureUserManagedIdentityClient;
import it.pagopa.swclient.mil.azureservices.identity.client.workload.AzureWorkloadIdentityClient;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.spi.DeploymentException;
Expand Down Expand Up @@ -80,6 +81,7 @@ void given_emptyCache_when_getAccessTokenInvoked_then_getNewOneCacheAndReturnIt(
* Test
*/
AzureIdentityReactiveService identityService = spy(new AzureIdentityReactiveService(
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of("https://login.microsoftonline.com/"),
Expand Down Expand Up @@ -126,6 +128,7 @@ void given_storedAccessToken_when_getAccessTokenInvoked_then_getReturnIt() {
* Test
*/
AzureIdentityReactiveService identityService = spy(new AzureIdentityReactiveService(
Optional.empty(),
Optional.of("https://login.microsoftonline.com/"),
Optional.of("45ed57a0-ec26-41c9-8333-29daf37697d3"),
Optional.empty(),
Expand Down Expand Up @@ -183,6 +186,7 @@ void given_expAccessTokenStored_when_getAccessTokenInvoked_then_getNewOneCacheAn
* Test
*/
AzureIdentityReactiveService identityService = spy(new AzureIdentityReactiveService(
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of("https://login.microsoftonline.com/"),
Expand Down Expand Up @@ -222,6 +226,7 @@ void given_systemManagedIdEnvironment_when_invokeGet_then_returnSuitableClient()
.thenReturn(identityClientInstance);

AzureIdentityReactiveService service = new AzureIdentityReactiveService(
Optional.empty(),
Optional.of("https://login.microsoftonline.com/"),
Optional.of("45ed57a0-ec26-41c9-8333-29daf37697d3"),
Optional.empty(),
Expand All @@ -237,10 +242,57 @@ void given_systemManagedIdEnvironment_when_invokeGet_then_returnSuitableClient()
*
*/
@Test
void given_partialsystemManagedIdEnvironment_when_invokeGet_then_throwException() {
void given_partialSystemManagedIdEnvironment_when_invokeGet_then_throwException() {
assertThrows( // NOSONAR
DeploymentException.class,
() -> new AzureIdentityReactiveService(
Optional.empty(),
Optional.of("https://login.microsoftonline.com/"),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
null));
}

/**
*
*/
@Test
void given_userManagedIdEnvironment_when_invokeGet_then_returnSuitableClient() {
AzureUserManagedIdentityClient identityClient = mock(AzureUserManagedIdentityClient.class);

Instance<AzureUserManagedIdentityClient> identityClientInstance = mock(Instance.class);
when(identityClientInstance.get())
.thenReturn(identityClient);

Instance<AzureIdentityClient> anyIdentityClient = mock(Instance.class);
when(anyIdentityClient.select(AzureUserManagedIdentityClient.class))
.thenReturn(identityClientInstance);

AzureIdentityReactiveService service = new AzureIdentityReactiveService(
Optional.of("67a40498-91c1-4e4c-9c43-8aeb09c0de5e"),
Optional.of("https://login.microsoftonline.com/"),
Optional.of("45ed57a0-ec26-41c9-8333-29daf37697d3"),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
anyIdentityClient);

assertTrue(service.getIdentityClient() instanceof AzureUserManagedIdentityClient);
}

/**
*
*/
@Test
void given_partialUserManagedIdEnvironment_when_invokeGet_then_throwException() {
assertThrows( // NOSONAR
DeploymentException.class,
() -> new AzureIdentityReactiveService(
Optional.empty(),
Optional.of("https://login.microsoftonline.com/"),
Optional.empty(),
Optional.empty(),
Expand All @@ -266,6 +318,7 @@ void given_workloadIdEnvironment_when_invokeGet_then_returnSuitableClient() {
.thenReturn(identityClientInstance);

AzureIdentityReactiveService service = new AzureIdentityReactiveService(
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of("https://login.microsoftonline.com/"),
Expand All @@ -285,6 +338,7 @@ void given_partialWorkloadIdEnvironment_when_invokeGet_then_throwException() {
assertThrows( // NOSONAR
DeploymentException.class,
() -> new AzureIdentityReactiveService(
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.of("https://login.microsoftonline.com/"),
Expand All @@ -308,6 +362,7 @@ void given_noIdentityEnvironment_when_invokeGet_then_throwException() {
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
null));
}
}

0 comments on commit 4757709

Please sign in to comment.