class MU::Cloud::AWS::User

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

Public Class Methods

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

Remove all users 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/user.rb, line 193
        def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, flags: {})
          MU.log "AWS::User.cleanup: need to support flags['known']", MU::DEBUG, details: flags

          # XXX this doesn't belong here; maybe under roles, maybe as its own stupid first-class resource
          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 policy /#{deploy_id}/#{policy.policy_name}"
              if !noop
                attachments = begin
                  MU::Cloud::AWS.iam(credentials: credentials).list_entities_for_policy(
                    policy_arn: policy.arn
                  )
                rescue ::Aws::IAM::Errors::NoSuchEntity
                end
                if attachments
                  begin
                    attachments.policy_users.each { |u|
                      MU::Cloud::AWS.iam(credentials: credentials).detach_user_policy(
                        user_name: u.user_name,
                        policy_arn: policy.arn
                      )
                    }
                  rescue ::Aws::IAM::Errors::NoSuchEntity
                  end
                  begin
                    attachments.policy_groups.each { |g|
                      MU::Cloud::AWS.iam(credentials: credentials).detach_group_policy(
                        group_name: g.group_name,
                        policy_arn: policy.arn
                      )
                    }
                  rescue ::Aws::IAM::Errors::NoSuchEntity
                  end
                  begin
                    attachments.policy_roles.each { |r|
                      MU::Cloud::AWS.iam(credentials: credentials).detach_role_policy(
                        role_name: r.role_name,
                        policy_arn: policy.arn
                      )
                    }
                  rescue ::Aws::IAM::Errors::NoSuchEntity
                  end
                end

                begin
                  MU::Cloud::AWS.iam(credentials: credentials).delete_policy(
                    policy_arn: policy.arn
                  )
                rescue ::Aws::IAM::Errors::DeleteConflict
                  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
                  }
                  retry
                rescue ::Aws::IAM::Errors::NoSuchEntity
rescue StandardError => e
MU.log e.inspect, MU::ERR, details: policy
                end
              end
            }
          end

          resp = MU::Cloud::AWS.iam(credentials: credentials).list_users

          # XXX this response includes a tags attribute, but it's always empty,
          # even when the user is tagged. So we go through the extra call for
          # each user. Inefficient. Probably Amazon's bug.
          resp.users.each { |u|
            tags = MU::Cloud::AWS.iam(credentials: credentials).list_user_tags(
              user_name: u.user_name
            ).tags
            has_nodelete = false
            has_ourdeploy = false
            has_ourmaster = false
            tags.each { |tag|
              if tag.key == "MU-ID" and tag.value == deploy_id
                has_ourdeploy = true
              elsif tag.key == "MU-MASTER-IP" and tag.value == MU.mu_public_ip
                has_ourmaster = true
              elsif tag.key == "MU-NO-DELETE" and tag.value == "true"
                has_nodelete = true
              end
            }
            if has_ourdeploy and !has_nodelete and (ignoremaster or has_ourmaster)
              MU.log "Deleting IAM user #{u.path}#{u.user_name}"
              if !@noop
                begin
                  groups = MU::Cloud::AWS.iam(credentials: credentials).list_groups_for_user(
                    user_name: u.user_name
                  ).groups

                  groups.each { |g|
                    MU::Cloud::AWS.iam(credentials: credentials).remove_user_from_group(
                      user_name: u.user_name,
                      group_name: g.group_name
                    )
                  }
                  MU::Cloud::AWS.iam(credentials: credentials).get_login_profile(
                    user_name: u.user_name
                  )
                  MU.log "Deleting IAM login profile for #{u.user_name}"
                  MU::Cloud::AWS.iam(credentials: credentials).delete_login_profile(
                    user_name: u.user_name
                  )
                rescue Aws::IAM::Errors::EntityTemporarilyUnmodifiable
                  sleep 10
                  retry
                rescue Aws::IAM::Errors::NoSuchEntity
                end
                keys = MU::Cloud::AWS.iam(credentials: credentials).list_access_keys(
                  user_name: u.user_name
                )
                if keys.access_key_metadata.size > 0
                  keys.access_key_metadata.each { |key|
                    MU.log "Deleting IAM access key #{key.access_key_id} for #{u.user_name}"
                    keys = MU::Cloud::AWS.iam(credentials: credentials).delete_access_key(
                      user_name: u.user_name,
                      access_key_id: key.access_key_id
                    )
                  }
                end

                poldesc = MU::Cloud::AWS.iam(credentials: credentials).list_user_policies(user_name: u.user_name)
                if poldesc and poldesc.policy_names and poldesc.policy_names.size > 0
                  poldesc.policy_names.each { |pol_name|
                    MU::Cloud::AWS.iam(credentials: credentials).delete_user_policy(user_name: u.user_name, policy_name: pol_name)
                  }
                end

                attached_policies = MU::Cloud::AWS.iam(credentials: credentials).list_attached_user_policies(
                  user_name: u.user_name
                ).attached_policies
                attached_policies.each { |a|
                  MU.log "Detaching IAM policy #{a.policy_arn} from user #{u.user_name}"
                  MU::Cloud::AWS.iam(credentials: credentials).detach_user_policy(user_name: u.user_name, policy_arn: a.policy_arn)
                }

                MU::Cloud::AWS.iam(credentials: credentials).delete_user(user_name: u.user_name)
              end
            end
          }

        end
