mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00
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:
commit
dcfdbf189d
3 changed files with 184 additions and 25 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue