From dd024d4af0b598b2633d5efb5f6addeec9aea8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:25:40 +0200 Subject: [PATCH 1/4] feat(auth): add new IAM profile selector to VPC Instance Auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- .../vpc-instance-authenticator.ts | 21 ++++++++++++++- .../vpc-instance-token-manager.ts | 27 ++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) 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..786a5c41 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,8 @@ 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 +125,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 +148,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 +199,10 @@ export class VpcInstanceTokenManager extends JwtTokenManager { body = { trusted_profile: { crn: this.iamProfileCrn }, }; + } else if (this.iamProfileName) { + body = { + trusted_profile: { name: this.iamProfileName }, + }; } const parameters = { From 0a595ca8b5766213b086d8ca866a5d9533cf1eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:30:29 +0200 Subject: [PATCH 2/4] test: add test cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- test/unit/vpc-instance-authenticator.test.js | 55 +++++++++++++++++- test/unit/vpc-instance-token-manager.test.js | 60 +++++++++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/test/unit/vpc-instance-authenticator.test.js b/test/unit/vpc-instance-authenticator.test.js index f610f864..7bbbb19e 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,59 @@ 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..a80b6637 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,37 @@ 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 +143,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 +272,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' }); From e7edca336f198f980b6bd4d8c0c89ae4ee05bc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:33:58 +0200 Subject: [PATCH 3/4] docs: update documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- Authentication.md | 8 +++++--- etc/ibm-cloud-sdk-core.api.md | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) 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/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) From f0fd891f9f87493859b4ea5df3bcbe8d2468452a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:35:49 +0200 Subject: [PATCH 4/4] chore: add lint fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- auth/token-managers/vpc-instance-token-manager.ts | 4 +++- test/unit/vpc-instance-authenticator.test.js | 12 +++++++++--- test/unit/vpc-instance-token-manager.test.js | 12 +++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/auth/token-managers/vpc-instance-token-manager.ts b/auth/token-managers/vpc-instance-token-manager.ts index 786a5c41..b930317a 100644 --- a/auth/token-managers/vpc-instance-token-manager.ts +++ b/auth/token-managers/vpc-instance-token-manager.ts @@ -98,7 +98,9 @@ export class VpcInstanceTokenManager extends JwtTokenManager { super(options); if (!atMostOne(options.iamProfileId, options.iamProfileCrn, options.iamProfileName)) { - throw new Error('At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.'); + throw new Error( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); } this.url = options.url || DEFAULT_IMS_ENDPOINT; diff --git a/test/unit/vpc-instance-authenticator.test.js b/test/unit/vpc-instance-authenticator.test.js index 7bbbb19e..7cdb6d5f 100644 --- a/test/unit/vpc-instance-authenticator.test.js +++ b/test/unit/vpc-instance-authenticator.test.js @@ -57,7 +57,9 @@ describe('VPC Instance Authenticator', () => { iamProfileCrn: 'crn', iamProfileId: 'id', }); - }).toThrow('At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` 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', () => { @@ -66,7 +68,9 @@ describe('VPC Instance Authenticator', () => { iamProfileCrn: 'crn', iamProfileName: IAM_PROFILE_NAME, }); - }).toThrow('At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.'); + }).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); }); it('should throw an error when both iamProfileId and iamProfileName are provided', () => { @@ -75,7 +79,9 @@ describe('VPC Instance Authenticator', () => { iamProfileId: 'id', iamProfileName: IAM_PROFILE_NAME, }); - }).toThrow('At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.'); + }).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); }); it('should store iamProfileName when provided in config', () => { diff --git a/test/unit/vpc-instance-token-manager.test.js b/test/unit/vpc-instance-token-manager.test.js index a80b6637..0a84129f 100644 --- a/test/unit/vpc-instance-token-manager.test.js +++ b/test/unit/vpc-instance-token-manager.test.js @@ -71,7 +71,9 @@ describe('VPC Instance Token Manager', () => { iamProfileCrn: IAM_PROFILE_CRN, iamProfileId: IAM_PROFILE_ID, }) - ).toThrow('At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` 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', () => { @@ -81,7 +83,9 @@ describe('VPC Instance Token Manager', () => { iamProfileCrn: IAM_PROFILE_CRN, iamProfileName: IAM_PROFILE_NAME, }) - ).toThrow('At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.'); + ).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); }); it('should throw an error when both `iamProfileId` and `iamProfileName` are provided', () => { @@ -91,7 +95,9 @@ describe('VPC Instance Token Manager', () => { iamProfileId: IAM_PROFILE_ID, iamProfileName: IAM_PROFILE_NAME, }) - ).toThrow('At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.'); + ).toThrow( + 'At most one of `iamProfileId`, `iamProfileCrn` or `iamProfileName` may be specified.' + ); }); it('should set given profile name', () => {