# Copyright:: Copyright (c) 2018 eGlobalTech, Inc., all rights reserved # # Licensed under the BSD-3 license (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License in the root of the project or at # # http://egt-labs.com/mu/LICENSE.html # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. module MU class Cloud class AWS # A user as configured in {MU::Config::BasketofKittens::users} class User < MU::Cloud::User @deploy = nil @config = nil attr_reader :mu_name attr_reader :config attr_reader :cloud_id # @param mommacat [MU::MommaCat]: A {MU::Mommacat} object containing the deploy of which this resource is/will be a member. # @param kitten_cfg [Hash]: The fully parsed and resolved {MU::Config} resource descriptor as defined in {MU::Config::BasketofKittens::users} def initialize(mommacat: nil, kitten_cfg: nil, mu_name: nil, cloud_id: nil) @deploy = mommacat @config = MU::Config.manxify(kitten_cfg) @cloud_id ||= cloud_id @mu_name ||= if @config['unique_name'] @deploy.getResourceName(@config["name"]) else @config['name'] end end # Called automatically by {MU::Deploy#createResources} def create begin MU::Cloud::AWS.iam(credentials: @config['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 => e @config['path'] ||= "/"+@deploy.deploy_id+"/" MU.log "Creating IAM user #{@config['path']}/#{@mu_name}" tags = get_tag_params MU::Cloud::AWS.iam(credentials: @config['credentials']).create_user( user_name: @mu_name, path: @config['path'], tags: tags ) end end # Called automatically by {MU::Deploy#createResources} def groom resp = MU::Cloud::AWS.iam(credentials: @config['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: @config['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: @config['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: @config['credentials']).create_login_profile( user_name: @mu_name, password: pw ) scratchitem = MU::Master.storeScratchPadSecret("AWS Console password for user #{@mu_name}:\n
#{pw}") 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: @config['credentials']).list_access_keys( user_name: @mu_name ) if resp.access_key_metadata.size == 0 resp = MU::Cloud::AWS.iam(credentials: @config['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 if @config['iam_policies'] @dependencies["role"].each_pair { |rolename, roleobj| roleobj.cloudobj.bindTo("user", @cloud_id) } end end # Return the metadata for this user cofiguration # @return [Hash] def notify descriptor = MU.structToHash(MU::Cloud::AWS.iam(credentials: @config['credentials']).get_user(user_name: @mu_name).user) descriptor["cloud_id"] = @mu_name descriptor end # Does this resource type exist as a global (cloud-wide) artifact, or # is it localized to a region/zone? # @return [Boolean] def self.isGlobal? true end # Denote whether this resource implementation is experiment, ready for # testing, or ready for production use. def self.quality MU::Cloud::BETA end # 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 # @param region [String]: The cloud provider region # @return [void] def self.cleanup(noop: false, ignoremaster: false, region: MU.curRegion, credentials: nil, 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: "/"+MU.deploy_id+"/" ) if resp and resp.policies resp.policies.each { |policy| MU.log "Deleting policy /#{MU.deploy_id}/#{policy.policy_name}" if !noop attachments = MU::Cloud::AWS.iam(credentials: credentials).list_entities_for_policy( policy_arn: policy.arn ) attachments.policy_users.each { |u| MU::Cloud::AWS.iam(credentials: credentials).detach_user_policy( user_name: u.user_name, policy_arn: policy.arn ) } attachments.policy_groups.each { |g| MU::Cloud::AWS.iam(credentials: credentials).detach_role_policy( group_name: g.group_name, policy_arn: policy.arn ) } attachments.policy_roles.each { |r| MU::Cloud::AWS.iam(credentials: credentials).detach_role_policy( role_name: r.role_name, policy_arn: policy.arn ) } MU::Cloud::AWS.iam(credentials: credentials).delete_policy( policy_arn: policy.arn ) 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 tags.each { |tag| if tag.key == "MU-ID" and tag.value == MU.deploy_id has_ourdeploy = true elsif tag.key == "MU-NO-DELETE" and tag.value == "true" has_nodelete = true end } if has_ourdeploy and !has_nodelete 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 ) } profile = 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 MU::Cloud::AWS.iam(credentials: credentials).delete_user(user_name: u.user_name) end end } end # Canonical Amazon Resource Number for this resource # @return [String] def arn cloud_desc.arn end # Locate an existing user group. # @param cloud_id [String]: The cloud provider's identifier for this resource. # @param region [String]: The cloud provider region. # @param flags [Hash]: Optional flags # @return [OpenStruct]: The cloud provider's complete descriptions of matching user group. def self.find(cloud_id: nil, region: MU.curRegion, credentials: nil, flags: {}) found = nil begin resp = MU::Cloud::AWS.iam.get_user(user_name: cloud_id) if resp and resp.user found ||= {} found[cloud_id] = resp.user end rescue ::Aws::IAM::Errors::NoSuchEntity end found end # Cloud-specific configuration properties. # @param config [MU::Config]: The calling MU::Config object # @return [Array