find(**args) click to toggle source

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

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

  if args[:cloud_id]
    begin
      resp = MU::Cloud::AWS.iam(credentials: args[:credentials]).get_user(user_name: args[:cloud_id])
      if resp and resp.user
        found[args[:cloud_id]] = resp.user
      end
    rescue ::Aws::IAM::Errors::NoSuchEntity
    end
  else
    marker = nil
    begin
      resp = MU::Cloud::AWS.iam(credentials: args[:credentials]).list_users(marker: marker)
      break if !resp or !resp.users
      marker = resp.marker

      resp.users.each { |u|
        found[u.user_name] = u
      }
    end while marker
  end

  found
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/user.rb, line 179
def self.isGlobal?
  true
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/user.rb, line 23
def initialize(**args)
  super
  @mu_name ||= if @config['unique_name']
    @deploy.getResourceName(@config["name"])
  else
    @config['name']
  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/user.rb, line 185
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/user.rb, line 465
        def self.schema(_config)
          toplevel_required = []
          polschema = MU::Config::Role.schema["properties"]["policies"]
          polschema.deep_merge!(MU::Cloud.resourceClass("AWS", "Role").condition_schema)

          schema = {
            "inline_policies" => polschema,
            "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"])
            },
            "raw_policies" => {
              "type" => "array",
              "items" => {
                "description" => "A key (name) with a value that is an Amazon-compatible policy document. See https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_examples.html for example policies.",
                "type" => "object"
              }
            },
            "name" => {
              "type" => "string",
              "description" => "A plain IAM user. If the user already exists, we will operate on that existing user. Otherwise, we will attempt to create a new user. AWS IAM does not distinguish between human user accounts and machine accounts."
            },
            "path" => {
              "type" => "string",
              "description" => "AWS IAM users can be namespaced with a path (ex: +/organization/unit/user+). If not specified, and if we do not see a matching existing user under +/+ with +use_if_exists+ set, we will prepend the deploy identifier to the path of users we create. Ex: +/IAMTESTS-DEV-2018112910-GR/myuser+.",
              "pattern" => '^\/(?:[^\/]+(?:\/[^\/]+)*\/$)?'
            },
            "tags" => MU::Config.tags_primitive,
            "optional_tags" => MU::Config.optional_tags_primitive,
            "unique_name" => {
              "type" => "boolean",
              "default" => false,
              "description" => "Instead of creating/updating a user account with
 the exact name specified in the 'name' field, generate a unique-per-deploy Mu-
style long name, like +IAMTESTS-DEV-2018112815-IS-USER-FOO+"
            },
            "create_console_password" => {
              "type" => "boolean",
              "default" => false,
              "description" => "Generate a password for this user, for use logging into the AWS Console. It will be shared via Scratchpad for one-time retrieval."
            }
          }
          [toplevel_required, schema]
        end
validateConfig(user, configurator) click to toggle source

