Skip to content

Commit

Permalink
Managed Identity Resource ID (#26824)
Browse files Browse the repository at this point in the history
* Managed Identity Resource ID

Add the ability to use a managed identity resource ID for supported services.

* Add test

* Fix spotbugs error

* fix typos

* Add e2e test for resource ID

* Move to 2019 endpoint for App Service

* remove extra params

* Update CHANGELOG

* Fix test failure

* Fixup javadocs

* pr feedback

* fix comment

* fix tests after rebase
  • Loading branch information
billwert authored Feb 14, 2022
1 parent e0e9ac6 commit 36cc765
Show file tree
Hide file tree
Showing 18 changed files with 251 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class IdentityTest {
*/
public static void main(String[] args) throws IllegalStateException {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(AZURE_IDENTITY_TEST_PLATFORM))) {
throw new IllegalStateException("Identity Test platform is not set. Set environemnt "
throw new IllegalStateException("Identity Test platform is not set. Set environment "
+ "variable AZURE_IDENTITY_TEST_PLATFORM to webjobs");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class WebJobsIdentityTest {
private static final Configuration CONFIGURATION = Configuration.getGlobalConfiguration().clone();
private static final String PROPERTY_IDENTITY_ENDPOINT = "IDENTITY_ENDPOINT";
private static final String PROPERTY_IDENTITY_HEADER = "IDENTITY_HEADER";
private static final String RESOURCE_ID = "RESOURCE_ID";
private final ClientLogger logger = new ClientLogger(WebJobsIdentityTest.class);

/**
Expand All @@ -38,8 +39,8 @@ class WebJobsIdentityTest {
*/
void run() throws IllegalStateException {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(AZURE_WEBJOBS_TEST_MODE))) {
throw logger.logExceptionAsError(new IllegalStateException("Webjobs Test mode is not set. Set environemnt "
+ "variable AZURE_WEBJOBS_TEST_MODE to user or system"));
throw logger.logExceptionAsError(new IllegalStateException("Webjobs Test mode is not set. Set environment "
+ "variable AZURE_WEBJOBS_TEST_MODE to user, userresourceid, or system"));
}

String mode = CONFIGURATION.get(AZURE_WEBJOBS_TEST_MODE).toLowerCase(Locale.ENGLISH);
Expand All @@ -49,6 +50,10 @@ void run() throws IllegalStateException {
identityTest.testMSIEndpointWithUserAssigned();
identityTest.testMSIEndpointWithUserAssignedAccessKeyVault();
break;
case "userresourceid":
identityTest.testMSIEndpointWithUserAssignedByResourceId();
identityTest.testMSIEndpointWithUserAssignedAccessKeyVaultByResourceId();
break;
case "system":
identityTest.testMSIEndpointWithSystemAssigned();
identityTest.testMSIEndpointWithSystemAssignedAccessKeyVault();
Expand All @@ -62,8 +67,8 @@ void run() throws IllegalStateException {
}

private void testMSIEndpointWithSystemAssigned() {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT))
&& CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_MSI_SECRET))) {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_IDENTITY_ENDPOINT))
&& CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_IDENTITY_HEADER))) {
throw logger.logExceptionAsError(
new IllegalStateException("testMSIEndpointWithUserAssigned - MSIEndpoint and Identity Point not"
+ "configured in the environment. At least one should be configured"));
Expand All @@ -74,8 +79,6 @@ private void testMSIEndpointWithSystemAssigned() {
AccessToken accessToken = client.authenticateToManagedIdentityEndpoint(
CONFIGURATION.get(PROPERTY_IDENTITY_ENDPOINT),
CONFIGURATION.get(PROPERTY_IDENTITY_HEADER),
CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT),
CONFIGURATION.get(Configuration.PROPERTY_MSI_SECRET),
new TokenRequestContext().addScopes("https://management.azure.com/.default"))
.flatMap(token -> {
if (token == null || token.getToken() == null) {
Expand Down Expand Up @@ -123,11 +126,11 @@ private void testMSIEndpointWithSystemAssignedAccessKeyVault() {
}

private void testMSIEndpointWithUserAssigned() {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT))
&& CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_MSI_SECRET))) {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_IDENTITY_ENDPOINT))
&& CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_IDENTITY_HEADER))) {
throw logger.logExceptionAsError(
new IllegalStateException("testMSIEndpointWithUserAssigned - MSIEndpoint and Identity Point not"
+ "configured in the environment. Atleast one should be configuured"));
+ "configured in the environment. At least one should be configured"));
}
assertConfigPresence(Configuration.PROPERTY_AZURE_CLIENT_ID,
"testMSIEndpointWithUserAssigned - Client is not configured in the environment.");
Expand All @@ -141,8 +144,6 @@ private void testMSIEndpointWithUserAssigned() {
AccessToken accessToken = client.authenticateToManagedIdentityEndpoint(
CONFIGURATION.get(PROPERTY_IDENTITY_ENDPOINT),
CONFIGURATION.get(PROPERTY_IDENTITY_HEADER),
CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT),
CONFIGURATION.get(Configuration.PROPERTY_MSI_SECRET),
new TokenRequestContext().addScopes("https://management.azure.com/.default"))
.flatMap(token -> {
if (token == null || token.getToken() == null) {
Expand Down Expand Up @@ -194,17 +195,87 @@ private void testMSIEndpointWithUserAssignedAccessKeyVault() {
"Error: Secret name didn't match expected name - testMSIEndpointWithUserAssignedAccessKeyVault - failed");
}

private void assertExpectedValue(String expected, String actual, String success, String faiure) {
private void testMSIEndpointWithUserAssignedByResourceId() {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT))
&& CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_MSI_SECRET))) {
throw logger.logExceptionAsError(
new IllegalStateException("testMSIEndpointWithUserAssignedByResourceId - MSIEndpoint and Identity Point not"
+ "configured in the environment. At least one should be configured"));
}
assertConfigPresence(RESOURCE_ID,
"testMSIEndpointWithUserAssignedByResourceId - Resource ID is not configured in the environment.");
assertConfigPresence(AZURE_VAULT_URL,
"testMSIEndpointWithUserAssignedByResourceId - Vault URL is not configured in the environment.");

