diff --git a/Authentication.md b/Authentication.md index 40cfc2f6..745f836b 100644 --- a/Authentication.md +++ b/Authentication.md @@ -488,6 +488,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 when obtaining the IAM access token. + - url: (optional) The VPC Instance Metadata Service's base URL. 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`. @@ -500,12 +502,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`, `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 from `iamProfileCrn`, `iamProfileId` or `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/auth/authenticators/vpc-instance-authenticator.ts b/auth/authenticators/vpc-instance-authenticator.ts index ca5d8f5f..cd06b2ff 100644 --- a/auth/authenticators/vpc-instance-authenticator.ts +++ b/auth/authenticators/vpc-instance-authenticator.ts @@ -24,6 +24,8 @@ export interface Options extends BaseOptions { iamProfileCrn?: string; /** The ID of the linked trusted IAM profile to be used when obtaining the IAM access token */ iamProfileId?: string; + /** The name of the linked trusted IAM profile to be used when obtaining the IAM access token */ + iamProfileName?: string; /** The version of the Instance Metadata Service to be used obtaining tokens */ serviceVersion?: string; /** The lifetime of the Instance Identity Token */ @@ -47,6 +49,8 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { private iamProfileId: string; + private iamProfileName: string; + private serviceVersion: string; private tokenLifetime: number; @@ -59,9 +63,10 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { * - url: (optional) the endpoint URL for the VPC Instance Metadata Service (default value: "http://169.254.169.254") * - iamProfileCrn: (optional) the CRN of the linked IAM trusted profile to be used to obtain the IAM access token * - iamProfileId: (optional) the ID of the linked IAM trusted profile to be used to obtain the IAM access token + * - iamProfileName: (optional) the name of the linked IAM trusted profile to be used to obtain the IAM access token * * @remarks - * At most one of "iamProfileCrn" or "iamProfileId" may be specified. If neither one is specified, + * At most one of "iamProfileCrn", "iamProfileId" or "iamProfileName" may be specified. If neither one is specified, * then the default IAM profile defined for the compute resource will be used. */ constructor(options: Options) { @@ -76,6 +81,9 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { if (options.iamProfileId) { this.iamProfileId = options.iamProfileId; } + if (options.iamProfileName) { + this.iamProfileName = options.iamProfileName; + } if (options.serviceVersion) { this.serviceVersion = options.serviceVersion; } @@ -111,6 +119,17 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { this.tokenManager.setIamProfileId(iamProfileId); } + /** + * Sets the "iamProfileName" value to be used when obtaining an IAM access token + * @param iamProfileName - the name of the linked IAM trusted profile to use when obtaining an IAM access token + */ + public setIamProfileName(iamProfileName: string): void { + this.iamProfileName = iamProfileName; + + // update properties in token manager + this.tokenManager.setIamProfileName(iamProfileName); + } + public setServiceVersion(serviceVersion: string): void { this.serviceVersion = serviceVersion; diff --git a/auth/token-managers/vpc-instance-token-manager.ts b/auth/token-managers/vpc-instance-token-manager.ts index 656847f3..b930317a 100644 --- a/auth/token-managers/vpc-instance-token-manager.ts +++ b/auth/token-managers/vpc-instance-token-manager.ts @@ -36,6 +36,8 @@ interface Options extends JwtTokenManagerOptions { iamProfileCrn?: string; /** The ID of the linked trusted IAM profile to be used when obtaining the IAM access token */ iamProfileId?: string; + /** The name of the linked trusted IAM profile to be used when obtaining the IAM access token */ + iamProfileName?: string; /** The version of the Instance Metadata Service to be used obtaining tokens */ serviceVersion?: string; /** The lifetime of the Instance Identity Token */ @@ -54,6 +56,7 @@ interface VpcTokenResponse { interface TrustedProfile { id?: string; crn?: string; + name?: string; } interface CreateIamTokenBody { @@ -68,6 +71,8 @@ export class VpcInstanceTokenManager extends JwtTokenManager { private iamProfileId: string; + private iamProfileName: string; + private serviceVersion: string; private tokenLifetime: number; @@ -80,9 +85,10 @@ export class VpcInstanceTokenManager extends JwtTokenManager { * - url: (optional) the endpoint URL for the VPC Instance Metadata Service (default value: "http://169.254.169.254") * - iamProfileCrn: (optional) the CRN of the linked IAM trusted profile to be used to obtain the IAM access token * - iamProfileId: (optional) the ID of the linked IAM trusted profile to be used to obtain the IAM access token + * - iamProfileName: (optional) the name of the linked IAM trusted profile to be used to obtain the IAM access token * * @remarks - * At most one of "iamProfileCrn" or "iamProfileId" may be specified. If neither one is specified, + * At most one of "iamProfileCrn", "iamProfileId" or "iamProfileName" may be specified. If neither one is specified, * then the default IAM profile defined for the compute resource will be used. */ constructor(options: Options) { @@ -91,8 +97,10 @@ export class VpcInstanceTokenManager extends JwtTokenManager { super(options); - if (!atMostOne(options.iamProfileId, options.iamProfileCrn)) { - throw new Error('At most one of `iamProfileId` or `iamProfileCrn` may be specified.'); + if (!atMostOne(options.iamProfileId, options.iamProfileCrn, options.iamProfileName)) { + throw new Error( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); } this.url = options.url || DEFAULT_IMS_ENDPOINT; @@ -119,6 +127,9 @@ export class VpcInstanceTokenManager extends JwtTokenManager { if (options.iamProfileId) { this.iamProfileId = options.iamProfileId; } + if (options.iamProfileName) { + this.iamProfileName = options.iamProfileName; + } this.userAgent = buildUserAgent('vpc-instance-authenticator'); } @@ -139,6 +150,14 @@ export class VpcInstanceTokenManager extends JwtTokenManager { this.iamProfileId = iamProfileId; } + /** + * Sets the name of the IAm trusted profile to use when fetching access token from the IAM token server. + * @param iamProfileName - the name of the IAM trusted profile + */ + public setIamProfileName(iamProfileName: string): void { + this.iamProfileName = iamProfileName; + } + public setServiceVersion(serviceVersion: string): void { if (!metadataServiceSupportedVersions.includes(serviceVersion)) { throw new Error( @@ -182,6 +201,10 @@ export class VpcInstanceTokenManager extends JwtTokenManager { body = { trusted_profile: { crn: this.iamProfileCrn }, }; + } else if (this.iamProfileName) { + body = { + trusted_profile: { name: this.iamProfileName }, + }; } const parameters = { diff --git a/etc/ibm-cloud-sdk-core.api.md b/etc/ibm-cloud-sdk-core.api.md index 995bcf7a..ffa98e2e 100644 --- a/etc/ibm-cloud-sdk-core.api.md +++ b/etc/ibm-cloud-sdk-core.api.md @@ -509,6 +509,7 @@ export class VpcInstanceAuthenticator extends TokenRequestBasedAuthenticator { authenticationType(): string; setIamProfileCrn(iamProfileCrn: string): void; setIamProfileId(iamProfileId: string): void; + setIamProfileName(iamProfileName: string): void; // (undocumented) setServiceVersion(serviceVersion: string): void; // (undocumented) @@ -530,6 +531,7 @@ export class VpcInstanceTokenManager extends JwtTokenManager { protected requestToken(): Promise; setIamProfileCrn(iamProfileCrn: string): void; setIamProfileId(iamProfileId: string): void; + setIamProfileName(iamProfileName: string): void; // (undocumented) setServiceVersion(serviceVersion: string): void; // (undocumented) diff --git a/test/unit/vpc-instance-authenticator.test.js b/test/unit/vpc-instance-authenticator.test.js index f610f864..7cdb6d5f 100644 --- a/test/unit/vpc-instance-authenticator.test.js +++ b/test/unit/vpc-instance-authenticator.test.js @@ -23,6 +23,7 @@ const SERVICE_VERSION_2025 = '2025-08-26'; const DEFAULT_TOKEN_LIFETIME = 300; const CUSTOM_TOKEN_LIFETIME = 600; const INVALID_SERVICE_VERSION_ERROR = `Invalid serviceVersion. Must be one of: ${SERVICE_VERSION_2022}, ${SERVICE_VERSION_2025}`; +const IAM_PROFILE_NAME = 'some-name'; // mock the `getToken` method in the token manager - dont make any rest calls const fakeToken = 'iam-acess-token'; @@ -56,7 +57,65 @@ describe('VPC Instance Authenticator', () => { iamProfileCrn: 'crn', iamProfileId: 'id', }); - }).toThrow('At most one of `iamProfileId` or `iamProfileCrn` may be specified.'); + }).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); + }); + + it('should throw an error when both iamProfileCrn and iamProfileName are provided', () => { + expect(() => { + const unused = new VpcInstanceAuthenticator({ + iamProfileCrn: 'crn', + iamProfileName: IAM_PROFILE_NAME, + }); + }).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); + }); + + it('should throw an error when both iamProfileId and iamProfileName are provided', () => { + expect(() => { + const unused = new VpcInstanceAuthenticator({ + iamProfileId: 'id', + iamProfileName: IAM_PROFILE_NAME, + }); + }).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); + }); + + it('should store iamProfileName when provided in config', () => { + const authenticator = new VpcInstanceAuthenticator({ iamProfileName: IAM_PROFILE_NAME }); + + expect(authenticator.iamProfileName).toBe(IAM_PROFILE_NAME); + expect(authenticator.iamProfileCrn).toBeUndefined(); + expect(authenticator.iamProfileId).toBeUndefined(); + + // should also be set on the token manager + expect(authenticator.tokenManager.iamProfileName).toBe(IAM_PROFILE_NAME); + }); + + it('should re-set iamProfileName using the setter', () => { + const authenticator = new VpcInstanceAuthenticator({ iamProfileName: 'initial-name' }); + expect(authenticator.iamProfileName).toBe('initial-name'); + + authenticator.setIamProfileName(IAM_PROFILE_NAME); + expect(authenticator.iamProfileName).toBe(IAM_PROFILE_NAME); + + // also, verify that the underlying token manager has been updated + expect(authenticator.tokenManager.iamProfileName).toBe(IAM_PROFILE_NAME); + }); + + it('should set iamProfileName using the setter when not declared in constructor', () => { + const authenticator = new VpcInstanceAuthenticator(); + expect(authenticator.iamProfileName).toBeUndefined(); + expect(authenticator.tokenManager.iamProfileName).toBeUndefined(); + + authenticator.setIamProfileName(IAM_PROFILE_NAME); + expect(authenticator.iamProfileName).toBe(IAM_PROFILE_NAME); + + // also, verify that the underlying token manager has been updated + expect(authenticator.tokenManager.iamProfileName).toBe(IAM_PROFILE_NAME); }); it('should re-set iamProfileCrn using the setter', () => { diff --git a/test/unit/vpc-instance-token-manager.test.js b/test/unit/vpc-instance-token-manager.test.js index 1c9bf42f..0a84129f 100644 --- a/test/unit/vpc-instance-token-manager.test.js +++ b/test/unit/vpc-instance-token-manager.test.js @@ -36,6 +36,7 @@ const debugLogSpy = jest.spyOn(logger, 'debug').mockImplementation(() => {}); const IAM_PROFILE_CRN = 'some-crn'; const IAM_PROFILE_ID = 'some-id'; +const IAM_PROFILE_NAME = 'some-name'; const EXPIRATION_WINDOW = 10; const SERVICE_VERSION_2022 = '2022-03-01'; const SERVICE_VERSION_2025 = '2025-08-26'; @@ -70,7 +71,43 @@ describe('VPC Instance Token Manager', () => { iamProfileCrn: IAM_PROFILE_CRN, iamProfileId: IAM_PROFILE_ID, }) - ).toThrow('At most one of `iamProfileId` or `iamProfileCrn` may be specified.'); + ).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); + }); + + it('should throw an error when both `iamProfileCrn` and `iamProfileName` are provided', () => { + expect( + () => + new VpcInstanceTokenManager({ + iamProfileCrn: IAM_PROFILE_CRN, + iamProfileName: IAM_PROFILE_NAME, + }) + ).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); + }); + + it('should throw an error when both `iamProfileId` and `iamProfileName` are provided', () => { + expect( + () => + new VpcInstanceTokenManager({ + iamProfileId: IAM_PROFILE_ID, + iamProfileName: IAM_PROFILE_NAME, + }) + ).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); + }); + + it('should set given profile name', () => { + const instance = new VpcInstanceTokenManager({ + iamProfileName: IAM_PROFILE_NAME, + }); + + expect(instance.iamProfileName).toBe(IAM_PROFILE_NAME); + expect(instance.iamProfileCrn).toBeUndefined(); + expect(instance.iamProfileId).toBeUndefined(); }); it('should use default url if none is given', () => { @@ -112,6 +149,22 @@ describe('VPC Instance Token Manager', () => { instance.setIamProfileId(IAM_PROFILE_ID); expect(instance.iamProfileId).toBe(IAM_PROFILE_ID); }); + + it('should set iamProfileName with the setter', () => { + const instance = new VpcInstanceTokenManager(); + expect(instance.iamProfileName).toBeUndefined(); + + instance.setIamProfileName(IAM_PROFILE_NAME); + expect(instance.iamProfileName).toBe(IAM_PROFILE_NAME); + }); + + it('should update iamProfileName with the setter when already set in constructor', () => { + const instance = new VpcInstanceTokenManager({ iamProfileName: 'initial-name' }); + expect(instance.iamProfileName).toBe('initial-name'); + + instance.setIamProfileName(IAM_PROFILE_NAME); + expect(instance.iamProfileName).toBe(IAM_PROFILE_NAME); + }); }); describe('getInstanceIdentityToken', () => { @@ -225,6 +278,17 @@ describe('VPC Instance Token Manager', () => { expect(requestOptions.body.trusted_profile.id).toBe('some-id'); }); + it('should set trusted profile to iam profile name, if set', async () => { + const instance = new VpcInstanceTokenManager({ iamProfileName: IAM_PROFILE_NAME }); + await instance.requestToken(); + + const requestOptions = getRequestOptions(sendRequestMock, 1); + expect(requestOptions).toBeDefined(); + expect(requestOptions.body).toBeDefined(); + expect(requestOptions.body.trusted_profile).toBeDefined(); + expect(requestOptions.body.trusted_profile.name).toBe(IAM_PROFILE_NAME); + }); + it('should set User-Agent header', async () => { const instance = new VpcInstanceTokenManager({ iamProfileId: 'some-id' });