class Miam::Client

Public Class Methods

new(options = {}) click to toggle source
# File lib/miam/client.rb, line 4
def initialize(options = {})
  @options = {
    format: :ruby,
    exclude: []
  }.merge(options)
  aws_config = options.delete(:aws_config) || {}
  @iam = Aws::IAM::Client.new(aws_config)
  @sts = Aws::STS::Client.new(aws_config)
  @driver = Miam::Driver.new(@iam, @sts, options)
  @password_manager = options[:password_manager] || Miam::PasswordManager.new('-', options)
end

Public Instance Methods

apply(file) click to toggle source
# File lib/miam/client.rb, line 56
def apply(file)
  walk(file)
end
export(export_options = {}) { |:type => type, :name => name, :dsl => dsl| ... } click to toggle source
# File lib/miam/client.rb, line 16
def export(export_options = {})
  exported, group_users, instance_profile_roles = Miam::Exporter.export(@iam, @options)
  exported.sort_array!

  if block_given?
    [:users, :groups, :roles, :instance_profiles, :policies].each do |type|
      splitted = {:users => {}, :groups => {}, :roles => {}, :instance_profiles => {}, :policies => {}}

      if export_options[:split_more]
        exported[type].sort_by {|k, v| k }.each do |name, attrs|
          more_splitted = splitted.dup
          more_splitted[type] = {}
          more_splitted[type][name] = attrs

          dsl = exec_by_format(
            :ruby => proc { Miam::DSL.convert(more_splitted, @options).strip },
            :json => proc { JSON.pretty_generate(more_splitted) }
          )

          yield(:type => type, :name => name, :dsl => dsl)
        end
      else
        splitted[type] = exported[type]

        dsl = exec_by_format(
          :ruby => proc { Miam::DSL.convert(splitted, @options).strip },
          :json => proc { JSON.pretty_generate(splitted) }
        )

        yield(:type => type, :dsl => dsl)
      end
    end
  else
    dsl = exec_by_format(
      :ruby => proc { Miam::DSL.convert(exported, @options).strip },
      :json => proc { JSON.pretty_generate(exported) }
    )
  end
end

Private Instance Methods

exec_by_format(proc_by_format) click to toggle source
# File lib/miam/client.rb, line 556
def exec_by_format(proc_by_format)
  format_proc = proc_by_format[@options[:format]]
  raise "Invalid format: #{@options[:format]}" unless format_proc
  format_proc.call
end
load_file(file) click to toggle source
# File lib/miam/client.rb, line 524
def load_file(file)
  if file.kind_of?(String)
    open(file) do |f|
      exec_by_format(
        :ruby => proc { Miam::DSL.parse(f.read, file) },
        :json => proc { load_json(f) }
      )
    end
  elsif file.respond_to?(:read)
    exec_by_format(
      :ruby => proc { Miam::DSL.parse(file.read, file.path) },
      :json => proc { load_json(f) }
    )
  else
    raise TypeError, "can't convert #{file} into File"
  end
end
load_json(json) click to toggle source
# File lib/miam/client.rb, line 562
def load_json(json)
  json = JSON.load(json)
  normalized = {}

  json.each do |top_key, top_value|
    normalized[top_key.to_sym] = top_attrs = {}

    top_value.each do |second_key, second_value|
      top_attrs[second_key] = second_attrs = {}

      second_value.each do |third_key, third_value|
        third_key = third_key.to_sym

        if third_key == :login_profile
          new_third_value = {}
          third_value.each {|k, v| new_third_value[k.to_sym] = v }
          third_value = new_third_value
        end

        second_attrs[third_key] = third_value
      end
    end
  end

  normalized
end
post_walk_managed_policies(actual) click to toggle source
# File lib/miam/client.rb, line 512
def post_walk_managed_policies(actual)
  updated = false

  actual.each do |policy_name, actual_attrs|
    next unless target_matched?(policy_name)
    @driver.delete_managed_policy(policy_name, actual_attrs[:path])
    updated = true
  end

  updated