IdentityClient client = new IdentityClientBuilder()
.resourceId(CONFIGURATION.get(RESOURCE_ID))
.build();

AccessToken accessToken = client.authenticateToManagedIdentityEndpoint(
CONFIGURATION.get(PROPERTY_IDENTITY_ENDPOINT),
CONFIGURATION.get(PROPERTY_IDENTITY_HEADER),
new TokenRequestContext().addScopes("https://management.azure.com/.default"))
.flatMap(token -> {
if (token == null || token.getToken() == null) {
return Mono.error(logger.logExceptionAsError(new IllegalStateException(
"Access Token not returned from System Assigned Identity")));
} else {
return Mono.just(token);
}
}).block();

if (accessToken == null) {
System.out.println("Error: Access token is null.");
return;
}

System.out.printf("Received token with length %d and expiry at %s %n "
+ "testMSIEndpointWithUserAssignedByResourceId - succeeded %n",
accessToken.getToken().length(), accessToken.getExpiresAt().format(DateTimeFormatter.ISO_DATE_TIME));
}

private void testMSIEndpointWithUserAssignedAccessKeyVaultByResourceId() {

if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT) )
&& CoreUtils.isNullOrEmpty(CONFIGURATION.get("IDENTITY_ENDPOINT"))) {

}

assertConfigPresence(Configuration.PROPERTY_MSI_ENDPOINT,
"testMSIEndpointWithUserAssignedKeyVaultByResourceId - MSIEndpoint not configured in the environment.");
assertConfigPresence(RESOURCE_ID,
"testMSIEndpointWithUserAssignedKeyVaultByResourceId - Resource ID is not configured in the environment.");
assertConfigPresence(AZURE_VAULT_URL,
"testMSIEndpointWithUserAssignedKeyVaultByResourceId - Vault URL is not configured in the environment.");

ManagedIdentityCredential credential = new ManagedIdentityCredentialBuilder()
.resourceId(CONFIGURATION.get(RESOURCE_ID))
.build();
SecretClient client = new SecretClientBuilder()
.credential(credential)
.vaultUrl(CONFIGURATION.get(AZURE_VAULT_URL))
.buildClient();

