Add IAM Role and Policy to Config
This commit is contained in:
parent
2e0e542efe
commit
7bc5b5c08f
6 changed files with 793 additions and 5 deletions
|
|
@ -47,8 +47,8 @@ from moto.config.exceptions import (
|
|||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.s3.config import s3_account_public_access_block_query, s3_config_query
|
||||
|
||||
from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID
|
||||
from moto.iam.config import role_config_query, policy_config_query
|
||||
|
||||
POP_STRINGS = [
|
||||
"capitalizeStart",
|
||||
|
|
@ -64,6 +64,8 @@ DEFAULT_PAGE_SIZE = 100
|
|||
RESOURCE_MAP = {
|
||||
"AWS::S3::Bucket": s3_config_query,
|
||||
"AWS::S3::AccountPublicAccessBlock": s3_account_public_access_block_query,
|
||||
"AWS::IAM::Role": role_config_query,
|
||||
"AWS::IAM::Policy": policy_config_query,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -766,6 +766,27 @@ class ConfigQueryModel(object):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def aggregate_regions(self, path, backend_region, resource_region):
|
||||
"""
|
||||
Returns a list of "region\1eresourcename" strings
|
||||
"""
|
||||
|
||||
filter_region = backend_region or resource_region
|
||||
if filter_region:
|
||||
filter_resources = list(self.backends[filter_region].__dict__[path].keys())
|
||||
return map(
|
||||
lambda resource: "{}\1e{}".format(filter_region, resource),
|
||||
filter_resources,
|
||||
)
|
||||
|
||||
# If we don't have a filter region
|
||||
ret = []
|
||||
for region in self.backends:
|
||||
this_region_resources = list(self.backends[region].__dict__[path].keys())
|
||||
for resource in this_region_resources:
|
||||
ret.append("{}\1e{}".format(region, resource))
|
||||
return ret
|
||||
|
||||
|
||||
class base_decorator(object):
|
||||
mock_backend = MockAWS
|
||||
|
|
|
|||
173
moto/iam/config.py
Normal file
173
moto/iam/config.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import json
|
||||
|
||||
from moto.core.exceptions import InvalidNextTokenException
|
||||
from moto.core.models import ConfigQueryModel
|
||||
from moto.iam import iam_backends
|
||||
|
||||
|
||||
class RoleConfigQuery(ConfigQueryModel):
|
||||
def list_config_service_resources(
|
||||
self,
|
||||
resource_ids,
|
||||
resource_name,
|
||||
limit,
|
||||
next_token,
|
||||
backend_region=None,
|
||||
resource_region=None,
|
||||
):
|
||||
# For aggregation -- did we get both a resource ID and a resource name?
|
||||
if resource_ids and resource_name:
|
||||
# If the values are different, then return an empty list:
|
||||
if resource_name not in resource_ids:
|
||||
return [], None
|
||||
|
||||
role_list = self.aggregate_regions("roles", backend_region, resource_region)
|
||||
|
||||
if not role_list:
|
||||
return [], None
|
||||
|
||||
# Pagination logic:
|
||||
sorted_roles = sorted(role_list)
|
||||
new_token = None
|
||||
|
||||
# Get the start:
|
||||
if not next_token:
|
||||
start = 0
|
||||
else:
|
||||
# "Tokens" are region + \00 + resource ID.
|
||||
if next_token not in sorted_roles:
|
||||
raise InvalidNextTokenException()
|
||||
|
||||
start = sorted_roles.index(next_token)
|
||||
|
||||
# Get the list of items to collect:
|
||||
role_list = sorted_roles[start : (start + limit)]
|
||||
|
||||
if len(sorted_roles) > (start + limit):
|
||||
new_token = sorted_roles[start + limit]
|
||||
|
||||
return (
|
||||
[
|
||||
{
|
||||
"type": "AWS::IAM::Role",
|
||||
"id": role.split("\1e")[1],
|
||||
"name": role.split("\1e")[1],
|
||||
"region": role.split("\1e")[0],
|
||||
}
|
||||
for role in role_list
|
||||
],
|
||||
new_token,
|
||||
)
|
||||
|
||||
def get_config_resource(
|
||||
self, resource_id, resource_name=None, backend_region=None, resource_region=None
|
||||
):
|
||||
|
||||
role = self.backends["global"].roles.get(resource_id, {})
|
||||
|
||||
if not role:
|
||||
return
|
||||
|
||||
if resource_name and role.name != resource_name:
|
||||
return
|
||||
|
||||
# Format the bucket to the AWS Config format:
|
||||
config_data = role.to_config_dict()
|
||||
|
||||
# The 'configuration' field is also a JSON string:
|
||||
config_data["configuration"] = json.dumps(config_data["configuration"])
|
||||
|
||||
# Supplementary config need all values converted to JSON strings if they are not strings already:
|
||||
for field, value in config_data["supplementaryConfiguration"].items():
|
||||
if not isinstance(value, str):
|
||||
config_data["supplementaryConfiguration"][field] = json.dumps(value)
|
||||
|
||||
return config_data
|
||||
|
||||
|
||||
class PolicyConfigQuery(ConfigQueryModel):
|
||||
def list_config_service_resources(
|
||||
self,
|
||||
resource_ids,
|
||||
resource_name,
|
||||
limit,
|
||||
next_token,
|
||||
backend_region=None,
|
||||
resource_region=None,
|
||||
):
|
||||
# For aggregation -- did we get both a resource ID and a resource name?
|
||||
if resource_ids and resource_name:
|
||||
# If the values are different, then return an empty list:
|
||||
if resource_name not in resource_ids:
|
||||
return [], None
|
||||
|
||||
# We don't want to include AWS Managed Policies
|
||||
policy_list = filter(
|
||||
lambda policy: not policy.split("\1e")[1].startswith("arn:aws:iam::aws"),
|
||||
self.aggregate_regions("managed_policies", backend_region, resource_region),
|
||||
)
|
||||
|
||||
if not policy_list:
|
||||
return [], None
|
||||
|
||||
# Pagination logic:
|
||||
sorted_policies = sorted(policy_list)
|
||||
new_token = None
|
||||
|
||||
# Get the start:
|
||||
if not next_token:
|
||||
start = 0
|
||||
else:
|
||||
# "Tokens" are region + \00 + resource ID.
|
||||
if next_token not in sorted_policies:
|
||||
raise InvalidNextTokenException()
|
||||
|
||||
start = sorted_policies.index(next_token)
|
||||
|
||||
# Get the list of items to collect:
|
||||
policy_list = sorted_policies[start : (start + limit)]
|
||||
|
||||
if len(sorted_policies) > (start + limit):
|
||||
new_token = sorted_policies[start + limit]
|
||||
|
||||
return (
|
||||
[
|
||||
{
|
||||
"type": "AWS::IAM::Policy",
|
||||
"id": policy.split("\1e")[1],
|
||||
"name": policy.split("\1e")[1],
|
||||
"region": policy.split("\1e")[0],
|
||||
}
|
||||
for policy in policy_list
|
||||
],
|
||||
new_token,
|
||||
)
|
||||
|
||||
def get_config_resource(
|
||||
self, resource_id, resource_name=None, backend_region=None, resource_region=None
|
||||
):
|
||||
|
||||
policy = self.backends["global"].managed_policies.get(resource_id, {})
|
||||
|
||||
if not policy:
|
||||
return
|
||||
|
||||
if resource_name and policy.name != resource_name:
|
||||
return
|
||||
|
||||
# Format the bucket to the AWS Config format:
|
||||
config_data = policy.to_config_dict()
|
||||
|
||||
# The 'configuration' field is also a JSON string:
|
||||
config_data["configuration"] = json.dumps(config_data["configuration"])
|
||||
|
||||
# Supplementary config need all values converted to JSON strings if they are not strings already:
|
||||
for field, value in config_data["supplementaryConfiguration"].items():
|
||||
if not isinstance(value, str):
|
||||
config_data["supplementaryConfiguration"][field] = json.dumps(value)
|
||||
|
||||
return config_data
|
||||
|
||||
|
||||
role_config_query = RoleConfigQuery(iam_backends)
|
||||
policy_config_query = PolicyConfigQuery(iam_backends)
|
||||
|
|
@ -8,11 +8,13 @@ import sys
|
|||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
from six.moves.urllib import parse
|
||||
from moto.core.exceptions import RESTError
|
||||
from moto.core import BaseBackend, BaseModel, ACCOUNT_ID, CloudFormationModel
|
||||
from moto.core.utils import (
|
||||
|
|
@ -153,7 +155,7 @@ class OpenIDConnectProvider(BaseModel):
|
|||
self._errors = []
|
||||
self._validate(url, thumbprint_list, client_id_list)
|
||||
|
||||
parsed_url = urlparse(url)
|
||||
parsed_url = parse.urlparse(url)
|
||||
self.url = parsed_url.netloc + parsed_url.path
|
||||
self.thumbprint_list = thumbprint_list
|
||||
self.client_id_list = client_id_list
|
||||
|
|
@ -201,7 +203,7 @@ class OpenIDConnectProvider(BaseModel):
|
|||
|
||||
self._raise_errors()
|
||||
|
||||
parsed_url = urlparse(url)
|
||||
parsed_url = parse.urlparse(url)
|
||||
if not parsed_url.scheme or not parsed_url.netloc:
|
||||
raise ValidationError("Invalid Open ID Connect Provider URL")
|
||||
|
||||
|
|
@ -265,6 +267,48 @@ class ManagedPolicy(Policy):
|
|||
def arn(self):
|
||||
return "arn:aws:iam::{0}:policy{1}{2}".format(ACCOUNT_ID, self.path, self.name)
|
||||
|
||||
def to_config_dict(self):
|
||||
return {
|
||||
"version": "1.3",
|
||||
"configurationItemCaptureTime": str(self.create_date),
|
||||
"configurationItemStatus": "OK",
|
||||
"configurationStateId": str(
|
||||
int(time.mktime(self.create_date.timetuple()))
|
||||
), # PY2 and 3 compatible
|
||||
"arn": "arn:aws:iam::{}:policy/{}".format(ACCOUNT_ID, self.name),
|
||||
"resourceType": "AWS::IAM::Policy",
|
||||
"resourceId": self.id,
|
||||
"resourceName": self.name,
|
||||
"awsRegion": "global",
|
||||
"availabilityZone": "Not Applicable",
|
||||
"resourceCreationTime": str(self.create_date),
|
||||
"configuration": {
|
||||
"policyName": self.name,
|
||||
"policyId": self.id,
|
||||
"arn": "arn:aws:iam::{}:policy/{}".format(ACCOUNT_ID, self.name),
|
||||
"path": self.path,
|
||||
"defaultVersionId": self.default_version_id,
|
||||
"attachmentCount": self.attachment_count,
|
||||
"permissionsBoundaryUsageCount": 0,
|
||||
"isAttachable": ManagedPolicy.is_attachable,
|
||||
"description": self.description,
|
||||
"createDate": str(self.create_date.isoformat()),
|
||||
"updateDate": str(self.create_date.isoformat()),
|
||||
"policyVersionList": list(
|
||||
map(
|
||||
lambda version: {
|
||||
"document": parse.quote(version.document),
|
||||
"versionId": version.version_id,
|
||||
"isDefaultVersion": version.is_default,
|
||||
"createDate": str(version.create_date),
|
||||
},
|
||||
self.versions,
|
||||
)
|
||||
),
|
||||
},
|
||||
"supplementaryConfiguration": {},
|
||||
}
|
||||
|
||||
|
||||
class AWSManagedPolicy(ManagedPolicy):
|
||||
"""AWS-managed policy."""
|
||||
|
|
@ -513,6 +557,69 @@ class Role(CloudFormationModel):
|
|||
def arn(self):
|
||||
return "arn:aws:iam::{0}:role{1}{2}".format(ACCOUNT_ID, self.path, self.name)
|
||||
|
||||
def to_config_dict(self):
|
||||
_managed_policies = []
|
||||
for key in self.managed_policies.keys():
|
||||
_managed_policies.append(
|
||||
{"policyArn": key, "policyName": iam_backend.managed_policies[key].name}
|
||||
)
|
||||
|
||||
_role_policy_list = []
|
||||
for key, value in self.policies.items():
|
||||
_role_policy_list.append(
|
||||
{"policyName": key, "policyDocument": parse.quote(value)}
|
||||
)
|
||||
|
||||
_instance_profiles = []
|
||||
for key, instance_profile in iam_backend.instance_profiles.items():
|
||||
for role in instance_profile.roles:
|
||||
_instance_profiles.append(instance_profile.to_embedded_config_dict())
|
||||
break
|
||||
|
||||
config_dict = {
|
||||
"version": "1.3",
|
||||
"configurationItemCaptureTime": str(self.create_date),
|
||||
"configurationItemStatus": "ResourceDiscovered",
|
||||
"configurationStateId": str(
|
||||
int(time.mktime(self.create_date.timetuple()))
|
||||
), # PY2 and 3 compatible
|
||||
"arn": "arn:aws:iam::{}:role/{}".format(ACCOUNT_ID, self.name),
|
||||
"resourceType": "AWS::IAM::Role",
|
||||
"resourceId": self.name,
|
||||
"resourceName": self.name,
|
||||
"awsRegion": "global",
|
||||
"availabilityZone": "Not Applicable",
|
||||
"resourceCreationTime": str(self.create_date),
|
||||
"relatedEvents": [],
|
||||
"relationships": [],
|
||||
"tags": self.tags,
|
||||
"configuration": {
|
||||
"path": self.path,
|
||||
"roleName": self.name,
|
||||
"roleId": self.id,
|
||||
"arn": "arn:aws:iam::{}:role/{}".format(ACCOUNT_ID, self.name),
|
||||
"assumeRolePolicyDocument": parse.quote(
|
||||
self.assume_role_policy_document
|
||||
)
|
||||
if self.assume_role_policy_document
|
||||
else None,
|
||||
"instanceProfileList": _instance_profiles,
|
||||
"rolePolicyList": _role_policy_list,
|
||||
"createDate": self.create_date.isoformat(),
|
||||
"attachedManagedPolicies": _managed_policies,
|
||||
"permissionsBoundary": self.permissions_boundary,
|
||||
"tags": list(
|
||||
map(
|
||||
lambda key: {"key": key, "value": self.tags[key]["Value"]},
|
||||
self.tags,
|
||||
)
|
||||
),
|
||||
"roleLastUsed": None,
|
||||
},
|
||||
"supplementaryConfiguration": {},
|
||||
}
|
||||
return config_dict
|
||||
|
||||
def put_policy(self, policy_name, policy_json):
|
||||
self.policies[policy_name] = policy_json
|
||||
|
||||
|
|
@ -590,6 +697,43 @@ class InstanceProfile(CloudFormationModel):
|
|||
return self.arn
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
def to_embedded_config_dict(self):
|
||||
# Instance Profiles aren't a config item itself, but they are returned in IAM roles with
|
||||
# a "config like" json structure It's also different than Role.to_config_dict()
|
||||
roles = []
|
||||
for role in self.roles:
|
||||
roles.append(
|
||||
{
|
||||
"path": role.path,
|
||||
"roleName": role.name,
|
||||
"roleId": role.id,
|
||||
"arn": "arn:aws:iam::{}:role/{}".format(ACCOUNT_ID, role.name),
|
||||
"createDate": str(role.create_date),
|
||||
"assumeRolePolicyDocument": parse.quote(
|
||||
role.assume_role_policy_document
|
||||
),
|
||||
"description": role.description,
|
||||
"maxSessionDuration": None,
|
||||
"permissionsBoundary": role.permissions_boundary,
|
||||
"tags": list(
|
||||
map(
|
||||
lambda key: {"key": key, "value": role.tags[key]["Value"]},
|
||||
role.tags,
|
||||
)
|
||||
),
|
||||
"roleLastUsed": None,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"path": self.path,
|
||||
"instanceProfileName": self.name,
|
||||
"instanceProfileId": self.id,
|
||||
"arn": "arn:aws:iam::{}:instance-profile/{}".format(ACCOUNT_ID, self.name),
|
||||
"createDate": str(self.create_date),
|
||||
"roles": roles,
|
||||
}
|
||||
|
||||
|
||||
class Certificate(BaseModel):
|
||||
def __init__(self, cert_name, cert_body, private_key, cert_chain=None, path=None):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue