From bd627b65f7c53713fc5e2b0d6eea1e90ffb941f8 Mon Sep 17 00:00:00 2001 From: gruebel Date: Fri, 18 Oct 2019 17:29:15 +0200 Subject: [PATCH 1/5] Add iam.create_open_id_connect_provider --- IMPLEMENTATION_COVERAGE.md | 4 +- moto/iam/exceptions.py | 24 +++++++ moto/iam/models.py | 87 ++++++++++++++++++++++++- moto/iam/responses.py | 19 ++++++ tests/test_iam/test_iam.py | 130 +++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 4 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 4fd909d67..3bea66408 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3163,7 +3163,7 @@ - [ ] describe_events ## iam -55% implemented +56% implemented - [ ] add_client_id_to_open_id_connect_provider - [X] add_role_to_instance_profile - [X] add_user_to_group @@ -3176,7 +3176,7 @@ - [X] create_group - [X] create_instance_profile - [X] create_login_profile -- [ ] create_open_id_connect_provider +- [X] create_open_id_connect_provider - [X] create_policy - [X] create_policy_version - [X] create_role diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py index ac08e0d88..0511fa144 100644 --- a/moto/iam/exceptions.py +++ b/moto/iam/exceptions.py @@ -93,3 +93,27 @@ class TooManyTags(RESTError): super(TooManyTags, self).__init__( 'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy " "constraint: Member must have length less than or equal to 50.".format(tags, param)) + + +class EntityAlreadyExists(RESTError): + code = 409 + + def __init__(self): + super(EntityAlreadyExists, self).__init__( + 'EntityAlreadyExists', "Unknown") + + +class ValidationError(RESTError): + code = 400 + + def __init__(self, message): + super(ValidationError, self).__init__( + 'ValidationError', message) + + +class InvalidInput(RESTError): + code = 400 + + def __init__(self, message): + super(InvalidInput, self).__init__( + 'InvalidInput', message) diff --git a/moto/iam/models.py b/moto/iam/models.py index 506f2a942..2a7d185a5 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -7,6 +7,7 @@ import re from cryptography import x509 from cryptography.hazmat.backends import default_backend +from six.moves.urllib.parse import urlparse from moto.core.exceptions import RESTError from moto.core import BaseBackend, BaseModel @@ -14,8 +15,9 @@ from moto.core.utils import iso_8601_datetime_without_milliseconds, iso_8601_dat from moto.iam.policy_validation import IAMPolicyDocumentValidator from .aws_managed_policies import aws_managed_policies_data -from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, IAMLimitExceededException, \ - MalformedCertificate, DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig +from .exceptions import (IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, IAMLimitExceededException, + MalformedCertificate, DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig, + EntityAlreadyExists, ValidationError, InvalidInput) from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id ACCOUNT_ID = 123456789012 @@ -93,6 +95,77 @@ class SAMLProvider(BaseModel): return "arn:aws:iam::{0}:saml-provider/{1}".format(ACCOUNT_ID, self.name) +class OpenIDConnectProvider(BaseModel): + def __init__(self, url, thumbprint_list, client_id_list=None): + self._errors = [] + self._validate(url, thumbprint_list, client_id_list) + + parsed_url = urlparse(url) + self.url = parsed_url.netloc + parsed_url.path + self.thumbprint_list = thumbprint_list + self.client_id_list = client_id_list + + @property + def arn(self): + return 'arn:aws:iam::{0}:oidc-provider/{1}'.format(ACCOUNT_ID, self.url) + + def _validate(self, url, thumbprint_list, client_id_list): + if any(len(client_id) > 255 for client_id in client_id_list): + self._errors.append(self._format_error( + key='clientIDList', + value=client_id_list, + constraint='Member must satisfy constraint: ' + '[Member must have length less than or equal to 255, ' + 'Member must have length greater than or equal to 1]', + )) + + if any(len(thumbprint) > 40 for thumbprint in thumbprint_list): + self._errors.append(self._format_error( + key='thumbprintList', + value=thumbprint_list, + constraint='Member must satisfy constraint: ' + '[Member must have length less than or equal to 40, ' + 'Member must have length greater than or equal to 40]', + )) + + if len(url) > 255: + self._errors.append(self._format_error( + key='url', + value=url, + constraint='Member must have length less than or equal to 255', + )) + + self._raise_errors() + + parsed_url = urlparse(url) + if not parsed_url.scheme or not parsed_url.netloc: + raise ValidationError('Invalid Open ID Connect Provider URL') + + if len(thumbprint_list) > 5: + raise InvalidInput('Thumbprint list must contain fewer than 5 entries.') + + if len(client_id_list) > 100: + raise IAMLimitExceededException('Cannot exceed quota for ClientIdsPerOpenIdConnectProvider: 100') + + def _format_error(self, key, value, constraint): + return 'Value "{value}" at "{key}" failed to satisfy constraint: {constraint}'.format( + constraint=constraint, + key=key, + value=value, + ) + + def _raise_errors(self): + if self._errors: + count = len(self._errors) + plural = "s" if len(self._errors) > 1 else "" + errors = "; ".join(self._errors) + self._errors = [] # reset collected errors + + raise ValidationError('{count} validation error{plural} detected: {errors}'.format( + count=count, plural=plural, errors=errors, + )) + + class PolicyVersion(object): def __init__(self, @@ -515,6 +588,7 @@ class IAMBackend(BaseBackend): self.managed_policies = self._init_managed_policies() self.account_aliases = [] self.saml_providers = {} + self.open_id_providers = {} self.policy_arn_regex = re.compile( r'^arn:aws:iam::[0-9]*:policy/.*$') super(IAMBackend, self).__init__() @@ -1264,5 +1338,14 @@ class IAMBackend(BaseBackend): return user return None + def create_open_id_connect_provider(self, url, thumbprint_list, client_id_list): + open_id_provider = OpenIDConnectProvider(url, thumbprint_list, client_id_list) + + if open_id_provider.arn in self.open_id_providers: + raise EntityAlreadyExists + + self.open_id_providers[open_id_provider.arn] = open_id_provider + return open_id_provider + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 8e63c1075..a4ed38666 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -755,6 +755,15 @@ class IamResponse(BaseResponse): template = self.response_template(UNTAG_ROLE_TEMPLATE) return template.render() + def create_open_id_connect_provider(self): + open_id_provider_url = self._get_param('Url') + thumbprint_list = self._get_multi_param('ThumbprintList.member') + client_id_list = self._get_multi_param('ClientIDList.member') + open_id_provider = iam_backend.create_open_id_connect_provider(open_id_provider_url, thumbprint_list, client_id_list) + + template = self.response_template(CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) + return template.render(open_id_provider=open_id_provider) + LIST_ENTITIES_FOR_POLICY_TEMPLATE = """ @@ -1974,3 +1983,13 @@ UNTAG_ROLE_TEMPLATE = """ + + {{ open_id_provider.arn }} + + + f248366a-4f64-11e4-aefa-bfd6aEXAMPLE + +""" diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 23846712d..deec15285 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -1565,3 +1565,133 @@ def test_create_role_with_permissions_boundary(): # Ensure the PermissionsBoundary is included in role listing as well conn.list_roles().get('Roles')[0].get('PermissionsBoundary').should.equal(expected) + + +@mock_iam +def test_create_open_id_connect_provider(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[] # even it is required to provide at least one thumbprint, AWS accepts an empty list + ) + + response['OpenIDConnectProviderArn'].should.equal( + 'arn:aws:iam::123456789012:oidc-provider/example.com' + ) + + response = client.create_open_id_connect_provider( + Url='http://example.org', + ThumbprintList=[ + 'b' * 40 + ], + ClientIDList=[ + 'b' + ] + ) + + response['OpenIDConnectProviderArn'].should.equal( + 'arn:aws:iam::123456789012:oidc-provider/example.org' + ) + + response = client.create_open_id_connect_provider( + Url='http://example.org/oidc', + ThumbprintList=[] + ) + + response['OpenIDConnectProviderArn'].should.equal( + 'arn:aws:iam::123456789012:oidc-provider/example.org/oidc' + ) + + response = client.create_open_id_connect_provider( + Url='http://example.org/oidc-query?test=true', + ThumbprintList=[] + ) + + response['OpenIDConnectProviderArn'].should.equal( + 'arn:aws:iam::123456789012:oidc-provider/example.org/oidc-query' + ) + + +@mock_iam +def test_create_open_id_connect_provider_errors(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[] + ) + open_id_arn = response['OpenIDConnectProviderArn'] + + client.create_open_id_connect_provider.when.called_with( + Url='https://example.com', + ThumbprintList=[] + ).should.throw( + ClientError, + 'Unknown' + ) + + client.create_open_id_connect_provider.when.called_with( + Url='example.org', + ThumbprintList=[] + ).should.throw( + ClientError, + 'Invalid Open ID Connect Provider URL' + ) + + client.create_open_id_connect_provider.when.called_with( + Url='example', + ThumbprintList=[] + ).should.throw( + ClientError, + 'Invalid Open ID Connect Provider URL' + ) + + client.create_open_id_connect_provider.when.called_with( + Url='http://example.org', + ThumbprintList=[ + 'a' * 40, + 'b' * 40, + 'c' * 40, + 'd' * 40, + 'e' * 40, + 'f' * 40, + ] + ).should.throw( + ClientError, + 'Thumbprint list must contain fewer than 5 entries.' + ) + + too_many_client_ids = ['{}'.format(i) for i in range(101)] + client.create_open_id_connect_provider.when.called_with( + Url='http://example.org', + ThumbprintList=[], + ClientIDList=too_many_client_ids + ).should.throw( + ClientError, + 'Cannot exceed quota for ClientIdsPerOpenIdConnectProvider: 100' + ) + + too_long_url = 'b' * 256 + too_long_thumbprint = 'b' * 41 + too_long_client_id = 'b' * 256 + client.create_open_id_connect_provider.when.called_with( + Url=too_long_url, + ThumbprintList=[ + too_long_thumbprint + ], + ClientIDList=[ + too_long_client_id + ] + ).should.throw( + ClientError, + '3 validation errors detected: ' + 'Value "{0}" at "clientIDList" failed to satisfy constraint: ' + 'Member must satisfy constraint: ' + '[Member must have length less than or equal to 255, ' + 'Member must have length greater than or equal to 1]; ' + 'Value "{1}" at "thumbprintList" failed to satisfy constraint: ' + 'Member must satisfy constraint: ' + '[Member must have length less than or equal to 40, ' + 'Member must have length greater than or equal to 40]; ' + 'Value "{2}" at "url" failed to satisfy constraint: ' + 'Member must have length less than or equal to 255'.format([too_long_client_id], [too_long_thumbprint], too_long_url) + ) From c492c5c2289abcf7b6aef8596764a020563d8b42 Mon Sep 17 00:00:00 2001 From: gruebel Date: Fri, 18 Oct 2019 20:37:35 +0200 Subject: [PATCH 2/5] Add iam.get_open_id_connect_provider --- IMPLEMENTATION_COVERAGE.md | 2 +- moto/iam/models.py | 13 +++++++++ moto/iam/responses.py | 30 ++++++++++++++++++++ tests/test_iam/test_iam.py | 57 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 99 insertions(+), 3 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 3bea66408..e91376eda 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3227,7 +3227,7 @@ - [X] get_group_policy - [X] get_instance_profile - [X] get_login_profile -- [ ] get_open_id_connect_provider +- [X] get_open_id_connect_provider - [ ] get_organizations_access_report - [X] get_policy - [X] get_policy_version diff --git a/moto/iam/models.py b/moto/iam/models.py index 2a7d185a5..b95c37df7 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -104,11 +104,16 @@ class OpenIDConnectProvider(BaseModel): self.url = parsed_url.netloc + parsed_url.path self.thumbprint_list = thumbprint_list self.client_id_list = client_id_list + self.create_date = datetime.utcnow() @property def arn(self): return 'arn:aws:iam::{0}:oidc-provider/{1}'.format(ACCOUNT_ID, self.url) + @property + def created_iso_8601(self): + return iso_8601_datetime_without_milliseconds(self.create_date) + def _validate(self, url, thumbprint_list, client_id_list): if any(len(client_id) > 255 for client_id in client_id_list): self._errors.append(self._format_error( @@ -1347,5 +1352,13 @@ class IAMBackend(BaseBackend): self.open_id_providers[open_id_provider.arn] = open_id_provider return open_id_provider + def get_open_id_connect_provider(self, arn): + open_id_provider = self.open_id_providers.get(arn) + + if not open_id_provider: + raise IAMNotFoundException('OpenIDConnect Provider not found for arn {}'.format(arn)) + + return open_id_provider + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index a4ed38666..06ad7b7ce 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -759,11 +759,20 @@ class IamResponse(BaseResponse): open_id_provider_url = self._get_param('Url') thumbprint_list = self._get_multi_param('ThumbprintList.member') client_id_list = self._get_multi_param('ClientIDList.member') + open_id_provider = iam_backend.create_open_id_connect_provider(open_id_provider_url, thumbprint_list, client_id_list) template = self.response_template(CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) return template.render(open_id_provider=open_id_provider) + def get_open_id_connect_provider(self): + open_id_provider_arn = self._get_param('OpenIDConnectProviderArn') + + open_id_provider = iam_backend.get_open_id_connect_provider(open_id_provider_arn) + + template = self.response_template(GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) + return template.render(open_id_provider=open_id_provider) + LIST_ENTITIES_FOR_POLICY_TEMPLATE = """ @@ -1993,3 +2002,24 @@ CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """f248366a-4f64-11e4-aefa-bfd6aEXAMPLE """ + + +GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """ + + + {% for thumbprint in open_id_provider.thumbprint_list %} + {{ thumbprint }} + {% endfor %} + + {{ open_id_provider.created_iso_8601 }} + + {% for client_id in open_id_provider.client_id_list %} + {{ client_id }} + {% endfor %} + + {{ open_id_provider.url }} + + + 2c91531b-4f65-11e4-aefa-bfd6aEXAMPLE + +""" diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index deec15285..ea0434bff 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -9,6 +9,9 @@ import sure # noqa import sys from boto.exception import BotoServerError from botocore.exceptions import ClientError +from dateutil.tz import tzutc +from freezegun import freeze_time + from moto import mock_iam, mock_iam_deprecated from moto.iam.models import aws_managed_policies from nose.tools import assert_raises, assert_equals @@ -1615,11 +1618,10 @@ def test_create_open_id_connect_provider(): @mock_iam def test_create_open_id_connect_provider_errors(): client = boto3.client('iam', region_name='us-east-1') - response = client.create_open_id_connect_provider( + client.create_open_id_connect_provider( Url='https://example.com', ThumbprintList=[] ) - open_id_arn = response['OpenIDConnectProviderArn'] client.create_open_id_connect_provider.when.called_with( Url='https://example.com', @@ -1695,3 +1697,54 @@ def test_create_open_id_connect_provider_errors(): 'Value "{2}" at "url" failed to satisfy constraint: ' 'Member must have length less than or equal to 255'.format([too_long_client_id], [too_long_thumbprint], too_long_url) ) + + +@freeze_time('2019-01-01 00:00:00') +@mock_iam +def test_get_open_id_connect_provider(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[ + 'b' * 40 + ], + ClientIDList=[ + 'b' + ] + ) + open_id_arn = response['OpenIDConnectProviderArn'] + + response = client.get_open_id_connect_provider( + OpenIDConnectProviderArn=open_id_arn + ) + + response['Url'].should.equal('example.com') + response['ThumbprintList'].should.equal([ + 'b' * 40 + ]) + response['ClientIDList'].should.equal([ + 'b' + ]) + response['CreateDate'].should.equal(datetime.now(tzutc())) + + +@mock_iam +def test_get_open_id_connect_provider_errors(): + client = boto3.client('iam', region_name = 'us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[ + 'b' * 40 + ], + ClientIDList=[ + 'b' + ] + ) + open_id_arn = response['OpenIDConnectProviderArn'] + + client.get_open_id_connect_provider.when.called_with( + OpenIDConnectProviderArn = open_id_arn + '-not-existing' + ).should.throw( + ClientError, + 'OpenIDConnect Provider not found for arn {}'.format(open_id_arn + '-not-existing') + ) From f4af9a1d548110ce763e4422150b8459e3c69bf9 Mon Sep 17 00:00:00 2001 From: gruebel Date: Fri, 18 Oct 2019 20:51:22 +0200 Subject: [PATCH 3/5] Add iam.delete_open_id_connect_provider --- IMPLEMENTATION_COVERAGE.md | 2 +- moto/iam/models.py | 3 +++ moto/iam/responses.py | 15 +++++++++++++++ tests/test_iam/test_iam.py | 30 ++++++++++++++++++++++++++++-- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index e91376eda..8ccfb2ccf 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3193,7 +3193,7 @@ - [ ] delete_group_policy - [ ] delete_instance_profile - [X] delete_login_profile -- [ ] delete_open_id_connect_provider +- [X] delete_open_id_connect_provider - [X] delete_policy - [X] delete_policy_version - [X] delete_role diff --git a/moto/iam/models.py b/moto/iam/models.py index b95c37df7..6c790c15f 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -1352,6 +1352,9 @@ class IAMBackend(BaseBackend): self.open_id_providers[open_id_provider.arn] = open_id_provider return open_id_provider + def delete_open_id_connect_provider(self, arn): + self.open_id_providers.pop(arn, None) + def get_open_id_connect_provider(self, arn): open_id_provider = self.open_id_providers.get(arn) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 06ad7b7ce..819f0e649 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -765,6 +765,14 @@ class IamResponse(BaseResponse): template = self.response_template(CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) return template.render(open_id_provider=open_id_provider) + def delete_open_id_connect_provider(self): + open_id_provider_arn = self._get_param('OpenIDConnectProviderArn') + + iam_backend.delete_open_id_connect_provider(open_id_provider_arn) + + template = self.response_template(DELETE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) + return template.render() + def get_open_id_connect_provider(self): open_id_provider_arn = self._get_param('OpenIDConnectProviderArn') @@ -2004,6 +2012,13 @@ CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """""" +DELETE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """ + + b5e49e29-4f64-11e4-aefa-bfd6aEXAMPLE + +""" + + GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """ diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index ea0434bff..cffa00a37 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -1699,6 +1699,32 @@ def test_create_open_id_connect_provider_errors(): ) +@mock_iam +def test_delete_open_id_connect_provider(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[] + ) + open_id_arn = response['OpenIDConnectProviderArn'] + + client.delete_open_id_connect_provider( + OpenIDConnectProviderArn=open_id_arn + ) + + client.get_open_id_connect_provider.when.called_with( + OpenIDConnectProviderArn=open_id_arn + ).should.throw( + ClientError, + 'OpenIDConnect Provider not found for arn {}'.format(open_id_arn) + ) + + # deleting a non existing provider should be successful + client.delete_open_id_connect_provider( + OpenIDConnectProviderArn=open_id_arn + ) + + @freeze_time('2019-01-01 00:00:00') @mock_iam def test_get_open_id_connect_provider(): @@ -1730,7 +1756,7 @@ def test_get_open_id_connect_provider(): @mock_iam def test_get_open_id_connect_provider_errors(): - client = boto3.client('iam', region_name = 'us-east-1') + client = boto3.client('iam', region_name='us-east-1') response = client.create_open_id_connect_provider( Url='https://example.com', ThumbprintList=[ @@ -1743,7 +1769,7 @@ def test_get_open_id_connect_provider_errors(): open_id_arn = response['OpenIDConnectProviderArn'] client.get_open_id_connect_provider.when.called_with( - OpenIDConnectProviderArn = open_id_arn + '-not-existing' + OpenIDConnectProviderArn=open_id_arn + '-not-existing' ).should.throw( ClientError, 'OpenIDConnect Provider not found for arn {}'.format(open_id_arn + '-not-existing') From cd8027ce9db403ac2460d8bd1e534a300ae232db Mon Sep 17 00:00:00 2001 From: gruebel Date: Fri, 18 Oct 2019 21:12:44 +0200 Subject: [PATCH 4/5] Add iam.list_open_id_connect_providers --- IMPLEMENTATION_COVERAGE.md | 4 ++-- moto/iam/models.py | 3 +++ moto/iam/responses.py | 22 ++++++++++++++++++++ tests/test_iam/test_iam.py | 41 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 8ccfb2ccf..284f4d68a 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3163,7 +3163,7 @@ - [ ] describe_events ## iam -56% implemented +57% implemented - [ ] add_client_id_to_open_id_connect_provider - [X] add_role_to_instance_profile - [X] add_user_to_group @@ -3253,7 +3253,7 @@ - [ ] list_instance_profiles - [ ] list_instance_profiles_for_role - [X] list_mfa_devices -- [ ] list_open_id_connect_providers +- [X] list_open_id_connect_providers - [X] list_policies - [ ] list_policies_granting_service_access - [X] list_policy_versions diff --git a/moto/iam/models.py b/moto/iam/models.py index 6c790c15f..ba7d0ac82 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -1363,5 +1363,8 @@ class IAMBackend(BaseBackend): return open_id_provider + def list_open_id_connect_providers(self): + return list(self.open_id_providers.keys()) + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 819f0e649..1c00d211c 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -781,6 +781,12 @@ class IamResponse(BaseResponse): template = self.response_template(GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) return template.render(open_id_provider=open_id_provider) + def list_open_id_connect_providers(self): + open_id_provider_arns = iam_backend.list_open_id_connect_providers() + + template = self.response_template(LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE) + return template.render(open_id_provider_arns=open_id_provider_arns) + LIST_ENTITIES_FOR_POLICY_TEMPLATE = """ @@ -2038,3 +2044,19 @@ GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """2c91531b-4f65-11e4-aefa-bfd6aEXAMPLE """ + + +LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE = """ + + + {% for open_id_provider_arn in open_id_provider_arns %} + + {{ open_id_provider_arn }} + + {% endfor %} + + + + de2c0228-4f63-11e4-aefa-bfd6aEXAMPLE + +""" diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index cffa00a37..f87af0d16 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -1774,3 +1774,44 @@ def test_get_open_id_connect_provider_errors(): ClientError, 'OpenIDConnect Provider not found for arn {}'.format(open_id_arn + '-not-existing') ) + + +@mock_iam +def test_list_open_id_connect_providers(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[] + ) + open_id_arn_1 = response['OpenIDConnectProviderArn'] + + response = client.create_open_id_connect_provider( + Url='http://example.org', + ThumbprintList=[ + 'b' * 40 + ], + ClientIDList=[ + 'b' + ] + ) + open_id_arn_2 = response['OpenIDConnectProviderArn'] + + response = client.create_open_id_connect_provider( + Url='http://example.org/oidc', + ThumbprintList=[] + ) + open_id_arn_3 = response['OpenIDConnectProviderArn'] + + response = client.list_open_id_connect_providers() + + response['OpenIDConnectProviderList'].should.equal([ + { + 'Arn': open_id_arn_1 + }, + { + 'Arn': open_id_arn_2 + }, + { + 'Arn': open_id_arn_3 + } + ]) From 3f3feb5bdb9140e9b7f0aab1a8a873daa2b579a3 Mon Sep 17 00:00:00 2001 From: gruebel Date: Sat, 19 Oct 2019 14:23:35 +0200 Subject: [PATCH 5/5] Fix tests --- tests/test_iam/test_iam.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index f87af0d16..2374fb599 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -10,7 +10,6 @@ import sys from boto.exception import BotoServerError from botocore.exceptions import ClientError from dateutil.tz import tzutc -from freezegun import freeze_time from moto import mock_iam, mock_iam_deprecated from moto.iam.models import aws_managed_policies @@ -1725,7 +1724,6 @@ def test_delete_open_id_connect_provider(): ) -@freeze_time('2019-01-01 00:00:00') @mock_iam def test_get_open_id_connect_provider(): client = boto3.client('iam', region_name='us-east-1') @@ -1751,7 +1749,7 @@ def test_get_open_id_connect_provider(): response['ClientIDList'].should.equal([ 'b' ]) - response['CreateDate'].should.equal(datetime.now(tzutc())) + response.should.have.key('CreateDate').should.be.a(datetime) @mock_iam @@ -1804,14 +1802,16 @@ def test_list_open_id_connect_providers(): response = client.list_open_id_connect_providers() - response['OpenIDConnectProviderList'].should.equal([ - { - 'Arn': open_id_arn_1 - }, - { - 'Arn': open_id_arn_2 - }, - { - 'Arn': open_id_arn_3 - } - ]) + sorted(response['OpenIDConnectProviderList'], key=lambda i: i['Arn']).should.equal( + [ + { + 'Arn': open_id_arn_1 + }, + { + 'Arn': open_id_arn_2 + }, + { + 'Arn': open_id_arn_3 + } + ] + )