KeyVaultSecret secret = client.getSecret(VAULT_SECRET_NAME);
System.out.printf("testMSIEndpointWithSystemAssignedAccessKeyVault - "
+ "Retrieved Secret with name %s and value %s %n",
secret.getName(), secret.getValue());
assertExpectedValue(VAULT_SECRET_NAME, secret.getName(),
"SUCCESS: Secret matched - testMSIEndpointWithUserAssignedAccessKeyVault - succeeded",
"Error: Secret name didn't match expected name - testMSIEndpointWithUserAssignedAccessKeyVault - failed");
}

private void assertExpectedValue(String expected, String actual, String success, String failure) {
if (expected.equals(actual)) {
System.out.println(success);
return;
}
System.out.println(faiure);
System.out.println(failure);
}


private void assertConfigPresence(String identitfer, String errorMessage) {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(identitfer))) {
private void assertConfigPresence(String identifier, String errorMessage) {
if (CoreUtils.isNullOrEmpty(CONFIGURATION.get(identifier))) {
throw logger.logExceptionAsError(new IllegalStateException(errorMessage));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,14 @@ public class ManagedIdentityCredentialLiveTest {

@Test
public void testMSIEndpointWithSystemAssigned() throws Exception {
org.junit.Assume.assumeNotNull(CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT));
org.junit.Assume.assumeNotNull(CONFIGURATION.get(Configuration.PROPERTY_IDENTITY_ENDPOINT));
org.junit.Assume.assumeTrue(CONFIGURATION.get(Configuration.PROPERTY_AZURE_CLIENT_ID) == null);
org.junit.Assume.assumeNotNull(CONFIGURATION.get(AZURE_VAULT_URL));

IdentityClient client = new IdentityClientBuilder().build();
StepVerifier.create(client.authenticateToManagedIdentityEndpoint(
CONFIGURATION.get(PROPERTY_IDENTITY_ENDPOINT),
CONFIGURATION.get(PROPERTY_IDENTITY_HEADER),
CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT),
CONFIGURATION.get(Configuration.PROPERTY_MSI_SECRET),
new TokenRequestContext().addScopes("https://management.azure.com/.default")))
.expectNextMatches(accessToken -> accessToken != null && accessToken.getToken() != null)
.verifyComplete();
Expand All @@ -66,7 +64,7 @@ public void testMSIEndpointWithSystemAssignedAccessKeyVault() throws Exception {

@Test
public void testMSIEndpointWithUserAssigned() throws Exception {
org.junit.Assume.assumeNotNull(CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT));
org.junit.Assume.assumeNotNull(CONFIGURATION.get(Configuration.PROPERTY_IDENTITY_ENDPOINT));
org.junit.Assume.assumeNotNull(CONFIGURATION.get(Configuration.PROPERTY_AZURE_CLIENT_ID));
org.junit.Assume.assumeNotNull(CONFIGURATION.get(AZURE_VAULT_URL));

Expand All @@ -76,8 +74,6 @@ public void testMSIEndpointWithUserAssigned() throws Exception {
StepVerifier.create(client.authenticateToManagedIdentityEndpoint(
CONFIGURATION.get(PROPERTY_IDENTITY_ENDPOINT),
CONFIGURATION.get(PROPERTY_IDENTITY_HEADER),
CONFIGURATION.get(Configuration.PROPERTY_MSI_ENDPOINT),
CONFIGURATION.get(Configuration.PROPERTY_MSI_SECRET),
new TokenRequestContext().addScopes("https://management.azure.com/.default")))
.expectNextMatches(accessToken -> accessToken != null && accessToken.getToken() != null)
.verifyComplete();
Expand Down
2 changes: 2 additions & 0 deletions sdk/identity/azure-identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
## 1.5.0-beta.1 (Unreleased)

### Features Added
- Added `resourceId` to Managed Identity for Virtual Machines, App Service, and Service Bus.

### Breaking Changes

### Bugs Fixed

### Other Changes
- Upgraded App Service Managed Identity endpoint to `2019-08-01`.

## 1.4.4 (2022-02-07)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
*/
@Immutable
class AppServiceMsiCredential extends ManagedIdentityServiceCredential {
private final String msiEndpoint;
private final String msiSecret;
private final String identityEndpoint;
private final String identityHeader;
private final ClientLogger logger = new ClientLogger(AppServiceMsiCredential.class);

/**
Expand All @@ -29,10 +29,10 @@ class AppServiceMsiCredential extends ManagedIdentityServiceCredential {
AppServiceMsiCredential(String clientId, IdentityClient identityClient) {
super(clientId, identityClient, "AZURE APP SERVICE MSI/IDENTITY ENDPOINT");
Configuration configuration = Configuration.getGlobalConfiguration().clone();
this.msiEndpoint = configuration.get(Configuration.PROPERTY_MSI_ENDPOINT);
this.msiSecret = configuration.get(Configuration.PROPERTY_MSI_SECRET);
if (msiEndpoint != null) {
validateEndpointProtocol(this.msiEndpoint, "MSI", logger);
this.identityEndpoint = configuration.get(Configuration.PROPERTY_IDENTITY_ENDPOINT);
this.identityHeader = configuration.get(Configuration.PROPERTY_IDENTITY_HEADER);
if (identityEndpoint != null) {
validateEndpointProtocol(this.identityEndpoint, "MSI", logger);
}
}

Expand All @@ -43,7 +43,7 @@ class AppServiceMsiCredential extends ManagedIdentityServiceCredential {
* @return A publisher that emits an {@link AccessToken}.
*/
public Mono<AccessToken> authenticate(TokenRequestContext request) {
return identityClient.authenticateToManagedIdentityEndpoint(null, null, msiEndpoint,
msiSecret, request);
return identityClient.authenticateToManagedIdentityEndpoint(identityEndpoint, identityHeader,
request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import com.azure.core.credential.TokenCredential;
import com.azure.core.util.Configuration;
import com.azure.core.util.logging.ClientLogger;

import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
Expand All @@ -17,6 +18,8 @@
*/
class AzureApplicationCredentialBuilder extends CredentialBuilderBase<AzureApplicationCredentialBuilder> {
private String managedIdentityClientId;
private String managedIdentityResouceId;
private final ClientLogger logger = new ClientLogger(AzureApplicationCredentialBuilder.class);

/**
* Creates an instance of a AzureApplicationCredentialBuilder.
Expand Down Expand Up @@ -51,6 +54,18 @@ public AzureApplicationCredentialBuilder managedIdentityClientId(String clientId
return this;
}

/**
* Specifies the resource ID of user assigned or system assigned identity, when this credential is running
* in an environment with managed identities.
*
* @param resourceId the resource ID
* @return An updated instance of this builder with the managed identity client id set as specified.
*/
public AzureApplicationCredentialBuilder managedIdentityResourceId(String resourceId) {
this.managedIdentityResouceId = resourceId;
return this;
}

/**
* Specifies the ExecutorService to be used to execute the authentication requests.
* Developer is responsible for maintaining the lifecycle of the ExecutorService.
Expand All @@ -74,17 +89,22 @@ public AzureApplicationCredentialBuilder executorService(ExecutorService executo

/**
* Creates new {@link AzureApplicationCredential} with the configured options set.
*
* @return a {@link AzureApplicationCredential} with the current configurations.
* @throws IllegalStateException if clientId and resourceId are both set.
*/
public AzureApplicationCredential build() {
if (managedIdentityClientId != null && managedIdentityResouceId != null) {
throw logger.logExceptionAsError(
new IllegalStateException("Only one of managedIdentityClientId and managedIdentityResourceId can be specified."));
}

return new AzureApplicationCredential(getCredentialsChain());
}

private ArrayList<TokenCredential> getCredentialsChain() {
ArrayList<TokenCredential> output = new ArrayList<TokenCredential>(2);
output.add(new EnvironmentCredential(identityClientOptions));
output.add(new ManagedIdentityCredential(managedIdentityClientId, identityClientOptions));
output.add(new ManagedIdentityCredential(managedIdentityClientId, managedIdentityResouceId, identityClientOptions));
return output;
}
}
Expand Down
Loading

0 comments on commit 36cc765

Please sign in to comment.