diff --git a/Authentication.md b/Authentication.md index 7a6d179..898c37d 100644 --- a/Authentication.md +++ b/Authentication.md @@ -440,7 +440,9 @@ The IAM access token is added to each outbound request in the `Authorization` he - iam_profile_id: (optional) the id 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. +- iam_profile_name: (optional) the name of the linked trusted IAM profile to be used as the identity of the compute resource. + +- 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`. @@ -452,12 +454,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 `iam_profile_crn` or `iam_profile_id` may be specified. The specified value must map +1. At most one of `iam_profile_crn`, `iam_profile_id` or `iam_profile_name` 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 `iam_profile_crn` and `iam_profile_id` are specified, then an error occurs. +2. If more than on from `iam_profile_crn`, `iam_profile_id` and `iam_profile_name` are specified, then an error occurs. -3. If neither `iam_profile_crn` nor `iam_profile_id` are specified, then the default trusted profile linked to the compute resource will be used to perform the IAM token exchange. +3. If neither `iam_profile_crn`, `iam_profile_id` nor `iam_profile_name` 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/ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py b/ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py index c2b7a13..6f29d21 100644 --- a/ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py +++ b/ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py @@ -61,6 +61,7 @@ def __init__( self, iam_profile_crn: Optional[str] = None, iam_profile_id: Optional[str] = None, + iam_profile_name: Optional[str] = None, url: Optional[str] = None, *, token_lifetime: Optional[int] = None, @@ -79,6 +80,7 @@ def __init__( url=url, iam_profile_crn=iam_profile_crn, iam_profile_id=iam_profile_id, + iam_profile_name=iam_profile_name, token_lifetime=token_lifetime, service_version=service_version, ) @@ -92,8 +94,19 @@ def authentication_type(self) -> str: def validate(self) -> None: super().validate() - if self.token_manager.iam_profile_crn and self.token_manager.iam_profile_id: - raise ValueError('At most one of "iam_profile_id" or "iam_profile_crn" may be specified.') + counter = 0 + + if self.token_manager.iam_profile_crn: + counter += 1 + if self.token_manager.iam_profile_id: + counter += 1 + if self.token_manager.iam_profile_name: + counter += 1 + + if counter > 1: + raise ValueError( + 'At most one of "iam_profile_id", "iam_profile_crn" or "iam_profile_name" may be specified.' + ) if self.token_manager.service_version not in self.VPC_AUTH_METADATA_SERVICE_SUPPORTED_VERSIONS: raise ValueError( @@ -125,7 +138,7 @@ def set_iam_profile_crn(self, iam_profile_crn: str) -> None: the identity of the compute resource. Raises: - ValueError: At most one of iam_profile_crn or iam_profile_id may be specified. + ValueError: At most one of iam_profile_crn, iam_profile_id or iam_profile_name may be specified. If neither one is specified, then the default IAM profile defined for the compute resource will be used. """ @@ -140,13 +153,28 @@ def set_iam_profile_id(self, iam_profile_id: str) -> None: the IAM access token Raises: - ValueError: At most one of iam_profile_crn or iam_profile_id may be specified. + ValueError: At most one of iam_profile_crn, iam_profile_id or iam_profile_name may be specified. If neither one is specified, then the default IAM profile defined for the compute resource will be used. """ self.token_manager.set_iam_profile_id(iam_profile_id) self.validate() + def set_iam_profile_name(self, iam_profile_name: str) -> None: + """Sets the ID of the IAM profile. + + Args: + iam_profile_name (str): name of the linked trusted IAM profile to be used when obtaining + the IAM access token + + Raises: + ValueError: At most one of iam_profile_crn, iam_profile_id or iam_profile_name may be specified. + If neither one is specified, then the default IAM profile defined + for the compute resource will be used. + """ + self.token_manager.set_iam_profile_name(iam_profile_name) + self.validate() + def set_token_lifetime(self, token_lifetime: int) -> None: """Sets the token lifetime. diff --git a/ibm_cloud_sdk_core/get_authenticator.py b/ibm_cloud_sdk_core/get_authenticator.py index f57b352..e18fa97 100644 --- a/ibm_cloud_sdk_core/get_authenticator.py +++ b/ibm_cloud_sdk_core/get_authenticator.py @@ -126,6 +126,7 @@ def __construct_authenticator(config: dict) -> Authenticator: authenticator = VPCInstanceAuthenticator( iam_profile_crn=config.get('IAM_PROFILE_CRN'), iam_profile_id=config.get('IAM_PROFILE_ID'), + iam_profile_name=config.get('IAM_PROFILE_NAME'), url=config.get('AUTH_URL'), service_version=config.get('VPC_IMS_VERSION'), ) diff --git a/ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py b/ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py index 0ce4656..3f15a63 100644 --- a/ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py +++ b/ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py @@ -37,18 +37,26 @@ class VPCInstanceTokenManager(JWTTokenManager): Keyword Arguments: iam_profile_crn (str, optional): The CRN of the linked trusted IAM profile to be used as the identity of the compute resource. - At most one of iam_profile_crn or iam_profile_id may be specified. If neither one is specified, - then the default IAM profile defined for the compute resource will be used. Defaults to None. + At most one of iam_profile_crn or iam_profile_id or iam_profile_name may be specified. + If neither one is specified, then the default IAM profile defined for the compute resource will be used. + Defaults to None. iam_profile_id (str, optional): The ID of the linked trusted IAM profile to be used when obtaining the IAM access token. - At most one of iam_profile_crn or iam_profile_id may be specified. If neither one is specified, - then the default IAM profile defined for the compute resource will be used. Defaults to None. + At most one of iam_profile_crn or iam_profile_id or iam_profile_name may be specified. + If neither one is specified, then the default IAM profile defined for the compute resource will be used. + Defaults to None. + iam_profile_name (str, optional): + The name of the linked trusted IAM profile to be used as the identity of the compute resource. + At most one of iam_profile_crn, iam_profile_id, or iam_profile_name may be specified. + If neither one is specified, then the default IAM profile defined for the compute resource will be used. + Defaults to None. url (str, optional): The VPC Instance Metadata Service's base endpoint URL. Defaults to 'http://169.254.169.254'. Attributes: iam_profile_crn (str, optional): The CRN of the linked trusted IAM profile. iam_profile_id (str, optional): The ID of the linked trusted IAM profile. + iam_profile_id (str, optional): The name of the linked trusted IAM profile. url (str, optional): The VPC Instance Metadata Service's base endpoint URL. """ @@ -68,6 +76,7 @@ def __init__( self, iam_profile_crn: Optional[str] = None, iam_profile_id: Optional[str] = None, + iam_profile_name: Optional[str] = None, url: Optional[str] = None, *, token_lifetime: Optional[int] = None, @@ -87,6 +96,7 @@ def __init__( self.iam_profile_crn = iam_profile_crn self.iam_profile_id = iam_profile_id + self.iam_profile_name = iam_profile_name self.token_lifetime = token_lifetime self.service_version = service_version @@ -112,6 +122,8 @@ def request_token(self) -> dict: request_payload = {'trusted_profile': {'crn': self.iam_profile_crn}} if self.iam_profile_id: request_payload = {'trusted_profile': {'id': self.iam_profile_id}} + if self.iam_profile_name: + request_payload = {'trusted_profile': {'name': self.iam_profile_name}} headers = { 'Content-Type': 'application/json', @@ -151,6 +163,15 @@ def set_iam_profile_id(self, iam_profile_id: str) -> None: """ self.iam_profile_id = iam_profile_id + def set_iam_profile_name(self, iam_profile_name: str) -> None: + """Sets the name of the IAM profile. + + Args: + iam_profile_name (str): name of the linked trusted IAM profile to be used when obtaining + the IAM access token + """ + self.iam_profile_name = iam_profile_name + def set_token_lifetime(self, token_lifetime: int) -> None: """Sets the lifetime of token. diff --git a/test/test_vpc_instance_authenticator.py b/test/test_vpc_instance_authenticator.py index c280888..24f27eb 100644 --- a/test/test_vpc_instance_authenticator.py +++ b/test/test_vpc_instance_authenticator.py @@ -10,6 +10,9 @@ TEST_IAM_PROFILE_CRN = 'crn:iam-profile:123' TEST_IAM_PROFILE_ID = 'iam-id-123' +TEST_IAM_PROFILE_NAME = 'iam-profile-name-1' + +EXPECTED_ERROR = 'At most one of "iam_profile_id", "iam_profile_crn" or "iam_profile_name" may be specified.' def test_constructor(): @@ -18,6 +21,17 @@ def test_constructor(): assert authenticator.authentication_type() == Authenticator.AUTHTYPE_VPC assert authenticator.token_manager.iam_profile_crn is None assert authenticator.token_manager.iam_profile_id == TEST_IAM_PROFILE_ID + assert authenticator.token_manager.iam_profile_name is None + assert authenticator.token_manager.url == 'someurl.com' + + +def test_constructor_with_iam_profile_name(): + authenticator = VPCInstanceAuthenticator(iam_profile_name=TEST_IAM_PROFILE_NAME, url='someurl.com') + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_VPC + assert authenticator.token_manager.iam_profile_crn is None + assert authenticator.token_manager.iam_profile_id is None + assert authenticator.token_manager.iam_profile_name == TEST_IAM_PROFILE_NAME assert authenticator.token_manager.url == 'someurl.com' @@ -29,11 +43,10 @@ def test_setters(): assert authenticator.token_manager.iam_profile_id == TEST_IAM_PROFILE_ID assert authenticator.token_manager.url == 'someurl.com' - # Set the IAM profile CRN to trigger a validation which will fail, - # because at most one of iam_profile_crn or iam_profile_id may be specified. + # Setting CRN while ID is still set must raise a validation error. with pytest.raises(ValueError) as err: authenticator.set_iam_profile_crn(TEST_IAM_PROFILE_CRN) - assert str(err.value) == 'At most one of "iam_profile_id" or "iam_profile_crn" may be specified.' + assert str(err.value) == EXPECTED_ERROR authenticator.set_iam_profile_id(None) assert authenticator.token_manager.iam_profile_id is None @@ -42,13 +55,63 @@ def test_setters(): assert authenticator.token_manager.iam_profile_crn == TEST_IAM_PROFILE_CRN +def test_setter_iam_profile_name(): + authenticator = VPCInstanceAuthenticator() + assert authenticator.token_manager.iam_profile_name is None + + authenticator.set_iam_profile_name(TEST_IAM_PROFILE_NAME) + assert authenticator.token_manager.iam_profile_name == TEST_IAM_PROFILE_NAME + + # Setting name while CRN is still set must raise a validation error. + authenticator2 = VPCInstanceAuthenticator(iam_profile_crn=TEST_IAM_PROFILE_CRN) + with pytest.raises(ValueError) as err: + authenticator2.set_iam_profile_name(TEST_IAM_PROFILE_NAME) + assert str(err.value) == EXPECTED_ERROR + + # Setting name while ID is still set must raise a validation error. + authenticator3 = VPCInstanceAuthenticator(iam_profile_id=TEST_IAM_PROFILE_ID) + with pytest.raises(ValueError) as err: + authenticator3.set_iam_profile_name(TEST_IAM_PROFILE_NAME) + assert str(err.value) == EXPECTED_ERROR + + # Clearing name is always allowed. + authenticator.set_iam_profile_name(None) + assert authenticator.token_manager.iam_profile_name is None + + def test_constructor_validate_failed(): + # CRN + ID + with pytest.raises(ValueError) as err: + VPCInstanceAuthenticator( + iam_profile_crn=TEST_IAM_PROFILE_CRN, + iam_profile_id=TEST_IAM_PROFILE_ID, + ) + assert str(err.value) == EXPECTED_ERROR + + # CRN + NAME + with pytest.raises(ValueError) as err: + VPCInstanceAuthenticator( + iam_profile_crn=TEST_IAM_PROFILE_CRN, + iam_profile_name=TEST_IAM_PROFILE_NAME, + ) + assert str(err.value) == EXPECTED_ERROR + + # ID + NAME + with pytest.raises(ValueError) as err: + VPCInstanceAuthenticator( + iam_profile_id=TEST_IAM_PROFILE_ID, + iam_profile_name=TEST_IAM_PROFILE_NAME, + ) + assert str(err.value) == EXPECTED_ERROR + + # CRN + ID + NAME with pytest.raises(ValueError) as err: VPCInstanceAuthenticator( iam_profile_crn=TEST_IAM_PROFILE_CRN, iam_profile_id=TEST_IAM_PROFILE_ID, + iam_profile_name=TEST_IAM_PROFILE_NAME, ) - assert str(err.value) == 'At most one of "iam_profile_id" or "iam_profile_crn" may be specified.' + assert str(err.value) == EXPECTED_ERROR def test_constructor_with_unsupported_service_version(): diff --git a/test/test_vpc_instance_token_manager.py b/test/test_vpc_instance_token_manager.py index d07ac68..9fafdc9 100644 --- a/test/test_vpc_instance_token_manager.py +++ b/test/test_vpc_instance_token_manager.py @@ -35,6 +35,7 @@ TEST_IAM_TOKEN = 'iam-abc123' TEST_IAM_PROFILE_CRN = 'crn:iam-profile:123' TEST_IAM_PROFILE_ID = 'iam-id-123' +TEST_IAM_PROFILE_NAME = 'iam-profile-name-1' EXPIRATION_WINDOW = 10 @@ -49,6 +50,18 @@ def test_constructor(): assert token_manager.iam_profile_crn is TEST_IAM_PROFILE_CRN assert token_manager.iam_profile_id is None + assert token_manager.iam_profile_name is None + assert token_manager.access_token is None + + +def test_constructor_with_iam_profile_name(): + token_manager = VPCInstanceTokenManager( + iam_profile_name=TEST_IAM_PROFILE_NAME, + ) + + assert token_manager.iam_profile_crn is None + assert token_manager.iam_profile_id is None + assert token_manager.iam_profile_name == TEST_IAM_PROFILE_NAME assert token_manager.access_token is None @@ -59,6 +72,7 @@ def test_setters(): assert token_manager.iam_profile_crn is TEST_IAM_PROFILE_CRN assert token_manager.iam_profile_id is None + assert token_manager.iam_profile_name is None assert token_manager.access_token is None token_manager.set_iam_profile_crn(None) @@ -67,6 +81,13 @@ def test_setters(): token_manager.set_iam_profile_id(TEST_IAM_PROFILE_ID) assert token_manager.iam_profile_id == TEST_IAM_PROFILE_ID + token_manager.set_iam_profile_id(None) + token_manager.set_iam_profile_name(TEST_IAM_PROFILE_NAME) + assert token_manager.iam_profile_name == TEST_IAM_PROFILE_NAME + + token_manager.set_iam_profile_name(None) + assert token_manager.iam_profile_name is None + @responses.activate def test_retrieve_instance_identity_token(): @@ -249,6 +270,36 @@ def mock_retrieve_instance_identity_token(): assert responses.calls[0].request.params['version'] == '2022-03-01' +@responses.activate +def test_request_token_with_name(): + token_manager = VPCInstanceTokenManager( + iam_profile_name=TEST_IAM_PROFILE_NAME, + ) + + # Mock the retrieve instance identity token method. + def mock_retrieve_instance_identity_token(): + return TEST_TOKEN + + token_manager.retrieve_instance_identity_token = mock_retrieve_instance_identity_token + + response = { + 'access_token': TEST_IAM_TOKEN, + } + + responses.add( + responses.POST, 'http://169.254.169.254/instance_identity/v1/iam_token', body=json.dumps(response), status=200 + ) + + response = token_manager.request_token() + assert len(responses.calls) == 1 + assert responses.calls[0].request.headers['Content-Type'] == 'application/json' + assert responses.calls[0].request.headers['Accept'] == 'application/json' + assert responses.calls[0].request.headers['Metadata-Flavor'] == 'ibm' + assert responses.calls[0].request.headers['Authorization'] == 'Bearer ' + TEST_TOKEN + assert responses.calls[0].request.body == '{"trusted_profile": {"name": "iam-profile-name-1"}}' + assert responses.calls[0].request.params['version'] == '2022-03-01' + + @responses.activate def test_request_token(): token_manager = VPCInstanceTokenManager()