content: # Requires pandas, bs4, html5lib, and lxml
  #
  # Call script with the output from aws_resource_actions callback, e.g.
  # python build_iam_policy_framework.py ['ec2:AuthorizeSecurityGroupEgress', 'ec2:AuthorizeSecurityGroupIngress', 'sts:GetCallerIdentity']
  #
  # The sample output:
  # {
  #     "Version": "2012-10-17",
  #     "Statement": [
  #         {
  #             "Sid": "AnsibleEditor0",
  #             "Effect": "Allow",
  #             "Action": [
  #                 "ec2:AuthorizeSecurityGroupEgress",
  #                 "ec2:AuthorizeSecurityGroupIngress"
  #             ],
  #             "Resource": "arn:aws:ec2:${Region}:${Account}:security-group/${SecurityGroupId}"
  #         },
  #         {
  #             "Sid": "AnsibleEditor1",
  #             "Effect": "Allow",
  #             "Action": [
  #                 "sts:GetCallerIdentity"
  #             ],
  #             "Resource": "*"
  #         }
  #     ]
  # }
  #
  # Policy troubleshooting:
  #    - If there are more actions in the policy than you provided, AWS has documented dependencies for some of your actions and
  #      those have been added to the policy.
  #    - If there are fewer actions in the policy than you provided, some of your actions are not in the IAM table of actions for
  #      that service. For example, the API call s3:DeleteObjects does not actually correlate to the permission needed in a policy.
  #      In this case s3:DeleteObject is the permission required to allow both the s3:DeleteObjects action and the s3:DeleteObject action.
  #    - The policies output are only as accurate as the AWS documentation. If the policy does not permit the
  #      necessary actions, look for undocumented dependencies. For example, redshift:CreateCluster requires ec2:DescribeVpcs,
  #      ec2:DescribeSubnets, ec2:DescribeSecurityGroups, and ec2:DescribeInternetGateways, but AWS does not document this.
  #
  
  import json
  import requests
  import sys
  
  missing_dependencies = []
  try:
      import pandas as pd
  except ImportError:
      missing_dependencies.append('pandas')
  try:
      import bs4
  except ImportError:
      missing_dependencies.append('bs4')
  try:
      import html5lib
  except ImportError:
      missing_dependencies.append('html5lib')
  try:
      import lxml
  except ImportError:
      missing_dependencies.append('lxml')
  
  
  irregular_service_names = {
      'a4b': 'alexaforbusiness',
      'appstream': 'appstream2.0',
      'acm': 'certificatemanager',
      'acm-pca': 'certificatemanagerprivatecertificateauthority',
      'aws-marketplace-management': 'marketplacemanagementportal',
      'ce': 'costexplorerservice',
      'cognito-identity': 'cognitoidentity',
      'cognito-sync': 'cognitosync',
      'cognito-idp': 'cognitouserpools',
      'cur': 'costandusagereport',
      'dax': 'dynamodbacceleratordax',
      'dlm': 'datalifecyclemanager',
      'dms': 'databasemigrationservice',
      'ds': 'directoryservice',
      'ec2messages': 'messagedeliveryservice',
      'ecr': 'ec2containerregistry',
      'ecs': 'elasticcontainerservice',
      'eks': 'elasticcontainerserviceforkubernetes',
      'efs': 'elasticfilesystem',
      'es': 'elasticsearchservice',
      'events': 'cloudwatchevents',
      'firehose': 'kinesisfirehose',
      'fms': 'firewallmanager',
      'health': 'healthapisandnotifications',
      'importexport': 'importexportdiskservice',
      'iot1click': 'iot1-click',
      'kafka': 'managedstreamingforkafka',
      'kinesisvideo': 'kinesisvideostreams',
      'kms': 'keymanagementservice',
      'license-manager': 'licensemanager',
      'logs': 'cloudwatchlogs',
      'opsworks-cm': 'opsworksconfigurationmanagement',
      'mediaconnect': 'elementalmediaconnect',
      'mediaconvert': 'elementalmediaconvert',
      'medialive': 'elementalmedialive',
      'mediapackage': 'elementalmediapackage',
      'mediastore': 'elementalmediastore',
      'mgh': 'migrationhub',
      'mobiletargeting': 'pinpoint',
      'pi': 'performanceinsights',
      'pricing': 'pricelist',
      'ram': 'resourceaccessmanager',
      'resource-groups': 'resourcegroups',
      'sdb': 'simpledb',
      'servicediscovery': 'cloudmap',
      'serverlessrepo': 'serverlessapplicationrepository',
      'sms': 'servermigrationservice',
      'sms-voice': 'pinpointsmsandvoiceservice',
      'sso-directory': 'ssodirectory',
      'ssm': 'systemsmanager',
      'ssmmessages': 'sessionmanagermessagegatewayservice',
      'states': 'stepfunctions',
      'sts': 'securitytokenservice',
      'swf': 'simpleworkflowservice',
      'tag': 'resourcegrouptaggingapi',
      'transfer': 'transferforsftp',
      'waf-regional': 'wafregional',
      'wam': 'workspacesapplicationmanager',
      'xray': 'x-ray'
  }
  
  irregular_service_links = {
      'apigateway': [
          'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_manageamazonapigateway.html'
      ],
      'aws-marketplace': [
          'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awsmarketplace.html',
          'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awsmarketplacemeteringservice.html',
          'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_awsprivatemarketplace.html'
      ],
      'discovery': [
          'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_applicationdiscovery.html'
      ],
      'elasticloadbalancing': [
          'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_elasticloadbalancing.html',
          'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_elasticloadbalancingv2.html'
      ],
      'globalaccelerator': [
          'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_globalaccelerator.html'
      ]
  }
  
  
  def get_docs_by_prefix(prefix):
      amazon_link_form = 'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazon{0}.html'
      aws_link_form = 'https://docs.aws.amazon.com/IAM/latest/UserGuide/list_aws{0}.html'
  
      if prefix in irregular_service_links:
          links = irregular_service_links[prefix]
      else:
          if prefix in irregular_service_names:
              prefix = irregular_service_names[prefix]
          links = [amazon_link_form.format(prefix), aws_link_form.format(prefix)]
  
      return links
  
  
  def get_html(links):
      html_list = []
      for link in links:
          html = requests.get(link).content
          try:
              parsed_html = pd.read_html(html)
              html_list.append(parsed_html)
          except ValueError as e:
              if 'No tables found' in str(e):
                  pass
              else:
                  raise e
  
      return html_list
  
  
  def get_tables(service):
      links = get_docs_by_prefix(service)
      html_list = get_html(links)
      action_tables = []
      arn_tables = []
      for df_list in html_list:
          for df in df_list:
              table = json.loads(df.to_json(orient='split'))
              table_data = table['data'][0]
              if 'Actions' in table_data and 'Resource Types (*required)' in table_data:
                  action_tables.append(table['data'][1::])
              elif 'Resource Types' in table_data and 'ARN' in table_data:
                  arn_tables.append(table['data'][1::])
  
      # Action table indices:
      # 0: Action, 1: Description, 2: Access level, 3: Resource type, 4: Condition keys, 5: Dependent actions
      # ARN tables indices:
      # 0: Resource type, 1: ARN template, 2: Condition keys
      return action_tables, arn_tables
  
  
  def add_dependent_action(resources, dependency):
      resource, action = dependency.split(':')
      if resource in resources:
          resources[resource].append(action)
      else:
          resources[resource] = [action]
      return resources
  
  
  def get_dependent_actions(resources):
      for service in dict(resources):
          action_tables, arn_tables = get_tables(service)
          for found_action_table in action_tables:
              for action_stuff in found_action_table:
                  if action_stuff is None:
                      continue
                  if action_stuff[0] in resources[service] and action_stuff[5]:
                      dependencies = action_stuff[5].split()
                      if isinstance(dependencies, list):
                          for dependency in dependencies:
                              resources = add_dependent_action(resources, dependency)
                      else:
                          resources = add_dependent_action(resources, dependencies)
      return resources
  
  
  def get_actions_by_service(resources):
      service_action_dict = {}
      dependencies = {}
      for service in resources:
          action_tables, arn_tables = get_tables(service)
  
          # Create dict of the resource type to the corresponding ARN
          arn_dict = {}
          for found_arn_table in arn_tables:
              for arn_stuff in found_arn_table:
                  arn_dict["{0}*".format(arn_stuff[0])] = arn_stuff[1]
  
          # Create dict of the action to the corresponding ARN
          action_dict = {}
          for found_action_table in action_tables:
              for action_stuff in found_action_table:
                  if action_stuff[0] is None:
                      continue
                  if arn_dict.get(action_stuff[3]):
                      action_dict[action_stuff[0]] = arn_dict[action_stuff[3]]
                  else:
                      action_dict[action_stuff[0]] = None
          service_action_dict[service] = action_dict
      return service_action_dict
  
  
  def get_resource_arns(aws_actions, action_dict):
      resource_arns = {}
      for resource_action in aws_actions:
          resource, action = resource_action.split(':')
          if action not in action_dict:
              continue
          if action_dict[action] is None:
              resource = "*"
          else:
              resource = action_dict[action].replace("${Partition}", "aws")
          if resource not in resource_arns:
              resource_arns[resource] = []
          resource_arns[resource].append(resource_action)
      return resource_arns
  
  
  def get_resources(actions):
      resources = {}
      for action in actions:
          resource, action = action.split(':')
          if resource not in resources:
              resources[resource] = []
          resources[resource].append(action)
      return resources
  
  
  def combine_arn_actions(resources, service_action_arn_dict):
      arn_actions = {}
      for service in service_action_arn_dict:
          service_arn_actions = get_resource_arns(aws_actions, service_action_arn_dict[service])
          for resource in service_arn_actions:
              if resource in arn_actions:
                  arn_actions[resource].extend(service_arn_actions[resource])
              else:
                  arn_actions[resource] = service_arn_actions[resource]
      return arn_actions
  
  
  def combine_actions_and_dependent_actions(resources):
      aws_actions = []
      for resource in resources:
          for action in resources[resource]:
              aws_actions.append('{0}:{1}'.format(resource, action))
      return set(aws_actions)
  
  
  def get_actions_restricted_by_arn(aws_actions):
      resources = get_resources(aws_actions)
      resources = get_dependent_actions(resources)
      service_action_arn_dict = get_actions_by_service(resources)
      aws_actions = combine_actions_and_dependent_actions(resources)
      return combine_arn_actions(aws_actions, service_action_arn_dict)
  
  
  def main(aws_actions):
      arn_actions = get_actions_restricted_by_arn(aws_actions)
      statement = []
      for resource_restriction in arn_actions:
          statement.append({
              "Sid": "AnsibleEditor{0}".format(len(statement)),
              "Effect": "Allow",
              "Action": arn_actions[resource_restriction],
              "Resource": resource_restriction
          })
  
      policy = {"Version": "2012-10-17", "Statement": statement}
      print(json.dumps(policy, indent=4))
  
  
  if __name__ == '__main__':
      if missing_dependencies:
          sys.exit('Missing Python libraries: {0}'.format(', '.join(missing_dependencies)))
      actions = sys.argv[1:]
      if len(actions) == 1:
          actions = sys.argv[1].split(',')
      aws_actions = [action.strip('[], "\'') for action in actions]
      main(aws_actions)
  