diff --git a/Authentication.md b/Authentication.md index 1240bf9a..a7cd5faa 100644 --- a/Authentication.md +++ b/Authentication.md @@ -460,6 +460,8 @@ The IAM access token is added to each outbound request in the `Authorization` he - iamProfileId: (optional) the id of the linked trusted IAM profile to be used when obtaining the IAM access token. +- iamProfileName: (optional) the name of the linked trusted IAM profile to be used as the identity of the compute resource. + - url: (optional) The base endpoint URL of the VPC Instance Metadata Service. The default value of this property is `http://169.254.169.254`. However, if the VPC Instance Metadata Service is configured with the HTTP Secure Protocol setting (`https`), then you should configure this property to be `https://api.metadata.cloud.ibm.com`. @@ -472,12 +474,12 @@ The default value is `2022-03-01`. When set to `2025-08-26`, the authenticator w The default value is `300` seconds. This property can only be configured programmatically (not via environment variables). Usage Notes: -1. At most one of `iamProfileCrn` or `iamProfileId` may be specified. The specified value must map +1. At most one of `iamProfileCrn` or `iamProfileId` or `iamProfileName` may be specified. The specified value must map to a trusted IAM profile that has been linked to the compute resource (virtual server instance). -2. If both `iamProfileCrn` and `iamProfileId` are specified, then an error occurs. +2. If more than one of `iamProfileCrn`, `iamProfileId` and `iamProfileName` are specified, then an error occurs. -3. If neither `iamProfileCrn` nor `iamProfileId` are specified, then the default trusted profile linked to the +3. If neither `iamProfileCrn`, `iamProfileId` nor `iamProfileName` are specified, then the default trusted profile linked to the compute resource will be used to perform the IAM token exchange. If no default trusted profile is defined for the compute resource, then an error occurs. diff --git a/src/main/java/com/ibm/cloud/sdk/core/security/AuthenticatorBase.java b/src/main/java/com/ibm/cloud/sdk/core/security/AuthenticatorBase.java index c840fd49..7e4b49bf 100644 --- a/src/main/java/com/ibm/cloud/sdk/core/security/AuthenticatorBase.java +++ b/src/main/java/com/ibm/cloud/sdk/core/security/AuthenticatorBase.java @@ -30,7 +30,7 @@ public class AuthenticatorBase { public static final String ERRORMSG_REQ_FAILED = "Error while fetching access token from token service: "; public static final String ERRORMSG_EXCLUSIVE_PROP_ERROR = "Exactly one of %s or %s must be specified."; public static final String ERRORMSG_ATLEAST_ONE_PROP_ERROR = "At least one of %s or %s must be specified."; - public static final String ERRORMSG_ATMOST_ONE_PROP_ERROR = "At most one of %s or %s may be specified."; + public static final String ERRORMSG_ATMOST_ONE_PROP_ERROR = "At most one of %s, %s or %s may be specified."; public static final String ERRORMSG_PROP_INVALID_INTEGER_VALUE = "The %s property must be a valid integer but was %s."; public static final String ERRORMSG_ACCOUNTID_PROP_ERROR = diff --git a/src/main/java/com/ibm/cloud/sdk/core/security/VpcInstanceAuthenticator.java b/src/main/java/com/ibm/cloud/sdk/core/security/VpcInstanceAuthenticator.java index 5a78c45d..64308a0e 100644 --- a/src/main/java/com/ibm/cloud/sdk/core/security/VpcInstanceAuthenticator.java +++ b/src/main/java/com/ibm/cloud/sdk/core/security/VpcInstanceAuthenticator.java @@ -58,6 +58,7 @@ public class VpcInstanceAuthenticator // Properties specific to a VpcInstanceAuthenticator. private String iamProfileCrn; private String iamProfileId; + private String iamProfileName; private String url; private String serviceVersion; private int tokenLifetime; @@ -68,6 +69,7 @@ public class VpcInstanceAuthenticator public static class Builder { private String iamProfileCrn; private String iamProfileId; + private String iamProfileName; private String url; private String serviceVersion; private int tokenLifetime; @@ -80,6 +82,7 @@ public Builder() { private Builder(VpcInstanceAuthenticator obj) { this.iamProfileCrn = obj.iamProfileCrn; this.iamProfileId = obj.iamProfileId; + this.iamProfileName = obj.iamProfileName; this.url = obj.url; this.serviceVersion = obj.serviceVersion; this.tokenLifetime = obj.tokenLifetime; @@ -100,7 +103,7 @@ public VpcInstanceAuthenticator build() { * * @param iamProfileCrn the CRN of the linked trusted IAM profile to be used as * the identity of the compute resource. At most one of - * iamProfileCrn or iamProfileId may be specified. If + * iamProfileCrn or iamProfileId or iamProfileName may be specified. If * neither one is specified, then the default IAM profile * defined for the compute resource will be used. * @return the Builder @@ -115,7 +118,7 @@ public Builder iamProfileCrn(String iamProfileCrn) { * * @param iamProfileId the id of the linked trusted IAM profile to be used as * the identity of the compute resource. At most one of - * iamProfileCrn or iamProfileId may be specified. If + * iamProfileCrn or iamProfileId or iamProfileName may be specified. If * neither one is specified, then the default IAM profile * defined for the compute resource will be used. * @return the Builder @@ -125,6 +128,21 @@ public Builder iamProfileId(String iamProfileId) { return this; } + /** + * Sets the iamProfileName property. + * + * @param iamProfileName the name of the linked trusted IAM profile to be used as + * the identity of the compute resource. At most one of + * iamProfileCrn or iamProfileId or iamProfileName may be specified. If + * neither one is specified, then the default IAM profile + * defined for the compute resource will be used. + * @return the Builder + */ + public Builder iamProfileName(String iamProfileName) { + this.iamProfileName = iamProfileName; + return this; + } + /** * Sets the url property. * @@ -174,6 +192,7 @@ protected VpcInstanceAuthenticator(Builder builder) { this(); this.iamProfileCrn = builder.iamProfileCrn; this.iamProfileId = builder.iamProfileId; + this.iamProfileName = builder.iamProfileName; this.url = builder.url; this.serviceVersion = StringUtils.isEmpty(builder.serviceVersion) ? metadataServiceVersion : builder.serviceVersion; this.tokenLifetime = builder.tokenLifetime == 0 ? instanceIdentityTokenLifetime : builder.tokenLifetime; @@ -199,7 +218,8 @@ public Builder newBuilder() { */ public static VpcInstanceAuthenticator fromConfiguration(Map config) { return new Builder().iamProfileCrn(config.get(PROPNAME_IAM_PROFILE_CRN)) - .iamProfileId(config.get(PROPNAME_IAM_PROFILE_ID)).url(config.get(PROPNAME_URL)) + .iamProfileId(config.get(PROPNAME_IAM_PROFILE_ID)).iamProfileName(config.get(PROPNAME_IAM_PROFILE_NAME)) + .url(config.get(PROPNAME_URL)) .serviceVersion(config.get(PROPNAME_VPC_IMS_VERSION)).build(); } @@ -208,10 +228,21 @@ public static VpcInstanceAuthenticator fromConfiguration(Map con */ @Override public void validate() { - // At most one of iamProfileCrn or iamProfileId may be specified. - if (StringUtils.isNotEmpty(getIamProfileCrn()) && StringUtils.isNotEmpty(getIamProfileId())) { + // At most one of iamProfileCrn or iamProfileId or iamProfileName may be specified. + int counter = 0; + if (StringUtils.isNotEmpty(getIamProfileCrn())) { + counter++; + } + if (StringUtils.isNotEmpty(getIamProfileId())) { + counter++; + } + if (StringUtils.isNotEmpty(getIamProfileName())) { + counter++; + } + + if (counter > 1) { throw new IllegalArgumentException( - String.format(ERRORMSG_ATMOST_ONE_PROP_ERROR, "iamProfileCrn", "iamProfileId")); + String.format(ERRORMSG_ATMOST_ONE_PROP_ERROR, "iamProfileCrn", "iamProfileId", "iamProfileName")); } if (!this.defaultServiceSupportedVersions.contains(this.serviceVersion)) { @@ -261,6 +292,22 @@ protected void setIamProfileId(String iamProfileId) { this.iamProfileId = iamProfileId; } + /** + * @return the iamProfileName configured on this Authenticator. + */ + public String getIamProfileName() { + return this.iamProfileName; + } + + /** + * Sets the iamProfileName property on this Authenticator. + * + * @param iamProfileName the value to set + */ + protected void setIamProfileName(String iamProfileName) { + this.iamProfileName = iamProfileName; + } + /** * @return the URL configured on this Authenticator. */ @@ -432,6 +479,9 @@ public IamToken retrieveIamAccessToken(String instanceIdentityToken) { if (!StringUtils.isEmpty(getIamProfileId())) { requestBody = String.format("{\"trusted_profile\": {\"id\": \"%s\"}}", getIamProfileId()); } + if (!StringUtils.isEmpty(getIamProfileName())) { + requestBody = String.format("{\"trusted_profile\": {\"name\": \"%s\"}}", getIamProfileName()); + } // If we created a request body above, then set it on the request now. if (!StringUtils.isEmpty(requestBody)) { diff --git a/src/test/java/com/ibm/cloud/sdk/core/test/security/VpcInstanceAuthenticatorTest.java b/src/test/java/com/ibm/cloud/sdk/core/test/security/VpcInstanceAuthenticatorTest.java index 0c26c20b..e8f9df69 100644 --- a/src/test/java/com/ibm/cloud/sdk/core/test/security/VpcInstanceAuthenticatorTest.java +++ b/src/test/java/com/ibm/cloud/sdk/core/test/security/VpcInstanceAuthenticatorTest.java @@ -54,6 +54,7 @@ public class VpcInstanceAuthenticatorTest extends BaseServiceUnitTest { private static final String mockIamProfileCrn = "crn:iam-profile:123"; private static final String mockIamProfileId = "iam-id-123"; + private static final String mockIamProfileName = "iam-profile-name-123"; private static final String operationPathCreateAccessToken = "/instance_identity/v1/token"; private static final String operationPathCreateIamToken = "/instance_identity/v1/iam_token"; @@ -105,6 +106,31 @@ public void testBuilderErrorProfileCrnAndId() { .build(); } + @Test(expectedExceptions = IllegalArgumentException.class) + public void testBuilderErrorProfileCrnAndName() { + new VpcInstanceAuthenticator.Builder() + .iamProfileCrn(mockIamProfileCrn) + .iamProfileName(mockIamProfileName) + .build(); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testBuilderErrorProfileIdAndName() { + new VpcInstanceAuthenticator.Builder() + .iamProfileId(mockIamProfileId) + .iamProfileName(mockIamProfileName) + .build(); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testBuilderErrorAllThreeProfiles() { + new VpcInstanceAuthenticator.Builder() + .iamProfileCrn(mockIamProfileCrn) + .iamProfileId(mockIamProfileId) + .iamProfileName(mockIamProfileName) + .build(); + } + @Test public void testBuilderDefaultConfig() { VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() @@ -113,6 +139,7 @@ public void testBuilderDefaultConfig() { assertNull(authenticator.getURL()); assertNull(authenticator.getIamProfileCrn()); assertNull(authenticator.getIamProfileId()); + assertNull(authenticator.getIamProfileName()); VpcInstanceAuthenticator auth2 = authenticator.newBuilder().build(); assertNotNull(auth2); @@ -142,6 +169,19 @@ public void testBuilderCorrectConfig2() { assertEquals(authenticator.getURL(), "url"); } + @Test + public void testBuilderCorrectConfig3() { + VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() + .iamProfileName(mockIamProfileName) + .url("url") + .build(); + assertEquals(authenticator.authenticationType(), Authenticator.AUTHTYPE_VPC); + assertNull(authenticator.getIamProfileCrn()); + assertNull(authenticator.getIamProfileId()); + assertEquals(authenticator.getIamProfileName(), mockIamProfileName); + assertEquals(authenticator.getURL(), "url"); + } + @Test public void testConfigCorrectConfig1() { Map props = new HashMap<>(); @@ -168,6 +208,20 @@ public void testConfigCorrectConfig2() { assertEquals(authenticator.getURL(), "url"); } + @Test + public void testConfigCorrectConfig3() { + Map props = new HashMap<>(); + props.put(Authenticator.PROPNAME_IAM_PROFILE_NAME, mockIamProfileName); + props.put(Authenticator.PROPNAME_URL, "url"); + + VpcInstanceAuthenticator authenticator = VpcInstanceAuthenticator.fromConfiguration(props); + assertEquals(authenticator.authenticationType(), Authenticator.AUTHTYPE_VPC); + assertNull(authenticator.getIamProfileCrn()); + assertNull(authenticator.getIamProfileId()); + assertEquals(authenticator.getIamProfileName(), mockIamProfileName); + assertEquals(authenticator.getURL(), "url"); + } + // // Tests involving the "retrieveInstanceIdentityToken()" method. // @@ -239,6 +293,29 @@ public void testRetrieveIamAccessTokenSuccess() throws Throwable { assertTrue(actualHeaders.get(HttpHeaders.USER_AGENT).startsWith("ibm-java-sdk-core/vpc-instance-authenticator")); } + @Test + public void testRetrieveIamAccessTokenWithProfileName() throws Throwable { + VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() + .iamProfileName(mockIamProfileName) + .url(url) + .build(); + + // Set mock server to send back a good response. + server.enqueue(jsonResponse(vpcIamAccessTokenResponse1)); + + String instanceIdentityToken = "vpc-token"; + IamToken iamToken = authenticator.retrieveIamAccessToken(instanceIdentityToken); + assertNotNull(iamToken); + assertEquals(iamToken.getAccessToken(), vpcIamAccessTokenResponse1.getAccessToken()); + + // Verify the request body contained the expected trusted_profile.name field. + RecordedRequest vpcIamTokenRequest = server.takeRequest(); + assertNotNull(vpcIamTokenRequest); + String requestBody = vpcIamTokenRequest.getBody().readUtf8(); + assertTrue(requestBody.contains("\"name\": \"" + mockIamProfileName + "\""), + "Expected request body to contain trusted_profile name, but was: " + requestBody); + } + @Test public void testRetrieveIamAccessTokenFailure() throws Throwable { VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() @@ -282,6 +359,31 @@ public void testRequestTokenSuccess() throws Throwable { assertEquals(iamToken.getAccessToken(), vpcIamAccessTokenResponse1.getAccessToken()); } + @Test + public void testRequestTokenSuccessWithProfileName() throws Throwable { + VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder() + .iamProfileName(mockIamProfileName) + .url(url) + .build(); + + // Set mock server to send back good responses. + server.enqueue(jsonResponse(vpcInstanceIdentityTokenResponse)); + server.enqueue(jsonResponse(vpcIamAccessTokenResponse1)); + + IamToken iamToken = authenticator.requestToken(); + assertNotNull(iamToken); + assertEquals(iamToken.getAccessToken(), vpcIamAccessTokenResponse1.getAccessToken()); + + // Consume the first request (instance identity token) and verify the second + // (IAM token) request body contained trusted_profile.name. + server.takeRequest(); // create_access_token + RecordedRequest iamTokenRequest = server.takeRequest(); // create_iam_token + assertNotNull(iamTokenRequest); + String requestBody = iamTokenRequest.getBody().readUtf8(); + assertTrue(requestBody.contains("\"name\": \"" + mockIamProfileName + "\""), + "Expected request body to contain trusted_profile name, but was: " + requestBody); + } + @Test public void testRequestTokenFailure1() throws Throwable { VpcInstanceAuthenticator authenticator = new VpcInstanceAuthenticator.Builder()