end
pre_walk_managed_policies(expected, actual) click to toggle source
# File lib/miam/client.rb, line 477
def pre_walk_managed_policies(expected, actual)
  updated = false

  expected.each do |policy_name, expected_attrs|
    next unless target_matched?(policy_name)
    actual_attrs = actual.delete(policy_name)

    if actual_attrs
      if expected_attrs[:path] != actual_attrs[:path]
        log(:warn, "ManagedPolicy `#{policy_name}`: 'path' cannot be updated", :color => :yellow)
      end

      updated = walk_managed_policy(policy_name, actual_attrs[:path], expected_attrs[:document], actual_attrs[:document]) || updated
    else
      @driver.create_managed_policy(policy_name, expected_attrs)
      updated = true
    end
  end

  updated
end
scan_rename(type, expected, actual, group_users) click to toggle source
# File lib/miam/client.rb, line 375
def scan_rename(type, expected, actual, group_users)
  updated = false

  expected.each do |name, expected_attrs|
    renamed_from = expected_attrs[:renamed_from]
    next unless renamed_from

    actual_attrs = actual.delete(renamed_from)
    next unless actual_attrs

    @driver.update_name(type, renamed_from, name)
    actual[name] = actual_attrs

    case type
    when :user
      group_users.each do |group_name, users|
        users.each do |user_name|
          if user_name == renamed_from
            user_name.replace(name)
          end
        end
      end
    when :group
      users = group_users.delete(renamed_from)
      group_users[name] = users if users
    end

    updated = true
  end

  updated
end
target_matched?(name) click to toggle source
# File lib/miam/client.rb, line 542
def target_matched?(name)
  result = true

  if @options[:exclude]
    result &&= @options[:exclude].all? {|r| name !~ r }
  end

  if @options[:target]
    result &&= @options[:target].any? {|r| name =~ r}
  end

  result
end
walk(file) click to toggle source
# File lib/miam/client.rb, line 62
def walk(file)
  expected = load_file(file)
  @options[:exclude] += expected[:exclude]

  actual, group_users, instance_profile_roles = Miam::Exporter.export(@iam, @options)
  updated = pre_walk_managed_policies(expected[:policies], actual[:policies])
  updated = walk_groups(expected[:groups], actual[:groups], actual[:users], group_users) || updated
  updated = walk_users(expected[:users], actual[:users], group_users) || updated
  updated = walk_instance_profiles(expected[:instance_profiles], actual[:instance_profiles], actual[:roles], instance_profile_roles) || updated
  updated = walk_roles(expected[:roles], actual[:roles], instance_profile_roles) || updated
  updated = post_walk_managed_policies(actual[:policies]) || updated

  if @options[:dry_run]
    false
  else
    updated
  end
end
walk_assume_role_policy(role_name, expected_assume_role_policy, actual_assume_role_policy) click to toggle source
# File lib/miam/client.rb, line 280
def walk_assume_role_policy(role_name, expected_assume_role_policy, actual_assume_role_policy)
  updated = false
  expected_assume_role_policy.sort_array!
  actual_assume_role_policy.sort_array!

  # With only one entity granted
  # On IAM
  #   (1) Statement => [ { Principal => AWS => arn } ]
  # Should be able to specify like:
  #   (2) Statement => [ { Principal => AWS => [arn] } ]
  # Actually (1) is reflected when config (2) is applied
  expected_arp_stmt = expected_assume_role_policy.fetch('Statement', [])
  expected_arp_stmt = expected_arp_stmt.select {|i| i.key?('Principal') }

  expected_arp_stmt.each do |stmt|
    stmt['Principal'].each do |k, v|
      entities = Array(v)
      stmt['Principal'][k] = entities.first if entities.length < 2
    end
  end

  if expected_assume_role_policy != actual_assume_role_policy
    @driver.update_assume_role_policy(role_name, expected_assume_role_policy, actual_assume_role_policy)
    updated = true
  end

  updated
