class MU::Cloud::AWS::Role

A user as configured in {MU::Config::BasketofKittens::roles}

Public Class Methods

cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, flags: {}) click to toggle source

Remove all roles associated with the currently loaded deployment. @param noop [Boolean]: If true, will only print what would be done @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server @return [void]

# File modules/mu/providers/aws/role.rb, line 444
def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, flags: {})

  resp = MU::Cloud::AWS.iam(credentials: credentials).list_policies(
    path_prefix: "/"+deploy_id+"/"
  )
  if resp and resp.policies
    resp.policies.each { |policy|
      MU.log "Deleting IAM policy /#{deploy_id}/#{policy.policy_name}"
      if !noop
        purgePolicy(policy.arn, credentials)
      end
    }
  end

  deleteme = []
  roles = MU::Cloud::AWS::Role.find(credentials: credentials).values
  roles.each { |r|
    next if !r.respond_to?(:role_name)
    if r.path.match(/^\/#{Regexp.quote(deploy_id)}/)
      deleteme << r
      next
    end
    # For some dumb reason, the list output that .find gets doesn't
    # include the tags, so we need to fetch each role individually to
    # check tags. Hardly seems efficient.
    desc = begin
      MU::Cloud::AWS.iam(credentials: credentials).get_role(role_name: r.role_name)
    rescue Aws::IAM::Errors::NoSuchEntity
      next
    end
    if desc.role and desc.role.tags and desc.role.tags
      master_match = false
      deploy_match = false
      desc.role.tags.each { |t|
        if t.key == "MU-ID" and t.value == deploy_id
          deploy_match = true
        elsif t.key == "MU-MASTER-IP" and t.value == MU.mu_public_ip
          master_match = true
        end
      }
      if deploy_match and (master_match or ignoremaster)
        deleteme << r
      end
    end
  }

  if flags and flags["known"]
    roles = MU::Cloud::AWS::Role.find(credentials: credentials).values
    roles.each { |r|
      next if !r.respond_to?(:role_name)
      deleteme << r if flags["known"].include?(r.role_name)
    }
    deleteme.uniq!
  end
  deleteme.reject! { |r| r.class.name == "Aws::IAM::Types::Policy" }

  if deleteme.size > 0
    deleteme.each { |r|
      MU.log "Deleting IAM role #{r.role_name}"
      if !noop
        # purgePolicy won't touch roles we don't own, so gently detach
        # those first
        detachables = MU::Cloud::AWS.iam(credentials: credentials).list_attached_role_policies(
          role_name: r.role_name
        ).attached_policies
        detachables.each { |rp|
          MU::Cloud::AWS.iam(credentials: credentials).detach_role_policy(
            role_name: r.role_name,
            policy_arn: rp.policy_arn
          )
        }

        begin
          MU::Cloud::AWS.iam(credentials: credentials).remove_role_from_instance_profile(
            instance_profile_name: r.role_name,
            role_name: r.role_name
          )
          MU::Cloud::AWS.iam(credentials: credentials).delete_instance_profile(instance_profile_name: r.role_name)
        rescue Aws::IAM::Errors::ValidationError => e
          MU.log "Cleaning up IAM role #{r.role_name}: #{e.inspect}", MU::WARN
        rescue Aws::IAM::Errors::NoSuchEntity
        end

        MU::Cloud::AWS.iam(credentials: credentials).delete_role(
          role_name: r.role_name
        )
      end
    }
  end

end
condition_schema() click to toggle source

Schema fragment for IAM policy conditions, which some other resource types may need to import.

# File modules/mu/providers/aws/role.rb, line 916
def self.condition_schema
  {
    "type" => "array",
    "items" => {
      "properties" => {
        "conditions" => {
          "type" => "array",
          "items" => {
            "type" => "object",
            "description" => "One or more conditions under which to apply this policy. See also: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html",
            "required" => ["comparison", "variable", "values"],
            "properties" => {
              "comparison" => {
                "type" => "string",
                "description" => "A comparison to make, like +DateGreaterThan+ or +IpAddress+."
              },
              "variable" => {
                "type" => "string",
                "description" => "The variable which we will compare, like +aws:CurrentTime+ or +aws:SourceIp+."
              },
              "values" => {
                "type" => "array",
                "items" => {
                  "type" => "string",
                  "description" => "Value(s) to which we will compare our variable, like +2013-08-16T15:00:00Z+ or +192.0.2.0/24+."
                }
              }
            }
          }
        }
      }
    }
  }
end
doc2MuPolicies(basename, doc, policies = []) click to toggle source

Convert an IAM policy document to our own shorthand Basket of Kittens schema. @param doc [Hash]: The decoded IAM policy document @param policies [Array<Hash>]: Existing policy list to append to, if any @return [Array<Hash>]

# File modules/mu/providers/aws/role.rb, line 720
        def self.doc2MuPolicies(basename, doc, policies = [])
          policies ||= []

          if !doc["Statement"].is_a?(Array)
            doc["Statement"] = [doc["Statement"]]
          end

          doc["Statement"].each { |s|
            if !s["Action"]
              MU.log "Statement in policy document for #{basename} didn't have an Action field", MU::WARN, details: doc
              next
            end
            s["Resource"] = [s["Resource"]] if s["Resource"].is_a?(String)
            s["Action"] = [s["Action"]] if s["Action"].is_a?(String)
            conditions = nil
            if s["Condition"]
              conditions = []
              s["Condition"].each_pair { |comparison, relation|
                relation.each_pair { |variable, values|
                  values = [values] if !values.is_a?(Array)
                  conditions << {
                    "comparison" => comparison,
                    "variable" => variable,
                    "values" => values
                  }
                }
              }
            end
            policy = {
              "name" => basename + (doc["Statement"].size > 1 ? "_"+policies.size.to_s : ""),
              "permissions" => s["Action"],
              "flag" => s["Effect"].downcase,
              "targets" => s["Resource"].map { |r|
                if r.match(/^arn:aws(-us-gov)?:([^:]+):.*?:([^:]*)$/)
# XXX which cases even count for blind references to sibling resources?
                  if Regexp.last_match[1] == "s3"
                    "bucket"
                  elsif Regexp.last_match[1]
                    MU.log "Service #{Regexp.last_match[1]} to type...", MU::WARN, details: r
                    nil
                  end
                end
                {
                  "identifier" => r
                }
              }
            }
            policy["conditions"] = conditions if conditions
            policies << policy
          }
          policies
        end
find(**args) click to toggle source

Locate an existing user group. @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching user group.

# File modules/mu/providers/aws/role.rb, line 538
def self.find(**args)
  found = {}

  if args[:cloud_id]

    begin
      # managed policies get fetched by ARN, roles by plain name. Ok!
      if args[:cloud_id].match(/^arn:.*?:policy\//)
        resp = MU::Cloud::AWS.iam(credentials: args[:credentials]).get_policy(
          policy_arn: args[:cloud_id]
        )
        if resp and resp.policy
          found[args[:cloud_id]] = resp.policy
        end
      else
        resp = MU::Cloud::AWS.iam(credentials: args[:credentials]).get_role(
          role_name: args[:cloud_id].sub(/^arn:.*?\/([^:\/]+)$/, '\1') # XXX if it's an ARN, actually parse it and look in the correct account when applicable
        )

        if resp and resp.role
          found[resp.role.role_name] = resp.role
        end
      end
    rescue ::Aws::IAM::Errors::NoSuchEntity
    end

  else
    resp = MU::Cloud::AWS.iam(credentials: args[:credentials]).list_roles
    resp.roles.each { |role|
      found[role.role_name] = role
    }

    resp = MU::Cloud::AWS.iam(credentials: args[:credentials]).list_policies(scope: "Local")
    resp.policies.each { |pol|
      found[pol.arn] = pol
    }
  end

  found
end
genPolicyDocument(policies, deploy_obj: nil, bucket_style: false, version: "2012-10-17", doc_id: nil) click to toggle source

Convert our generic internal representation of access policies into structures suitable for AWS IAM policy documents. Let's return a single policy with a bunch of statements, though, instead of a shedload of individual policies. @param policies [Array<Hash>]: One or more policy chunks @param deploy_obj [MU::MommaCat]: Deployment object to use when looking up sibling Mu resources @return [Array<Hash>]

# File modules/mu/providers/aws/role.rb, line 1138
        def self.genPolicyDocument(policies, deploy_obj: nil, bucket_style: false, version: "2012-10-17", doc_id: nil)
          if policies
            name = nil
            doc = {
              "Version" => version,
              "Statement" => []
            }
            doc["Id"] = doc_id if doc_id
            policies.each { |policy|
              policy["flag"] ||= "Allow"
              statement = {
                "Sid" => policy["name"].gsub(/[^0-9A-Za-z]*/, ""),
                "Effect" => policy['flag'].capitalize,
                "Action" => [],
                "Resource" => []
              }
              name ||= statement["Sid"]
              policy["permissions"].each { |perm|
                statement["Action"] << perm
              }
              if policy["conditions"]
                statement["Condition"] ||= {}
                policy["conditions"].each { |cond|
                  statement["Condition"][cond['comparison']] = {
                    cond["variable"] => cond["values"]
                  }
                }
              end
              if policy["grant_to"] # XXX factor this with target, they're too similar
                statement["Principal"] ||= []
                policy["grant_to"].each { |grantee|
                  grantee["identifier"] ||= grantee["id"]
                  if grantee["type"] and deploy_obj
                    sibling = deploy_obj.findLitterMate(
                      name: grantee["identifier"],
                      type: grantee["type"]
                    )
                    if sibling
                      id = sibling.cloudobj.arn
                      if bucket_style
                        statement["Principal"] << { "AWS" => id }
                      else
                        statement["Principal"] << id
                      end
                    else
                      raise MuError, "Couldn't find a #{grantee["type"]} named #{grantee["identifier"]} when generating IAM policy"
                    end
                  else
                    bucket_prefix = if grantee["identifier"].match(/^[^\.]+\.amazonaws\.com$/)
                      "Service"
                    elsif grantee["identifier"] =~ /^[a-f0-9]+$/
                      "CanonicalUser"
                    else
                      "AWS"
                    end

                    if bucket_style
                      statement["Principal"] << { bucket_prefix => grantee["identifier"] }
                    else
                      statement["Principal"] << grantee["identifier"]
                    end
                  end
                }
                if policy["grant_to"].size == 1
                  statement["Principal"] = statement["Principal"].first
                end
              end
              if policy["targets"]
                policy["targets"].each { |target|
                  target["identifier"] ||= target["id"]
                  if target["type"] and deploy_obj
                    sibling = deploy_obj.findLitterMate(
                      name: target["identifier"],
                      type: target["type"]
                    )
                    if sibling
                      id = sibling.cloudobj.arn
                      id.sub!(/:([^:]+)$/, ":"+'\1'+target["path"]) if target["path"]
                      statement["Resource"] << id
                      if id.match(/:log-group:/)
                        stream_id = id.sub(/:([^:]+)$/, ":log-stream:*")
#                        "arn:aws:logs:us-east-2:accountID:log-group:log_group_name:log-stream:CloudTrail_log_stream_name_prefix*"
                        statement["Resource"] << stream_id
                      elsif id.match(/:s3:/)
                        statement["Resource"] << id+"/*"
                      end
                    else
                      raise MuError, "Couldn't find a #{target["type"]} named #{target["identifier"]} when generating IAM policy"
                    end
                  else
                    target["identifier"] += target["path"] if target["path"]
                    statement["Resource"] << target["identifier"]
                  end
                }
              end
              doc["Statement"] << statement
            }
            return [ { name => doc} ]
          end

          []
        end
isGlobal?() click to toggle source

Does this resource type exist as a global (cloud-wide) artifact, or is it localized to a region/zone? @return [Boolean]

# File modules/mu/providers/aws/role.rb, line 430
def self.isGlobal?
  true
end
manageRawPolicies(raw_policies, basename: "", credentials: nil, path: "/"+MU.deploy_id) click to toggle source

Take some AWS policy documents and turn them into policies @param raw_policies [Array<Hash>] @param basename [String] @param credentials [String] @param path [String] @return [Array<String>]

# File modules/mu/providers/aws/role.rb, line 152
def self.manageRawPolicies(raw_policies, basename: "", credentials: nil, path: "/"+MU.deploy_id)
  arns = []
  raw_policies.each { |policy|
    policy.values.each { |p|
      p["Version"] ||= "2012-10-17"
    }

    policy_name = basename+"-"+policy.keys.first.upcase
    arn = "arn:"+(MU::Cloud::AWS.isGovCloud? ? "aws-us-gov" : "aws")+":iam::"+MU::Cloud::AWS.credToAcct(credentials)+":policy#{path}/#{policy_name}"
    resp = begin
      desc = MU::Cloud::AWS.iam(credentials: credentials).get_policy(policy_arn: arn)

      version = MU::Cloud::AWS.iam(credentials: credentials).get_policy_version(
        policy_arn: arn,
        version_id: desc.policy.default_version_id
      )

      ext = JSON.parse(CGI.unescape(version.policy_version.document))
      if ext != policy.values.first
        # Special exception- we don't want to overwrite extra rules
        # in MuSecrets policies, because our siblings might have
        # (will have) injected those and they should stay.
        if policy.size == 1 and policy["MuSecrets"]
          if (ext["Statement"][0]["Resource"] & policy["MuSecrets"]["Statement"][0]["Resource"]).sort == policy["MuSecrets"]["Statement"][0]["Resource"].sort
            next
          end
        end
        MU.log "Updating IAM policy #{policy_name}", MU::NOTICE, details: policy
        ext.diff(policy.values.first)
        update_policy(arn, policy.values.first)
        MU::Cloud::AWS.iam(credentials: credentials).get_policy(policy_arn: arn)
      else
        desc
      end

    rescue Aws::IAM::Errors::NoSuchEntity
      MU.log "Creating IAM policy #{policy_name}", details: policy.values.first
      desc = MU::Cloud::AWS.iam(credentials: credentials).create_policy(
        policy_name: policy_name,
        path: path+"/",
        policy_document: JSON.generate(policy.values.first),
        description: "Raw policy from #{basename}"
      )
      MU.retrier([Aws::IAM::Errors::NoSuchEntity], loop_if: Proc.new { desc.nil? }) {
        desc = MU::Cloud::AWS.iam(credentials: credentials).get_policy(policy_arn: arn)
      }
      desc
    end
    arns << resp.policy.arn
  }
  arns
end
new(**args) click to toggle source

Initialize this cloud resource object. Calling super will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like +@vpc+, for us. @param args [Hash]: Hash of named arguments passed via Ruby's double-splat

Calls superclass method
# File modules/mu/providers/aws/role.rb, line 23
def initialize(**args)
  super

  if @cloud_id and (!cloud_desc["role"] or cloud_desc["role"].empty?)
    @config['bare_policies'] = true
    if @config['name'].match(/^arn:/) and cloud_desc['policies'].size == 1
      @config['name'] = cloud_desc['policies'].first.policy_name
    end
  end

  @mu_name ||= @config['scrub_mu_isms'] ? @config['name'] : @deploy.getResourceName(@config["name"], max_length: 64)
end
purgePolicy(policy_arn, credentials) click to toggle source

Delete an IAM policy, along with attendant versions and attachments. @param policy_arn [String]: The ARN of the policy to purge

# File modules/mu/providers/aws/role.rb, line 370
def self.purgePolicy(policy_arn, credentials)
  attachments = MU::Cloud::AWS.iam(credentials: credentials).list_entities_for_policy(
    policy_arn: policy_arn
  )
  attachments.policy_users.each { |u|
    begin
      MU::Cloud::AWS.iam(credentials: credentials).detach_user_policy(
        user_name: u.user_name,
        policy_arn: policy_arn
      )
    rescue ::Aws::IAM::Errors::NoSuchEntity
    end
  }
  attachments.policy_groups.each { |g|
    begin
      MU::Cloud::AWS.iam(credentials: credentials).detach_group_policy(
        group_name: g.group_name,
        policy_arn: policy_arn
      )
    rescue ::Aws::IAM::Errors::NoSuchEntity
    end
  }
  attachments.policy_roles.each { |r|
    begin
      MU::Cloud::AWS.iam(credentials: credentials).detach_role_policy(
        role_name: r.role_name,
        policy_arn: policy_arn
      )
    rescue ::Aws::IAM::Errors::NoSuchEntity
    end
  }
  versions = MU::Cloud::AWS.iam(credentials: credentials).list_policy_versions(
    policy_arn: policy_arn,
  ).versions
  versions.each { |v|
    next if v.is_default_version
    begin
      MU::Cloud::AWS.iam(credentials: credentials).delete_policy_version(
        policy_arn: policy_arn,
        version_id: v.version_id
      )
    rescue ::Aws::IAM::Errors::NoSuchEntity
    end
  }

  # Delete the policy, unless it's one of the global canned ones owned
  # by AWS
  if !policy_arn.match(/^arn:aws:iam::aws:/)
    begin
      MU::Cloud::AWS.iam(credentials: credentials).delete_policy(
        policy_arn: policy_arn
      )
    rescue ::Aws::IAM::Errors::NoSuchEntity
    end
  end
end
quality() click to toggle source

Denote whether this resource implementation is experiment, ready for testing, or ready for production use.

# File modules/mu/providers/aws/role.rb, line 436
def self.quality
  MU::Cloud::BETA
end
schema(_config) click to toggle source

Cloud-specific configuration properties. @param _config [MU::Config]: The calling MU::Config object @return [Array<Array,Hash>]: List of required fields, and json-schema Hash of cloud-specific configuration parameters for this resource

# File modules/mu/providers/aws/role.rb, line 954
        def self.schema(_config)
          toplevel_required = []
          aws_resource_types = MU::Cloud.resource_types.keys.reject { |t|
            begin
              MU::Cloud.resourceClass("AWS", t)
              false
            rescue MuCloudResourceNotImplemented
              true
            end
          }.map { |t| MU::Cloud.resource_types[t][:cfg_name] }.sort

          schema = {
            "tags" => MU::Config.tags_primitive,
            "optional_tags" => MU::Config.optional_tags_primitive,
            "policies" => MU::Cloud::AWS::Role.condition_schema,
            "import" => {
              "type" => "array",
              "items" => {
                "type" => "string",
                "description" => "DEPRECATED, use +attachable_policies+ instead. A shorthand reference to a canned IAM policy like +AdministratorAccess+, a full ARN like +arn:aws:iam::aws:policy/AmazonESCognitoAccess+."
              }
            },
            "attachable_policies" => {
              "type" => "array",
              "items" => MU::Config::Ref.schema(type: "roles", desc: "Reference to a managed policy, which can either refer to an existing managed policy or a sibling {MU::Config::BasketofKittens::roles} object which has +bare_policies+ set.", omit_fields: ["region", "tag"])
            },
            "strip_path" => {
              "type" => "boolean",
              "default" => false,
              "description" => "Normally we namespace IAM roles with a +path+ set to match our +deploy_id+; this disables that behavior. Temporary workaround for a bug in EKS/IAM integration."
            },
            "bare_policies" => {
              "type" => "boolean",
              "default" => false,
              "description" => "Do not create a role, but simply create the policies specified in +policies+ and/or +iam_policies+ for direct attachment to other entities."
            },
            "can_assume" => {
              "type" => "array",
              "items" => {
                "type" => "object",
                "description" => "Entities which are permitted to assume this role. Can be services, IAM objects, or other Mu resources.",
                "required" => ["entity_type", "entity_id"],
                "properties" => {
                  "conditions" => MU::Cloud::AWS::Role.condition_schema["items"]["properties"]["conditions"],
                  "entity_type" => {
                    "type" => "string",
                    "description" => "Type of entity which will be permitted to assume this role. See +entity_id+ for details.",
                    "enum" => ["service", "aws", "federated"]+aws_resource_types
                  },
                  "assume_method" => {
                    "type" => "string",
                    "description" => "https://docs.aws.amazon.com/STS/latest/APIReference/API_Operations.html",
                    "enum" => ["basic", "saml", "web"],
                    "default" => "basic"
                  },
                  "entity_id" => {
                    "type" => "string",
                    "description" => "An identifier appropriate for the +entity_type+ which is allowed to assume this role- see details for valid formats.\n
**service**: The name of a service which is allowed to assume this role, such as +ec2.amazonaws.com+. See also https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-service.html#roles-creatingrole-service-api. For an unofficial list of service names, see https://gist.github.com/shortjared/4c1e3fe52bdfa47522cfe5b41e5d6f22\n
**#{aws_resource_types.join(", ")}**: A resource of one of these Mu types, declared elsewhere in this stack with a name specified in +entity_id+, for which Mu will attempt to resolve the appropriate *aws* or *service* identifier.\n
**aws**: An ARN which should be permitted to assume this role, often another role like +arn:aws:iam::AWS-account-ID:role/role-name+ or a specific user session such as +arn:aws:sts::AWS-account-ID:assumed-role/role-name/role-session-name+. See also https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#Principal_specifying\n
**federated**: A federated identity provider, such as +accounts.google.com+ or +arn:aws:iam::AWS-account-ID:saml-provider/provider-name+. See also https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#Principal_specifying"
                  },
# XXX it's possible that 'role' is the only Mu resource type that maps to something that can assume another role in AWS IAM, so maybe that aws_resource_types.join should be something simpler
                }
              }
            },
            "raw_policies" => {
              "type" => "array",
              "items" => {
                "type" => "object",
                "description" => "Amazon-compatible policy documents, as YAML objects if your Basket of Kittens is written YAML, or JSON objects if in JSON. Note that +policies+ is considerably easier to use, and is recommended. For more on the raw AWS policy format, see https://docs.aws.amazon.com/IAM/latest/RoleGuide/access_policies_examples.html for example policies.",
              }
            },
            "iam_policies" => {
              "type" => "array",
              "items" => {
                "type" => "object",
                "description" => "DEPRECATED, use +raw_policies+ or +policies+ instead."
              }
            }
          }
          [toplevel_required, schema]
        end
update_policy(arn, doc, credentials: nil) click to toggle source

Update a policy, handling deletion of old versions as needed @param arn [String]: @param doc [Hash]: @param credentials [String]:

# File modules/mu/providers/aws/role.rb, line 1245
        def self.update_policy(arn, doc, credentials: nil)
# XXX this is just blindly replacing identical versions, when it should check
# and guard
          begin
            MU::Cloud::AWS.iam(credentials: credentials).create_policy_version(
              policy_arn: arn,
              set_as_default: true,
              policy_document: JSON.generate(doc)
            )
          rescue Aws::IAM::Errors::LimitExceeded
            delete_version = MU::Cloud::AWS.iam(credentials: credentials).list_policy_versions(
              policy_arn: arn,
            ).versions.last.version_id
            MU.log "Purging oldest version (#{delete_version}) of IAM policy #{arn}", MU::NOTICE
            MU::Cloud::AWS.iam(credentials: credentials).delete_policy_version(
              policy_arn: arn,
              version_id: delete_version
            )
            retry
          end
        end
validateAttachablePolicies(attachables, credentials: nil, region: MU.curRegion) click to toggle source

Verify that managed policies from attachable_policies actually exist. @param attachables [Array<Hash>] @param credentials [String] @param region [String]

# File modules/mu/providers/aws/role.rb, line 1044
        def self.validateAttachablePolicies(attachables, credentials: nil, region: MU.curRegion)
          ok = true
          return ok if !attachables

          attachables.each { |ref|
            next if !ref["id"]
# XXX search our account too
            arn = if !ref["id"].match(/^arn:/i)
              "arn:"+(MU::Cloud::AWS.isGovCloud?(region) ? "aws-us-gov" : "aws")+":iam::aws:policy/"+ref["id"]
            else
              ref["id"]
            end
            subpaths = ["service-role", "aws-service-role", "job-function"]
            begin
              MU::Cloud::AWS.iam(credentials: credentials).get_policy(policy_arn: arn)
            rescue Aws::IAM::Errors::NoSuchEntity
              if subpaths.size > 0
                arn = "arn:"+(MU::Cloud::AWS.isGovCloud?(region) ? "aws-us-gov" : "aws")+":iam::aws:policy/#{subpaths.shift}/"+ref["id"]
                retry
              end
              MU.log "No such canned AWS IAM policy '#{arn}'", MU::ERR
              ok = false
            end
            ref["id"] = arn
          }

          ok
        end
validateConfig(role, _configurator) click to toggle source

Cloud-specific pre-processing of {MU::Config::BasketofKittens::roles}, bare and unvalidated. @param role [Hash]: The resource to process and validate @param _configurator [MU::Config]: The overall deployment configurator of which this resource is a member @return [Boolean]: True if validation succeeded, False otherwise

# File modules/mu/providers/aws/role.rb, line 1077
def self.validateConfig(role, _configurator)
  ok = true

  # munge things declared with the deprecated import keyword into
  # attachable_policies where they belong
  if role['import']
    role['attachable_policies'] ||= []
    role['import'].each { |policy|
      role['attachable_policies'] << { "id" => policy }
    }
    role.delete("import")
  end

  role['strip_path'] = true if role['scrub_mu_isms']

  # If we're attaching some managed policies, make sure all of the ones
  # that should already exist do indeed exist
  if role['attachable_policies']
    ok = false if !self.validateAttachablePolicies(
      role['attachable_policies'],
      credentials: role['credentials'],
      region: role['region']
    )
  end

  if role['iam_policies'] and !role['iam_policies'].empty?
    role['raw_policies'] = Marshal.load(Marshal.dump(role['iam_policies']))
    role.delete('iam_policies')
  end

  if role["bare_policies"] and (!role["raw_policies"] or role["raw_policies"].empty?) and (!role["policies"] or role["policies"].empty?)
    MU.log "IAM role #{role['name']} has bare_policies set, but no policies or raw_policies were specified", MU::ERR
    ok = false
  end

  if (!role['can_assume'] or role['can_assume'].empty?) and
     !role["bare_policies"]
    MU.log "IAM role #{role['name']} must specify at least one can_assume entry", MU::ERR
    ok = false
  end

  if role['policies']
    role['policies'].each { |policy|
      policy['targets'].each { |target|
        if target['type']
          MU::Config.addDependency(role, target['identifier'], target['type'], my_phase: "groom")
        end
      }
    }
  end

  ok
end

Public Instance Methods

arn() click to toggle source

Canonical Amazon Resource Number for this resource @return [String]

# File modules/mu/providers/aws/role.rb, line 207
def arn
  desc = cloud_desc
  if desc["role"]
    if desc['role'].is_a?(Hash)
      desc["role"][:arn] # why though
    else
      desc["role"].arn
    end
  else
    nil
  end
end
bindTo(entitytype, entityname) click to toggle source

Attach this role or group of loose policies to the specified entity. @param entitytype [String]: The type of entity (user, group or role for policies; instance_profile for roles)

# File modules/mu/providers/aws/role.rb, line 775
def bindTo(entitytype, entityname)
  if entitytype == "instance_profile"
    begin
      resp = MU::Cloud::AWS.iam(credentials: @credentials).get_instance_profile(
        instance_profile_name: entityname
      ).instance_profile

      if !resp.roles.map { |r| r.role_name}.include?(@mu_name)
        MU::Cloud::AWS.iam(credentials: @credentials).add_role_to_instance_profile(
          instance_profile_name: entityname,
          role_name: @mu_name
        )
      end
    rescue StandardError => e
      MU.log "Error binding role #{@mu_name} to instance profile #{entityname}: #{e.message}", MU::ERR
      raise e
    end
  elsif ["user", "group", "role"].include?(entitytype)
    mypolicies = MU::Cloud::AWS.iam(credentials: @credentials).list_policies(
      path_prefix: "/"+@deploy.deploy_id+"/"
    ).policies
    mypolicies.reject! { |p|
      !p.policy_name.match(/^#{Regexp.quote(@mu_name)}(-|$)/)
    }

    if @config['attachable_policies']
      @config['attachable_policies'].each { |policy_hash|
        policy = policy_hash["id"]
        p_arn = if !policy.match(/^arn:/i)
          "arn:"+(MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws")+":iam::aws:policy/"+policy
        else
          policy
        end

        subpaths = ["service-role", "aws-service-role", "job-function"]
        begin
          mypolicies << MU::Cloud::AWS.iam(credentials: @credentials).get_policy(
            policy_arn: p_arn
          ).policy
        rescue Aws::IAM::Errors::NoSuchEntity => e
          if subpaths.size > 0
            p_arn = "arn:"+(MU::Cloud::AWS.isGovCloud?(@region) ? "aws-us-gov" : "aws")+":iam::aws:policy/#{subpaths.shift}/"+policy
            retry
          end
          raise e
        end
      }
    end

    if @config['raw_policies']
      raw_arns = MU::Cloud::AWS::Role.manageRawPolicies(
        @config['raw_policies'],
        basename: @deploy.getResourceName(@config['name']),
        credentials: @credentials
      )
      raw_arns.each { |p_arn|
        mypolicies << MU::Cloud::AWS.iam(credentials: @credentials).get_policy(
          policy_arn: p_arn
        ).policy
      }
    end

    mypolicies.each { |p|
      if entitytype == "user"
        resp = MU::Cloud::AWS.iam(credentials: @credentials).list_attached_user_policies(
          path_prefix: "/"+@deploy.deploy_id+"/",
          user_name: entityname
        )
        if !resp or !resp.attached_policies.map { |a_p| a_p.policy_name }.include?(p.policy_name)
          MU.log "Attaching IAM policy #{p.policy_name} to user #{entityname}", MU::NOTICE
          MU::Cloud::AWS.iam(credentials: @credentials).attach_user_policy(
            policy_arn: p.arn,
            user_name: entityname
          )
        end
      elsif entitytype == "group"
        resp = MU::Cloud::AWS.iam(credentials: @credentials).list_attached_group_policies(
          path_prefix: "/"+@deploy.deploy_id+"/",
          group_name: entityname
        )
        if !resp or !resp.attached_policies.map { |a_p| a_p.policy_name }.include?(p.policy_name)
          MU.log "Attaching policy #{p.policy_name} to group #{entityname}", MU::NOTICE
          MU::Cloud::AWS.iam(credentials: @credentials).attach_group_policy(
            policy_arn: p.arn,
            group_name: entityname
          )
        end
      elsif entitytype == "role"
        resp = MU::Cloud::AWS.iam(credentials: @credentials).list_attached_role_policies(
          role_name: entityname
        )

        if !resp or !resp.attached_policies.map { |a_p| a_p.policy_name }.include?(p.policy_name)
          MU.log "Attaching policy #{p.policy_name} to role #{entityname}", MU::NOTICE
          MU::Cloud::AWS.iam(credentials: @credentials).attach_role_policy(
            policy_arn: p.arn,
            role_name: entityname
          )
        end
      end
    }
  else
    raise MuError, "Invalid entitytype '#{entitytype}' passed to MU::Cloud::AWS::Role.bindTo. Must be be one of: user, group, role, instance_profile"
  end
  cloud_desc(use_cache: false)
end
cloud_desc(use_cache: true) click to toggle source

Return a hash containing a role element and a policies element, populated with one or both depending on what this resource has defined.

# File modules/mu/providers/aws/role.rb, line 224
        def cloud_desc(use_cache: true)
          require 'aws-sdk-iam'

          # we might inherit a naive cached description from the base cloud
          # layer; rearrange it to our tastes
          if @cloud_desc_cache.is_a?(::Aws::IAM::Types::Role)
            new_desc = {
              "role" => @cloud_desc_cache
            }
            @cloud_desc_cache = new_desc
          elsif @cloud_desc_cache.is_a?(::Aws::IAM::Types::Policy)
            new_desc = {
              "policies" => [@cloud_desc_cache]
            }
            @cloud_desc_cache = new_desc
          end

          return @cloud_desc_cache if @cloud_desc_cache and !@cloud_desc_cache.empty? and use_cache

          @cloud_desc_cache = {}
          if @config['bare_policies']
            if @cloud_id
              pol_desc = MU::Cloud::AWS::Role.find(credentials: @credentials, cloud_id: @cloud_id).values.first
              if pol_desc
                @cloud_desc_cache['policies'] = [pol_desc]
                return @cloud_desc_cache
              end
            end

            if @deploy and @deploy.deploy_id
              @cloud_desc_cache["policies"] = MU::Cloud::AWS.iam(credentials: @credentials).list_policies(
                path_prefix: "/"+@deploy.deploy_id+"/"
              ).policies
              @cloud_desc_cache["policies"].reject! { |p|
                !p.policy_name.match(/^#{Regexp.quote(@mu_name)}-/)
              }
              # this is quasi-wrong because we can be mulitple cloud is, but
              # we can't really set this type to has_multiples because that's
              # just how managed policies work not anything else, goddammit
              # AWS why can't you just bundle everything in roles
              if @cloud_desc_cache["policies"] and @cloud_desc_cache["policies"].size > 0
                @cloud_id ||= @cloud_desc_cache["policies"].first.arn
              end
            end
          else
            if @cloud_id.match(/^arn:aws(:?-us-gov)?:[^:]*:[^:]*:\d*:policy\//)
              pol_desc = MU::Cloud::AWS::Role.find(credentials: @credentials, cloud_id: @cloud_id).values.first
              if pol_desc
                @cloud_desc_cache['policies'] = [pol_desc]
                return @cloud_desc_cache
              end
            end
begin
            @cloud_desc_cache['role'] = MU::Cloud::AWS::Role.find(credentials: @credentials, cloud_id: @cloud_id).values.first
            @cloud_desc_cache['role'] ||= MU::Cloud::AWS::Role.find(credentials: @credentials, cloud_id: @mu_name).values.first
            MU::Cloud::AWS.iam(credentials: @credentials).list_attached_role_policies(
              role_name: @mu_name
            ).attached_policies.each { |p|
              @cloud_desc_cache["policies"] ||= []
              @cloud_desc_cache["policies"] << MU::Cloud::AWS.iam(credentials: @credentials).get_policy(
                policy_arn: p.policy_arn
              ).policy
            }

            inline = MU::Cloud::AWS.iam(credentials: @credentials).list_role_policies(role_name: @mu_name).policy_names
            inline.each { |pol_name|
              @cloud_desc_cache["policies"] ||= []
              @cloud_desc_cache["policies"] << MU::Cloud::AWS.iam(credentials: @credentials).get_role_policy(
                role_name: @mu_name,
                policy_name: pol_name
              )
            }

rescue ::Aws::IAM::Errors::ValidationError => e
MU.log @cloud_id+" "+@mu_name, MU::WARN, details: e.inspect
end
          end
          @cloud_desc_cache['cloud_id'] ||= @cloud_id

          @cloud_desc_cache
        end
create() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/aws/role.rb, line 37
def create
  if @config['raw_policies']
    @config['raw_policies'].each { |policy|
      policy.values.each { |p|
        p["Version"] ||= "2012-10-17"
      }

      policy_name = @mu_name+"-"+policy.keys.first.upcase
      MU.log "Creating IAM policy #{policy_name}"
      MU::Cloud::AWS.iam(credentials: @credentials).create_policy(
        policy_name: policy_name,
        path: "/"+@deploy.deploy_id+"/",
        policy_document: JSON.generate(policy.values.first),
        description: "Generated from inline policy document for Mu role #{@mu_name}"
      )
    }
  end

  if !@config['bare_policies']
    @cloud_id = @mu_name
    path = @config['strip_path'] ? nil : "/"+@deploy.deploy_id+"/"
    params = {
      :path => path,
      :role_name => @mu_name,
      :description => "Generated by Mu",
      :assume_role_policy_document => gen_assume_role_policy_doc,
      :tags => get_tag_params(@config['scrub_mu_isms'])
    }

    MU.log "Creating IAM role #{@mu_name} (#{@credentials})", details: params
    MU::Cloud::AWS.iam(credentials: @credentials).create_role(params)
  end
end
createInstanceProfile() click to toggle source

Create an instance profile for EC2 instances, named identically and bound to this role.

# File modules/mu/providers/aws/role.rb, line 884
def createInstanceProfile
  if @config['bare_policies']
    raise MuError, "#{self} has 'bare_policies' set, cannot create an instance profile without a role to bind"
  end

  resp = begin
    MU.log "Creating instance profile #{@mu_name} #{@credentials}"
    MU::Cloud::AWS.iam(credentials: @credentials).create_instance_profile(
      instance_profile_name: @mu_name
    )
  rescue Aws::IAM::Errors::EntityAlreadyExists
    MU::Cloud::AWS.iam(credentials: @credentials).get_instance_profile(
      instance_profile_name: @mu_name
    )
  end

  # make sure it's really there before moving on
  begin
    MU::Cloud::AWS.iam(credentials: @credentials).get_instance_profile(instance_profile_name: @mu_name)
  rescue Aws::IAM::Errors::NoSuchEntity => e
    MU.log e.inspect, MU::WARN
    sleep 10
    retry
  end

  bindTo("instance_profile", @mu_name)

  resp.instance_profile.arn
end
groom() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/aws/role.rb, line 72
def groom
  if @config['policies']
    @config['raw_policies'] ||= []

    @config['raw_policies'].concat(convert_policies_to_iam)
  end

  if !@config['bare_policies']
    resp = MU::Cloud::AWS.iam(credentials: @credentials).get_role(
      role_name: @mu_name
    ).role
    ext_tags = resp.tags.map { |t| t.to_h }
    tag_param = get_tag_params(true)
    tag_param.reject! { |t| ext_tags.include?(t) }

    if tag_param.size > 0
      MU.log "Updating tags on IAM role #{@mu_name}", MU::NOTICE, details: tag_param
      MU::Cloud::AWS.iam(credentials: @credentials).tag_role(role_name: @mu_name, tags: tag_param)
    end
  end

  if @config['raw_policies'] or @config['attachable_policies']
    configured_policies = []

    if @config['raw_policies']
      MU.log "Attaching #{@config['raw_policies'].size.to_s} raw #{@config['raw_policies'].size > 1 ? "policies" : "policy"} to role #{@mu_name}", MU::NOTICE
      configured_policies = @config['raw_policies'].map { |p|
        @mu_name+"-"+p.keys.first.upcase
      }
    end

    if @config['attachable_policies']
      MU.log "Attaching #{@config['attachable_policies'].size.to_s} external #{@config['attachable_policies'].size > 1 ? "policies" : "policy"} to role #{@mu_name}", MU::NOTICE
      configured_policies.concat(@config['attachable_policies'].map { |p|
        id = if p.is_a?(MU::Config::Ref)
          p.cloud_id
        else
          p = MU::Config::Ref.get(p)
          p.kitten
          p.cloud_id
        end
        id.gsub(/.*?\/([^:\/]+)$/, '\1')
      })
    end

    # Purge anything that doesn't belong
    if !@config['bare_policies']
      attached_policies = MU::Cloud::AWS.iam(credentials: @credentials).list_attached_role_policies(
        role_name: @mu_name
      ).attached_policies
      attached_policies.each { |a|
        if !configured_policies.include?(a.policy_name)
          MU.log "Removing IAM policy #{a.policy_name} from role #{@mu_name}", MU::NOTICE, details: configured_policies
          MU::Cloud::AWS::Role.purgePolicy(a.policy_arn, @credentials)
        end
      }
    end

    # XXX not sure we're binding these sanely, validate that
    if @config['raw_policies']
      MU::Cloud::AWS::Role.manageRawPolicies(
        @config['raw_policies'],
        basename: @deploy.getResourceName(@config['name']),
        credentials: @credentials
      )
    end
  end

  if !@config['bare_policies'] and
     (@config['raw_policies'] or @config['attachable_policies'])
    bindTo("role", @mu_name)
  end
end
injectPolicyTargets(policy, targets, attach: false) click to toggle source

Insert a new target entity into an existing policy. @param policy [String]: The name of the policy to which we're appending, which must already exist as part of this role resource @param targets [Array<String>]: The target resource. If target_type isn't specified, this should be a fully-resolved ARN.

# File modules/mu/providers/aws/role.rb, line 315
def injectPolicyTargets(policy, targets, attach: false)
  if @deploy and !policy.match(/^#{@deploy.deploy_id}/)
    policy = @mu_name+"-"+policy.upcase
  end
  my_policies = cloud_desc(use_cache: false)["policies"]
  my_policies ||= []

  seen_policy = false
  my_policies.each { |p|
    if p.policy_name == policy
      seen_policy = true
      old = MU::Cloud::AWS.iam(credentials: @credentials).get_policy_version(
        policy_arn: p.arn,
        version_id: p.default_version_id
      ).policy_version

      doc = JSON.parse URI.decode_www_form_component old.document
      need_update = false

      doc["Statement"].each { |s|
        targets.each { |target|
          target_string = target

          if target['type'] and @deploy
            sibling = @deploy.findLitterMate(
              name: target["identifier"],
              type: target["type"]
            )

            target_string = sibling.cloudobj.arn
          elsif target.is_a? Hash
            target_string = target['identifier']
          end

          unless s["Resource"].include? target_string
            s["Resource"] << target_string
            need_update = true
          end
        }
      }

      if need_update
        MU.log "Updating IAM policy #{policy} to grant permissions on #{targets.to_s}", details: doc
        update_policy(p.arn, doc)
      end
    end
  }

  if !seen_policy
    MU.log "Was given new targets for policy #{policy}, but I don't see any such policy attached to role #{@cloud_id}", MU::WARN, details: targets
  end
end
notify() click to toggle source

Return the metadata for this user cofiguration @return [Hash]

# File modules/mu/providers/aws/role.rb, line 308
def notify
  MU.structToHash(cloud_desc)
end
toKitten(**_args) click to toggle source

Reverse-map our cloud description into a runnable config hash. We assume that any values we have in +@config+ are placeholders, and calculate our own accordingly based on what's live in the cloud.

# File modules/mu/providers/aws/role.rb, line 582
def toKitten(**_args)
  bok = {
    "cloud" => "AWS",
    "credentials" => @credentials,
    "cloud_id" => @cloud_id
  }

  if !cloud_desc or (!@config['bare_policies'] and !cloud_desc['role'])
    MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config
    return nil
  end

  desc = cloud_desc['role']
  if desc
    bok["name"] = desc.role_name
  else
    desc = cloud_desc['policies']
  end

  policies = cloud_desc['policies']
  if policies and policies.size > 0
    if @config['bare_policies']
      bok['name'] = policies.first.policy_name
      bok['bare_policies'] = true
    end

    policies.each { |pol|
      if pol.respond_to?(:arn) and
         pol.arn.match(/^arn:aws(?:-us-gov)?:iam::aws:policy\/.*?([^\/]+)$/)
        next # we'll get these later
      else
        doc = begin
          resp = MU::Cloud::AWS.iam(credentials: @credentials).get_role_policy(
            role_name: @cloud_id,
            policy_name: pol.policy_name
          )
          if resp and resp.policy_document
            JSON.parse(CGI.unescape(resp.policy_document))
          end
        rescue ::Aws::IAM::Errors::NoSuchEntity, ::Aws::IAM::Errors::ValidationError
          resp = MU::Cloud::AWS.iam(credentials: @credentials).get_policy(
            policy_arn: pol.arn
          )
          version = MU::Cloud::AWS.iam(credentials: @credentials).get_policy_version(
            policy_arn: pol.arn,
            version_id: resp.policy.default_version_id
          )
          JSON.parse(CGI.unescape(version.policy_version.document))
        end
        bok["policies"] = MU::Cloud::AWS::Role.doc2MuPolicies(pol.policy_name, doc, bok["policies"])
      end
    }

    return bok if @config['bare_policies']
  end

  if desc.tags and desc.tags.size > 0
    bok["tags"] = MU.structToHash(desc.tags, stringify_keys: true)
  end

  bok["strip_path"] = true if desc.path == "/"

  if desc.assume_role_policy_document
    assume_doc = JSON.parse(CGI.unescape(desc.assume_role_policy_document))
    assume_doc["Statement"].each { |s|
      bok["can_assume"] ||= []
      method = if s["Action"] == "sts:AssumeRoleWithWebIdentity"
        "web"
      elsif s["Action"] == "sts:AssumeRoleWithSAML"
        "saml"
      else
        "basic"
      end
      assume_block = {
        "assume_method" => method
      }
      if s["Condition"]
        assume_block["conditions"] ||= []
        s["Condition"].each_pair { |comparison, relation|
          relation.each_pair { |variable, values|
            values = [values] if !values.is_a?(Array)
            assume_block["conditions"] << {
              "comparison" => comparison,
              "variable" => variable,
              "values" => values
            }
          }
        }
      end
      s["Principal"].each_pair { |type, principals|
        my_assume = assume_block.merge({ "entity_type" => type.downcase })
        if principals.is_a?(String)
          bok["can_assume"] << my_assume.merge({
            "entity_id" => principals
          })
        else
          principals.each { |p|
            bok["can_assume"] << my_assume.merge({
              "entity_id" => p
            })
          }
        end
      }
    }
  end

  # Grab and reference any managed policies attached to this role
  resp = MU::Cloud::AWS.iam(credentials: @credentials).list_attached_role_policies(role_name: @cloud_id)
  if resp and resp.attached_policies
    resp.attached_policies.each { |pol|
      bok["attachable_policies"] ||= []
      if pol.policy_arn.match(/arn:aws(?:-us-gov)?:iam::aws:policy\//)
        bok["attachable_policies"] << MU::Config::Ref.get(
          id: pol.policy_name,
          cloud: "AWS"
        )
      else
        bok["attachable_policies"] << MU::Config::Ref.get(
          id: pol.policy_arn,
          name: pol.policy_name,
          cloud: "AWS",
          type: "roles"
        )
      end
    }
  end

  bok["attachable_policies"].uniq! if bok["attachable_policies"]
  bok["name"].gsub!(/[^a-zA-Z0-9_\-]/, "_")

  bok
end

Private Instance Methods

convert_policies_to_iam() click to toggle source

Convert entries from the cloud-neutral @config list into AWS syntax.

# File modules/mu/providers/aws/role.rb, line 1271
def convert_policies_to_iam
  MU::Cloud::AWS::Role.genPolicyDocument(@config['policies'], deploy_obj: @deploy)
end
gen_assume_role_policy_doc() click to toggle source
# File modules/mu/providers/aws/role.rb, line 1295
def gen_assume_role_policy_doc
  role_policy_doc = {
    "Version" => "2012-10-17",
  }

  statements = []
  if @config['can_assume']
    act_map = {
      "basic" => "sts:AssumeRole",
      "saml" => "sts:AssumeRoleWithSAML",
      "web" => "sts:AssumeRoleWithWebIdentity"
    }
    @config['can_assume'].each { |svc|
      statement = {
        "Effect" => "Allow",
        "Action" => act_map[svc['assume_method']],
        "Principal" => {}
      }
      if svc["conditions"]
        statement["Condition"] ||= {}
        svc["conditions"].each { |cond|
          statement["Condition"][cond['comparison']] = {
            cond["variable"] => cond["values"]
          }
        }
      end
      if ["service", "iam", "federated"].include?(svc["entity_type"])
        statement["Principal"][svc["entity_type"].capitalize] = svc["entity_id"]
      else
        sibling = @deploy.findLitterMate(
          name: svc["entity_id"],
          type: svc["entity_type"]
        )
        if sibling
          statement["Principal"][svc["entity_type"].capitalize] = sibling.cloudobj.arn
        else
          raise MuError, "Couldn't find a #{svc["entity_type"]} named #{svc["entity_id"]} when generating IAM policy in role #{@mu_name}"
        end
      end
      statements << statement
    }
  end

  role_policy_doc["Statement"] = statements

  JSON.generate(role_policy_doc)
end
get_tag_params(strip_std = false) click to toggle source
# File modules/mu/providers/aws/role.rb, line 1275
def get_tag_params(strip_std = false)
  @config['tags'] ||= []

  if !strip_std
    MU::MommaCat.listStandardTags.each_pair { |key, value|
      @config['tags'] << { "key" => key, "value" => value }
    }

    if @config['optional_tags']
      MU::MommaCat.listOptionalTags.each { |key, value|
        @config['tags'] << { "key" => key, "value" => value }
      }
    end
  end

  @config['tags'].map { |t|
    { :key => t["key"], :value => t["value"] }
  }
end
update_policy(arn, doc) click to toggle source

Update a policy, handling deletion of old versions as needed

# File modules/mu/providers/aws/role.rb, line 1344
def update_policy(arn, doc)
  MU::Cloud::AWS::Role.update_policy(arn, doc, credentials: @credentials)
end