Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -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.


Expand Down
36 changes: 32 additions & 4 deletions ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
)
Expand All @@ -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(
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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.

Expand Down
1 change: 1 addition & 0 deletions ibm_cloud_sdk_core/get_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
)
Expand Down
29 changes: 25 additions & 4 deletions ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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',
Expand Down Expand Up @@ -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.

Expand Down
71 changes: 67 additions & 4 deletions test/test_vpc_instance_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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'


Expand All @@ -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
Expand All @@ -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():
Expand Down
51 changes: 51 additions & 0 deletions test/test_vpc_instance_token_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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


Expand All @@ -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)
Expand All @@ -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():
Expand Down Expand Up @@ -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()
Expand Down
Loading