Merge pull request #23249 from lyft/boto-iam-assume-role-support

Support for assumed role policies and roles without instance profiles
This commit is contained in:
Nicole Thomas 2015-05-05 09:20:15 -06:00
commit dcfdbf189d
3 changed files with 184 additions and 25 deletions

View file

@ -165,7 +165,19 @@ def describe_role(name, region=None, key=None, keyid=None, profile=None):
info = conn.get_role(name)
if not info:
return False
return info
role = info.get_role_response.get_role_result.role
role['assume_role_policy_document'] = json.loads(_unquote(
role.assume_role_policy_document
))
# If Sid wasn't defined by the user, boto will still return a Sid in
# each policy. To properly check idempotently, let's remove the Sid
# from the return if it's not actually set.
for policy_key, policy in role['assume_role_policy_document'].items():
if policy_key == 'Statement':
for val in policy:
if 'Sid' in val and not val['Sid']:
del val['Sid']
return role
except boto.exception.BotoServerError as e:
log.debug(e)
msg = 'Failed to get {0} information.'
@ -887,6 +899,65 @@ def delete_role_policy(role_name, policy_name, region=None, key=None,
return False
def update_assume_role_policy(role_name, policy_document, region=None,
key=None, keyid=None, profile=None):
'''
Update an assume role policy for a role.
CLI example::
salt myminion boto_iam.update_assume_role_policy myrole '{"Statement":"..."}'
'''
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
if isinstance(policy_document, string_types):
policy_document = json.loads(policy_document,
object_pairs_hook=odict.OrderedDict)
try:
_policy_document = json.dumps(policy_document)
conn.update_assume_role_policy(role_name, _policy_document)
msg = 'Successfully updated assume role policy for role {0}.'
log.info(msg.format(role_name))
return True
except boto.exception.BotoServerError as e:
log.debug(e)
msg = 'Failed to update assume role policy for role {0}.'
log.error(msg.format(role_name))
return False
def build_policy(region=None, key=None, keyid=None, profile=None):
'''
Build a default assume role policy.
CLI example::
salt myminion boto_iam.build_policy
'''
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
if hasattr(conn, 'build_policy'):
policy = json.loads(conn.build_policy())
elif hasattr(conn, '_build_policy'):
policy = json.loads(conn._build_policy())
else:
return {}
# The format we get from build_policy isn't going to be what we get back
# from AWS for the exact same policy. AWS converts single item list values
# into strings, so let's do the same here.
for key, policy_val in policy.items():
for statement in policy_val:
if (isinstance(statement['Action'], list)
and len(statement['Action']) == 1):
statement['Action'] = statement['Action'][0]
if (isinstance(statement['Principal']['Service'], list)
and len(statement['Principal']['Service']) == 1):
statement['Principal']['Service'] = statement['Principal']['Service'][0]
# build_policy doesn't add a version field, which AWS is going to set to a
# default value, when we get it back, so let's set it.
policy['Version'] = '2008-10-17'
return policy
def get_account_id(region=None, key=None, keyid=None, profile=None):
'''
Get a the AWS account id associated with the used credentials.

View file

@ -91,6 +91,7 @@ def present(
path=None,
policies=None,
policies_from_pillars=None,
create_instance_profile=True,
region=None,
key=None,
keyid=None,
@ -105,7 +106,7 @@ def present(
The policy that grants an entity permission to assume the role. (See http://boto.readthedocs.org/en/latest/ref/iam.html#boto.iam.connection.IAMConnection.create_role)
path
The path to the instance profile. (See http://boto.readthedocs.org/en/latest/ref/iam.html#boto.iam.connection.IAMConnection.create_role)
The path to the role/instance profile. (See http://boto.readthedocs.org/en/latest/ref/iam.html#boto.iam.connection.IAMConnection.create_role)
policies
A dict of IAM role policies.
@ -119,6 +120,10 @@ def present(
in the policies argument will override the keys defined in
policies_from_pillars.
create_instance_profile
A boolean of whether or not to create an instance profile and associate
it with this role.
region
Region to connect to.
@ -150,20 +155,21 @@ def present(
ret['result'] = _ret['result']
if ret['result'] is False:
return ret
_ret = _instance_profile_present(name, region, key, keyid, profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
if not _ret['result']:
ret['result'] = _ret['result']
if ret['result'] is False:
return ret
_ret = _instance_profile_associated(name, region, key, keyid, profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
if not _ret['result']:
ret['result'] = _ret['result']
if ret['result'] is False:
return ret
if create_instance_profile:
_ret = _instance_profile_present(name, region, key, keyid, profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
if not _ret['result']:
ret['result'] = _ret['result']
if ret['result'] is False:
return ret
_ret = _instance_profile_associated(name, region, key, keyid, profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
if not _ret['result']:
ret['result'] = _ret['result']
if ret['result'] is False:
return ret
_ret = _policies_present(name, _policies, region, key, keyid, profile)
ret['changes'] = dictupdate.update(ret['changes'], _ret['changes'])
ret['comment'] = ' '.join([ret['comment'], _ret['comment']])
@ -181,9 +187,9 @@ def _role_present(
keyid=None,
profile=None):
ret = {'result': True, 'comment': '', 'changes': {}}
exists = __salt__['boto_iam.role_exists'](name, region, key, keyid,
role = __salt__['boto_iam.describe_role'](name, region, key, keyid,
profile)
if not exists:
if not role:
if __opts__['test']:
ret['comment'] = 'IAM role {0} is set to be created.'.format(name)
ret['result'] = None
@ -200,6 +206,36 @@ def _role_present(
ret['comment'] = 'Failed to create {0} IAM role.'.format(name)
else:
ret['comment'] = '{0} role present.'.format(name)
update_needed = False
_policy_document = None
if not policy_document:
policy = __salt__['boto_iam.build_policy'](region, key, keyid,
profile)
if role['assume_role_policy_document'] != policy:
update_needed = True
_policy_document = policy
else:
if role['assume_role_policy_document'] != policy_document:
update_needed = True
_policy_document = policy_document
if update_needed:
if __opts__['test']:
msg = 'Assume role policy document to be updated.'
ret['comment'] = '{0} {1}'.format(ret['comment'], msg)
ret['result'] = None
return ret
updated = __salt__['boto_iam.update_assume_role_policy'](
name, _policy_document, region, key, keyid, profile
)
if updated:
msg = 'Assume role policy document updated.'
ret['comment'] = '{0} {1}'.format(ret['comment'], msg)
ret['changes']['old'] = {'policy_document': policy_document}
ret['changes']['new'] = {'policy_document': _policy_document}
else:
ret['result'] = False
msg = 'Failed to update assume role policy.'
ret['comment'] = '{0} {1}'.format(ret['comment'], msg)
return ret

View file

@ -25,7 +25,7 @@ boto_iam_role.__opts__ = {}
@skipIf(NO_MOCK, NO_MOCK_REASON)
class BotoElbTestCase(TestCase):
class BotoIAMRoleTestCase(TestCase):
'''
Test cases for salt.states.boto_iam_role
'''
@ -42,16 +42,63 @@ class BotoElbTestCase(TestCase):
'changes': {},
'comment': ''}
mock = MagicMock(side_effect=[False, True, False, True, True,
False, True, True, True, True])
_desc_role = {
'create_date': '2015-02-11T19:47:14Z',
'role_id': 'HIUHBIUBIBNKJNBKJ',
'assume_role_policy_document': {
'Version': '2008-10-17',
'Statement': [{
'Action': 'sts:AssumeRole',
'Principal': {'Service': 'ec2.amazonaws.com'},
'Effect': 'Allow'
}]},
'role_name': 'myfakerole',
'path': '/',
'arn': 'arn:aws:iam::12345:role/myfakerole'
}
_desc_role2 = {
'create_date': '2015-02-11T19:47:14Z',
'role_id': 'HIUHBIUBIBNKJNBKJ',
'assume_role_policy_document': {
'Version': '2008-10-17',
'Statement': [{
'Action': 'sts:AssumeRole',
'Principal': {
'Service': [
'ec2.amazonaws.com',
'datapipeline.amazonaws.com'
]
},
'Effect': 'Allow'
}]},
'role_name': 'myfakerole',
'path': '/',
'arn': 'arn:aws:iam::12345:role/myfakerole'
}
mock_desc = MagicMock(side_effect=[
False, _desc_role, _desc_role, _desc_role2, _desc_role
])
_build_policy = {
'Version': '2008-10-17',
'Statement': [{
'Action': 'sts:AssumeRole',
'Effect': 'Allow',
'Principal': {'Service': 'ec2.amazonaws.com'}
}]
}
mock_policy = MagicMock(return_value=_build_policy)
mock_ipe = MagicMock(side_effect=[False, True, True, True])
mock_pa = MagicMock(side_effect=[False, True, True, True])
mock_bool = MagicMock(return_value=False)
mock_lst = MagicMock(return_value=[])
with patch.dict(boto_iam_role.__salt__,
{'boto_iam.role_exists': mock,
{'boto_iam.describe_role': mock_desc,
'boto_iam.create_role': mock_bool,
'boto_iam.instance_profile_exists': mock,
'boto_iam.build_policy': mock_policy,
'boto_iam.update_assume_role_policy': mock_bool,
'boto_iam.instance_profile_exists': mock_ipe,
'boto_iam.create_instance_profile': mock_bool,
'boto_iam.profile_associated': mock,
'boto_iam.profile_associated': mock_pa,
'boto_iam.associate_profile_to_role': mock_bool,
'boto_iam.list_role_policies': mock_lst}):
with patch.dict(boto_iam_role.__opts__, {'test': False}):
@ -69,6 +116,11 @@ class BotoElbTestCase(TestCase):
ret.update({'comment': comt})
self.assertDictEqual(boto_iam_role.present(name), ret)
comt = (' myrole role present. Failed to update assume role'
' policy.')
ret.update({'comment': comt})
self.assertDictEqual(boto_iam_role.present(name), ret)
comt = (' myrole role present. ')
ret.update({'comment': comt, 'result': True})
self.assertDictEqual(boto_iam_role.present(name), ret)
@ -124,4 +176,4 @@ class BotoElbTestCase(TestCase):
if __name__ == '__main__':
from integration import run_tests
run_tests(BotoElbTestCase, needs_daemon=False)
run_tests(BotoIAMRoleTestCase, needs_daemon=False)