Cloud-specific pre-processing of {MU::Config::BasketofKittens::users}, bare and unvalidated. @param user [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/user.rb, line 514
def self.validateConfig(user, configurator)
  ok = true

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

  if user['groups']
    user['groups'].each { |group|
      need_dependency = false
      if configurator.haveLitterMate?(group, "groups")
        need_dependency = true
      else
        found = MU::Cloud.resourceClass("AWS", "Group").find(cloud_id: group)
        if found.nil? or found.empty? or (configurator.updating and
           found.values.first.group.path == "/"+configurator.updating+"/")
          groupdesc = {
            "name" => group
          }
          configurator.insertKitten(groupdesc, "groups")
          need_dependency = true
        end
      end

      if need_dependency
        MU::Config.addDependency(user, group, "group")
      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/user.rb, line 351
def arn
  cloud_desc.arn
end
create() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/aws/user.rb, line 33
def create

  begin
    MU::Cloud::AWS.iam(credentials: @credentials).get_user(user_name: @mu_name, path: @config['path'])
    if !@config['use_if_exists']
      raise MuError, "IAM user #{@mu_name} already exists and use_if_exists is false"
    end
  rescue Aws::IAM::Errors::NoSuchEntity
    @config['path'] ||= "/"+@deploy.deploy_id+"/"
    MU.log "Creating IAM user #{@config['path']}/#{@mu_name}"
    tags = get_tag_params
    MU::Cloud::AWS.iam(credentials: @credentials).create_user(
      user_name: @mu_name,
      path: @config['path'],
      tags: tags
    )
  end

end
groom() click to toggle source

Called automatically by {MU::Deploy#createResources}

# File modules/mu/providers/aws/user.rb, line 54
def groom
  resp = MU::Cloud::AWS.iam(credentials: @credentials).list_user_tags(user_name: @mu_name)

  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 user #{@mu_name}", MU::NOTICE, details: tag_param
    MU::Cloud::AWS.iam(credentials: @credentials).tag_user(user_name: @mu_name, tags: tag_param)
  end
  # Note: We don't delete tags, because we often share user accounts
  # managed outside of Mu. We have no way of know what tags might come
  # from other things, so we err on the side of caution instead of
  # deleting stuff.

  if @config['create_console_password']
    begin
      MU::Cloud::AWS.iam(credentials: @credentials).get_login_profile(user_name: @mu_name)
    rescue Aws::IAM::Errors::NoSuchEntity
      pw = Password.pronounceable(12..14)
      retries = 0
      begin
        MU::Cloud::AWS.iam(credentials: @credentials).create_login_profile(
          user_name: @mu_name,
          password: pw
        )
        scratchitem = MU::Master.storeScratchPadSecret("AWS Console password for user #{@mu_name}:\n<pre>#{pw}</pre>")
        MU.log "User #{@mu_name}'s AWS Console password can be retrieved from: https://#{$MU_CFG['public_address']}/scratchpad/#{scratchitem}", MU::SUMMARY
      rescue Aws::IAM::Errors::PasswordPolicyViolation => e
        if retries < 1
          pw = MU.generateWindowsPassword
          retries += 1
          sleep 1
          retry
        else
          MU.log "Error setting password for #{e.message}", MU::WARN
        end
      end
    end
  end

  if @config['create_api_keys']
    resp = MU::Cloud::AWS.iam(credentials: @credentials).list_access_keys(
      user_name: @mu_name
    )
    if resp.access_key_metadata.size == 0
      resp = MU::Cloud::AWS.iam(credentials: @credentials).create_access_key(
        user_name: @mu_name
      )
      scratchitem = MU::Master.storeScratchPadSecret("AWS Access Key and Secret for user #{@mu_name}:\nKEY: #{resp.access_key.access_key_id}\nSECRET: #{resp.access_key.secret_access_key}")
      MU.log "User #{@mu_name}'s AWS Key and Secret can be retrieved from: https://#{$MU_CFG['public_address']}/scratchpad/#{scratchitem}", MU::SUMMARY
    end
  end

  # Create these if necessary, then append them to the list of
  # attachable_policies
  if @config['raw_policies']
    pol_arns = MU::Cloud.resourceClass("AWS", "Role").manageRawPolicies(
      @config['raw_policies'],
      basename: @deploy.getResourceName(@config['name']),
      credentials: @credentials
    )
    @config['attachable_policies'] ||= []
    @config['attachable_policies'].concat(pol_arns.map { |a| { "id" => a } })
  end

  if @config['attachable_policies']
    configured_policies = @config['attachable_policies'].map { |p|
      if p.is_a?(MU::Config::Ref)
        p.cloud_id
      else
        p = MU::Config::Ref.get(p)
        p.kitten
        p.cloud_id
      end
    }

    attached_policies = MU::Cloud::AWS.iam(credentials: @credentials).list_attached_user_policies(
      user_name: @cloud_id
    ).attached_policies
    attached_policies.each { |a|
      if !configured_policies.include?(a.policy_arn)
        MU.log "Removing IAM policy #{a.policy_arn} from user #{@mu_name}", MU::NOTICE
        MU::Cloud.resourceClass("AWS", "Role").purgePolicy(a.policy_arn, @credentials)
      else
        configured_policies.delete(a.policy_arn)
      end
    }

    configured_policies.each { |policy_arn|
      MU.log "Attaching #{policy_arn} to user #{@cloud_id}"
      MU::Cloud::AWS.iam(credentials: @credentials).attach_user_policy(
        policy_arn: policy_arn,
        user_name: @cloud_id
      )
    }
  end

  if @config['inline_policies']
    docs = MU::Cloud.resourceClass("AWS", "Role").genPolicyDocument(@config['inline_policies'], deploy_obj: @deploy)
    docs.each { |doc|
      MU.log "Putting user policy #{doc.keys.first} to user #{@cloud_id} "
      MU::Cloud::AWS.iam(credentials: @credentials).put_user_policy(
        policy_document: JSON.generate(doc.values.first),
        policy_name: doc.keys.first,
        user_name: @cloud_id
      )
    }
  end

end
notify() click to toggle source

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

# File modules/mu/providers/aws/user.rb, line 170
def notify
  descriptor = MU.structToHash(MU::Cloud::AWS.iam(credentials: @credentials).get_user(user_name: @mu_name).user)
  descriptor["cloud_id"] = @mu_name
  descriptor
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/user.rb, line 387
def toKitten(**_args)
  bok = {
    "cloud" => "AWS",
    "credentials" => @credentials,
    "cloud_id" => @cloud_id
  }

  if !cloud_desc
    MU.log "toKitten failed to load a cloud_desc from #{@cloud_id}", MU::ERR, details: @config
    return nil
  end

  bok['name'] = cloud_desc.user_name

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

  if cloud_desc.path != "/"
    bok["path"] = cloud_desc.path
  end

  begin
    resp = MU::Cloud::AWS.iam(credentials: @credentials).get_login_profile(user_name: @cloud_id)
    if resp and resp.login_profile
      bok['create_console_password'] = true
      if resp.login_profile.password_reset_required
        bok['force_password_change'] = true
      end
    end
  rescue ::Aws::IAM::Errors::NoSuchEntity
  end

  begin
    resp = MU::Cloud::AWS.iam(credentials: @credentials).list_access_keys(user_name: @cloud_id)
    if resp and resp.access_key_metadata
      bok['create_api_key'] = true
    end
  rescue ::Aws::IAM::Errors::NoSuchEntity
  end

  # Grab and assimilate any inline policies attached to this user
  resp = MU::Cloud::AWS.iam(credentials: @credentials).list_user_policies(user_name: @cloud_id)
  if resp and resp.policy_names and resp.policy_names.size > 0
    resp.policy_names.each { |pol_name|
      pol = MU::Cloud::AWS.iam(credentials: @credentials).get_user_policy(user_name: @cloud_id, policy_name: pol_name)
      doc = JSON.parse(CGI.unescape(pol.policy_document))
      bok["inline_policies"] = MU::Cloud.resourceClass("AWS", "Role").doc2MuPolicies(pol.policy_name, doc, bok["inline_policies"])
    }
  end

  # Grab and reference any managed policies attached to this user
  resp = MU::Cloud::AWS.iam(credentials: @credentials).list_attached_user_policies(user_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
end

Private Instance Methods

get_tag_params(strip_std = false) click to toggle source
# File modules/mu/providers/aws/user.rb, line 555
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

  if @config['preserve_on_cleanup']
    @config['tags'] << { "key" => "MU-NO-DELETE", "value" => "true" }
  end

  @config['tags'].map { |t|
    { :key => t["key"], :value => t["value"] }
  }
end