end
walk_attached_managed_policies(type, name, expected_attached_managed_policies, actual_attached_managed_policies) click to toggle source
# File lib/miam/client.rb, line 454
def walk_attached_managed_policies(type, name, expected_attached_managed_policies, actual_attached_managed_policies)
  expected_attached_managed_policies = expected_attached_managed_policies.sort
  actual_attached_managed_policies = actual_attached_managed_policies.sort
  updated = false

  if expected_attached_managed_policies != actual_attached_managed_policies
    add_attached_managed_policies = expected_attached_managed_policies - actual_attached_managed_policies
    remove_attached_managed_policies = actual_attached_managed_policies - expected_attached_managed_policies

    unless add_attached_managed_policies.empty?
      @driver.attach_policies(type, name, add_attached_managed_policies)
    end

    unless remove_attached_managed_policies.empty?
      @driver.detach_policies(type, name, remove_attached_managed_policies)
    end

    updated = true
  end

  updated
end
walk_group(group_name, expected_attrs, actual_attrs) click to toggle source
# File lib/miam/client.rb, line 212
def walk_group(group_name, expected_attrs, actual_attrs)
  updated = walk_policies(:group, group_name, expected_attrs[:policies], actual_attrs[:policies])
  walk_attached_managed_policies(:group, group_name, expected_attrs[:attached_managed_policies], actual_attrs[:attached_managed_policies]) || updated
end
walk_groups(expected, actual, actual_users, group_users) click to toggle source
# File lib/miam/client.rb, line 178
def walk_groups(expected, actual, actual_users, group_users)
  updated = scan_rename(:group, expected, actual, group_users)

  expected.each do |group_name, expected_attrs|
    next unless target_matched?(group_name)

    actual_attrs = actual.delete(group_name)

    if actual_attrs
      updated = walk_path(:group, group_name, expected_attrs[:path], actual_attrs[:path]) || updated
      updated = walk_group(group_name, expected_attrs, actual_attrs) || updated
    else
      actual_attrs = @driver.create_group(group_name, expected_attrs)
      walk_group(group_name, expected_attrs, actual_attrs)
      updated = true
    end
  end

  actual.each do |group_name, attrs|
    next unless target_matched?(group_name)

    users_in_group = group_users.delete(group_name) || []
    @driver.delete_group(group_name, attrs, users_in_group)

    actual_users.each do |user_name, user_attrs|
      user_attrs[:groups].delete(group_name)
    end

    updated = true
  end

  updated
end
walk_instance_profile(instance_profile_name, expected_attrs, actual_attrs) click to toggle source
# File lib/miam/client.rb, line 365
def walk_instance_profile(instance_profile_name, expected_attrs, actual_attrs)
  updated = false

  if expected_attrs != actual_attrs
    log(:warn, "InstanceProfile `#{instance_profile_name}`: 'path' cannot be updated", :color => :yellow)
  end

  updated
end
walk_instance_profiles(expected, actual, actual_roles, instance_profile_roles) click to toggle source
# File lib/miam/client.rb, line 332
def walk_instance_profiles(expected, actual, actual_roles, instance_profile_roles)
  updated = false

  expected.each do |instance_profile_name, expected_attrs|
    next unless target_matched?(instance_profile_name)

    actual_attrs = actual.delete(instance_profile_name)

    if actual_attrs
      updated = walk_instance_profile(instance_profile_name, expected_attrs, actual_attrs) || updated
    else
      actual_attrs = @driver.create_instance_profile(instance_profile_name, expected_attrs)
      walk_instance_profile(instance_profile_name, expected_attrs, actual_attrs)
      updated = true
    end
  end

  actual.each do |instance_profile_name, attrs|
    next unless target_matched?(instance_profile_name)

    roles_in_instance_profile = instance_profile_roles.delete(instance_profile_name) || []
    @driver.delete_instance_profile(instance_profile_name, attrs, roles_in_instance_profile)

    actual_roles.each do |role_name, role_attrs|
      role_attrs[:instance_profiles].delete(instance_profile_name)
    end

    updated = true
  end

  updated
end
walk_login_profile(user_name, expected_login_profile, actual_login_profile) click to toggle source
# File lib/miam/client.rb, line 127
def walk_login_profile(user_name, expected_login_profile, actual_login_profile)
  updated = false

  [expected_login_profile, actual_login_profile].each do |login_profile|
    if login_profile and not login_profile.has_key?(:password_reset_required)
      login_profile[:password_reset_required] = false
    end
  end

  if expected_login_profile and not actual_login_profile
    expected_login_profile[:password] ||= @password_manager.identify(user_name, :login_profile, @driver.password_policy)
    @driver.create_login_profile(user_name, expected_login_profile)
    updated = true
  elsif not expected_login_profile and actual_login_profile
    @driver.delete_login_profile(user_name)
    updated = true
  elsif expected_login_profile != actual_login_profile
    if @options[:ignore_login_profile]
      log(:warn, "User `#{user_name}`: difference of login profile has been ignored: expected=#{expected_login_profile.inspect}, actual=#{actual_login_profile.inspect}", :color => :yellow)
    else
      @driver.update_login_profile(user_name, expected_login_profile, actual_login_profile)
      updated = true
    end
  end

  updated
