Merge pull request #2494 from edekadigital/add-iam-open-id-connect-provider

Add iam open id connect provider
This commit is contained in:
Steve Pulec 2019-10-19 07:54:45 -05:00 committed by GitHub
commit b433399cb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 469 additions and 7 deletions

View File

@ -3163,7 +3163,7 @@
- [ ] describe_events
## iam
55% implemented
57% 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
@ -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
@ -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
@ -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

View File

@ -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)

View File

@ -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,82 @@ 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
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(
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 +593,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 +1343,28 @@ 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
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)
if not open_id_provider:
raise IAMNotFoundException('OpenIDConnect Provider not found for arn {}'.format(arn))
return open_id_provider
def list_open_id_connect_providers(self):
return list(self.open_id_providers.keys())
iam_backend = IAMBackend()

View File

@ -755,6 +755,38 @@ 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)
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')
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)
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 = """<ListEntitiesForPolicyResponse>
<ListEntitiesForPolicyResult>
@ -1974,3 +2006,57 @@ UNTAG_ROLE_TEMPLATE = """<UntagRoleResponse xmlns="https://iam.amazonaws.com/doc
<RequestId>EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE</RequestId>
</ResponseMetadata>
</UntagRoleResponse>"""
CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """<CreateOpenIDConnectProviderResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<CreateOpenIDConnectProviderResult>
<OpenIDConnectProviderArn>{{ open_id_provider.arn }}</OpenIDConnectProviderArn>
</CreateOpenIDConnectProviderResult>
<ResponseMetadata>
<RequestId>f248366a-4f64-11e4-aefa-bfd6aEXAMPLE</RequestId>
</ResponseMetadata>
</CreateOpenIDConnectProviderResponse>"""
DELETE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """<DeleteOpenIDConnectProviderResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ResponseMetadata>
<RequestId>b5e49e29-4f64-11e4-aefa-bfd6aEXAMPLE</RequestId>
</ResponseMetadata>
</DeleteOpenIDConnectProviderResponse>"""
GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """<GetOpenIDConnectProviderResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<GetOpenIDConnectProviderResult>
<ThumbprintList>
{% for thumbprint in open_id_provider.thumbprint_list %}
<member>{{ thumbprint }}</member>
{% endfor %}
</ThumbprintList>
<CreateDate>{{ open_id_provider.created_iso_8601 }}</CreateDate>
<ClientIDList>
{% for client_id in open_id_provider.client_id_list %}
<member>{{ client_id }}</member>
{% endfor %}
</ClientIDList>
<Url>{{ open_id_provider.url }}</Url>
</GetOpenIDConnectProviderResult>
<ResponseMetadata>
<RequestId>2c91531b-4f65-11e4-aefa-bfd6aEXAMPLE</RequestId>
</ResponseMetadata>
</GetOpenIDConnectProviderResponse>"""
LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE = """<ListOpenIDConnectProvidersResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
<ListOpenIDConnectProvidersResult>
<OpenIDConnectProviderList>
{% for open_id_provider_arn in open_id_provider_arns %}
<member>
<Arn>{{ open_id_provider_arn }}</Arn>
</member>
{% endfor %}
</OpenIDConnectProviderList>
</ListOpenIDConnectProvidersResult>
<ResponseMetadata>
<RequestId>de2c0228-4f63-11e4-aefa-bfd6aEXAMPLE</RequestId>
</ResponseMetadata>
</ListOpenIDConnectProvidersResponse>"""

View File

@ -9,6 +9,8 @@ import sure # noqa
import sys
from boto.exception import BotoServerError
from botocore.exceptions import ClientError
from dateutil.tz import tzutc
from moto import mock_iam, mock_iam_deprecated
from moto.iam.models import aws_managed_policies
from nose.tools import assert_raises, assert_equals
@ -1565,3 +1567,251 @@ 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')
client.create_open_id_connect_provider(
Url='https://example.com',
ThumbprintList=[]
)
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)
)
@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
)
@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.should.have.key('CreateDate').should.be.a(datetime)
@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')
)
@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()
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
}
]
)