end
walk_managed_policy(policy_name, policy_path, expected_document, actual_document) click to toggle source
# File lib/miam/client.rb, line 499
def walk_managed_policy(policy_name, policy_path, expected_document, actual_document)
  updated = false
  expected_document.sort_array!
  actual_document.sort_array!

  if expected_document != actual_document
    @driver.update_managed_policy(policy_name, policy_path, expected_document, actual_document)
    updated = true
  end

  updated
end
walk_path(type, user_or_group_name, expected_path, actual_path) click to toggle source
# File lib/miam/client.rb, line 408
def walk_path(type, user_or_group_name, expected_path, actual_path)
  updated = false

  if expected_path != actual_path
    @driver.update_path(type, user_or_group_name, expected_path, actual_path)
    updated = true
  end

  updated
end
walk_policies(type, user_or_group_name, expected_policies, actual_policies) click to toggle source
# File lib/miam/client.rb, line 419
def walk_policies(type, user_or_group_name, expected_policies, actual_policies)
  updated = false

  expected_policies.each do |policy_name, expected_document|
    actual_document = actual_policies.delete(policy_name)

    if actual_document
      updated = walk_policy(type, user_or_group_name, policy_name, expected_document, actual_document) || updated
    else
      @driver.create_policy(type, user_or_group_name, policy_name, expected_document)
      updated = true
    end
  end

  actual_policies.each do |policy_name, document|
    @driver.delete_policy(type, user_or_group_name, policy_name)
    updated = true
  end

  updated
end
walk_policy(type, user_or_group_name, policy_name, expected_document, actual_document) click to toggle source
# File lib/miam/client.rb, line 441
def walk_policy(type, user_or_group_name, policy_name, expected_document, actual_document)
  updated = false
  expected_document.sort_array!
  actual_document.sort_array!

  if expected_document != actual_document
    @driver.update_policy(type, user_or_group_name, policy_name, expected_document, actual_document)
    updated = true
  end

  updated
end
walk_role(role_name, expected_attrs, actual_attrs) click to toggle source
# File lib/miam/client.rb, line 257
def walk_role(role_name, expected_attrs, actual_attrs)
  if expected_attrs.values_at(:path) != actual_attrs.values_at(:path)
    log(:warn, "Role `#{role_name}`: 'path' cannot be updated", :color => :yellow)
  end

  updated = walk_role_settings(role_name, {max_session_duration: expected_attrs[:max_session_duration]}, {max_session_duration: actual_attrs[:max_session_duration]})
  updated = walk_assume_role_policy(role_name, expected_attrs[:assume_role_policy_document], actual_attrs[:assume_role_policy_document]) || updated
  updated = walk_role_instance_profiles(role_name, expected_attrs[:instance_profiles], actual_attrs[:instance_profiles]) || updated
  updated = walk_attached_managed_policies(:role, role_name, expected_attrs[:attached_managed_policies], actual_attrs[:attached_managed_policies]) || updated
  walk_policies(:role, role_name, expected_attrs[:policies], actual_attrs[:policies]) || updated
end
walk_role_instance_profiles(role_name, expected_instance_profiles, actual_instance_profiles) click to toggle source
# File lib/miam/client.rb, line 309
def walk_role_instance_profiles(role_name, expected_instance_profiles, actual_instance_profiles)
  expected_instance_profiles = expected_instance_profiles.sort
  actual_instance_profiles = actual_instance_profiles.sort
  updated = false

  if expected_instance_profiles != actual_instance_profiles
    add_instance_profiles = expected_instance_profiles - actual_instance_profiles
    remove_instance_profiles = actual_instance_profiles - expected_instance_profiles

    unless add_instance_profiles.empty?
      @driver.add_role_to_instance_profiles(role_name, add_instance_profiles)
    end

    unless remove_instance_profiles.empty?
      @driver.remove_role_from_instance_profiles(role_name, remove_instance_profiles)
    end

    updated = true
  end

  updated
end
walk_role_settings(role_name, expected_settings, actual_settings) click to toggle source
# File lib/miam/client.rb, line 269
def walk_role_settings(role_name, expected_settings, actual_settings)
  updated = false

  if expected_settings != actual_settings
    @driver.update_role_settings(role_name, expected_settings, actual_settings)
    updated = true
  end

  updated
end
walk_roles(expected, actual, instance_profile_roles) click to toggle source
# File lib/miam/client.rb, line 217
def walk_roles(expected, actual, instance_profile_roles)
  updated = false

  expected.each do |role_name, expected_attrs|
    next unless target_matched?(role_name)

    actual_attrs = actual.delete(role_name)

    if actual_attrs
      updated = walk_role(role_name, expected_attrs, actual_attrs) || updated
    else
      actual_attrs = @driver.create_role(role_name, expected_attrs)
      walk_role(role_name, expected_attrs, actual_attrs)
      updated = true
    end
  end

  actual.each do |role_name, attrs|
    next unless target_matched?(role_name)

    instance_profile_names = []

    instance_profile_roles.each do |instance_profile_name, roles|
      if roles.include?(role_name)
        instance_profile_names << instance_profile_name
      end
    end

    @driver.delete_role(role_name, instance_profile_names, attrs)

    instance_profile_roles.each do |instance_profile_name, roles|
      roles.delete(role_name)
    end

    updated = true
  end

  updated
end
walk_user(user_name, expected_attrs, actual_attrs) click to toggle source
# File lib/miam/client.rb, line 120
def walk_user(user_name, expected_attrs, actual_attrs)
  updated = walk_login_profile(user_name, expected_attrs[:login_profile], actual_attrs[:login_profile])
  updated = walk_user_groups(user_name, expected_attrs[:groups], actual_attrs[:groups]) || updated
  updated = walk_attached_managed_policies(:user, user_name, expected_attrs[:attached_managed_policies], actual_attrs[:attached_managed_policies]) || updated
  walk_policies(:user, user_name, expected_attrs[:policies], actual_attrs[:policies]) || updated
end
walk_user_groups(user_name, expected_groups, actual_groups) click to toggle source
# File lib/miam/client.rb, line 155
def walk_user_groups(user_name, expected_groups, actual_groups)
  expected_groups = expected_groups.sort
  actual_groups = actual_groups.sort
  updated = false

  if expected_groups != actual_groups
    add_groups = expected_groups - actual_groups
    remove_groups = actual_groups - expected_groups

    unless add_groups.empty?
      @driver.add_user_to_groups(user_name, add_groups)
    end

    unless remove_groups.empty?
      @driver.remove_user_from_groups(user_name, remove_groups)
    end

    updated = true
  end

  updated
end
walk_users(expected, actual, group_users) click to toggle source
# File lib/miam/client.rb, line 81
def walk_users(expected, actual, group_users)
  updated = scan_rename(:user, expected, actual, group_users)

  expected.each do |user_name, expected_attrs|
    next unless target_matched?(user_name)

    actual_attrs = actual.delete(user_name)

    if actual_attrs
      updated = walk_path(:user, user_name, expected_attrs[:path], actual_attrs[:path]) || updated
      updated = walk_user(user_name, expected_attrs, actual_attrs) || updated
    else
      actual_attrs = @driver.create_user(user_name, expected_attrs)
      access_key = @driver.create_access_key(user_name) unless @options[:no_access_key]

      if access_key
        @password_manager.puts_password(user_name, access_key[:access_key_id], access_key[:secret_access_key])
      end

      walk_user(user_name, expected_attrs, actual_attrs)
      updated = true
    end
  end

  actual.each do |user_name, attrs|
    next unless target_matched?(user_name)

    @driver.delete_user(user_name, attrs)

    group_users.each do |group_name, users|
      users.delete(user_name)
    end

    updated = true
  end

  updated
end