Skip to content
Permalink
Browse files

Merge branch '0.2.4' into master

  • Loading branch information...
Genki Sugawara
Genki Sugawara committed Jul 10, 2017
2 parents 813a45a + 1a9d828 commit b803336483540f1bcbc317582e37f7261fea7c23
Showing with 118 additions and 39 deletions.
  1. +2 −1 .rspec
  2. +1 −1 .travis.yml
  3. +7 −0 README.md
  4. +1 −0 bin/miam
  5. +35 −10 lib/miam/client.rb
  6. +33 −13 lib/miam/driver.rb
  7. +9 −3 lib/miam/dsl/converter.rb
  8. +5 −4 lib/miam/exporter.rb
  9. +23 −4 lib/miam/password_manager.rb
  10. +1 −1 lib/miam/version.rb
  11. +1 −2 miam.gemspec
3 .rspec
@@ -1,4 +1,5 @@
--require rspec/instafail
--format RSpec::Instafail
--format documentation
#RSpec::Instafail
--colour
--require spec_helper
@@ -4,7 +4,7 @@ rvm:
- 2.3.4
script:
- bundle install
- bundle exec rake
- travis_wait bundle exec rake
env:
global:
- secure: c5kyaYSGrKMg3bsVMCnIYe2w/3jJ4rxkoIwM06YGLmw5lPW0twWgpwpuBsGf6WKvDLv0eQjGq3B0I1DqHmdYHq4dQ+PdsvWg0kQUpzWpKD2ccVXevyPeDs5pC1UcLtDUpRk0Rv1Hpo4v5kiT2zsmLx29Z6F4n7agtzqK5Q2vdPs=
@@ -22,6 +22,12 @@ It defines the state of IAM using DSL, and updates IAM according to DSL.
* Sort policy array
* `>= 0.2.3`
* Support Custom Managed Policy
* `>= 0.2.4`
* Fix for Password Policy ([RP#22](https://github.com/winebarrel/miam/pull/22))
* Fix `--target` option for Policies ([RP#21](https://github.com/winebarrel/miam/pull/21))
* Fix for `Rate exceeded` ([PR#23](https://github.com/winebarrel/miam/pull/23))
* Fix for non-User credentials ([PR#17](https://github.com/winebarrel/miam/pull/17))
* Add `--exclude` option

## Installation

@@ -71,6 +77,7 @@ Usage: miam [options]
--format=FORMAT
--export-concurrency N
--target REGEXP
--exclude REGEXP
--ignore-login-profile
--no-color
--no-progress
@@ -52,6 +52,7 @@ ARGV.options do |opt|
opt.on('', '--format=FORMAT', [:ruby, :json]) {|v| format_passed = true; options[:format] = v }
opt.on('' , '--export-concurrency N', Integer) {|v| options[:export_concurrency] = v }
opt.on('' , '--target REGEXP') {|v| options[:target] = Regexp.new(v) }
opt.on('' , '--exclude REGEXP') {|v| options[:exclude] = Regexp.new(v) }
opt.on('' , '--ignore-login-profile') { options[:ignore_login_profile] = true }
opt.on('' , '--no-color') { options[:color] = false }
opt.on('' , '--no-progress') { options[:no_progress] = true }
@@ -5,7 +5,8 @@ def initialize(options = {})
@options = {:format => :ruby}.merge(options)
aws_config = options.delete(:aws_config) || {}
@iam = Aws::IAM::Client.new(aws_config)
@driver = Miam::Driver.new(@iam, options)
@sts = Aws::STS::Client.new(aws_config)
@driver = Miam::Driver.new(@iam, @sts, options)
@password_manager = options[:password_manager] || Miam::PasswordManager.new('-', options)
end

@@ -129,15 +130,15 @@ def walk_login_profile(user_name, expected_login_profile, actual_login_profile)
end

if expected_login_profile and not actual_login_profile
expected_login_profile[:password] ||= @password_manager.identify(user_name, :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 loging profile has been ignored: expected=#{expected_login_profile.inspect}, actual=#{actual_login_profile.inspect}", :color => :yellow)
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
@@ -265,6 +266,22 @@ def walk_assume_role_policy(role_name, expected_assume_role_policy, actual_assum
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
@@ -445,14 +462,15 @@ 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, expected_attrs[:document], actual_attrs[:document]) || updated
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
@@ -462,13 +480,13 @@ def pre_walk_managed_policies(expected, actual)
updated
end

def walk_managed_policy(policy_name, expected_document, actual_document)
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, expected_document, actual_document)
@driver.update_managed_policy(policy_name, policy_path, expected_document, actual_document)
updated = true
end

@@ -479,7 +497,8 @@ def post_walk_managed_policies(actual)
updated = false

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

@@ -505,11 +524,17 @@ def load_file(file)
end

def target_matched?(name)
result = true

if @options[:exclude]
result &&= name !~ @options[:exclude]
end

if @options[:target]
name =~ @options[:target]
else
true
result &&= name =~ @options[:target]
end

result
end

def exec_by_format(proc_by_format)
@@ -4,9 +4,11 @@ class Miam::Driver
MAX_POLICY_SIZE = 2048
MAX_POLICY_VERSIONS = 5

def initialize(iam, options = {})
def initialize(iam, sts, options = {})
@iam = iam
@sts = sts
@options = options
@account_id = nil
end

def create_user(user_name, attrs)
@@ -67,6 +69,14 @@ def delete_user(user_name, attrs)
@iam.delete_signing_certificate(:user_name => user_name, :certificate_id => certificate_id)
end

mfa_devices = @iam.list_mfa_devices(:user_name => user_name).map {|resp|
resp.mfa_devices
}.flatten

mfa_devices.each do |md|
@iam.deactivate_mfa_device(:user_name => user_name, :serial_number => md.serial_number)
end

@iam.delete_user(:user_name => user_name)
end
end
@@ -377,37 +387,37 @@ def create_managed_policy(policy_name, attrs)
end
end

def delete_managed_policy(policy_name)
def delete_managed_policy(policy_name, policy_path)
log(:info, "Delete ManagedPolicy `#{policy_name}`", :color => :red)

unless_dry_run do
policy_versions = @iam.list_policy_versions(
:policy_arn => policy_arn(policy_name),
:policy_arn => policy_arn(policy_name, policy_path),
:max_items => MAX_POLICY_VERSIONS
)

policy_versions.versions.reject {|pv|
pv.is_default_version
}.each {|pv|
@iam.delete_policy_version(
:policy_arn => policy_arn(policy_name),
:policy_arn => policy_arn(policy_name, policy_path),
:version_id => pv.version_id
)
}

@iam.delete_policy(
:policy_arn => policy_arn(policy_name)
:policy_arn => policy_arn(policy_name, policy_path)
)
end
end

def update_managed_policy(policy_name, policy_document, old_policy_document)
def update_managed_policy(policy_name, policy_path, policy_document, old_policy_document)
log(:info, "Update ManagedPolicy `#{policy_name}`", :color => :green)
log(:info, Miam::Utils.diff(old_policy_document, policy_document, :color => @options[:color]), :color => false)

unless_dry_run do
policy_versions = @iam.list_policy_versions(
:policy_arn => policy_arn(policy_name),
:policy_arn => policy_arn(policy_name, policy_path),
:max_items => MAX_POLICY_VERSIONS
)

@@ -417,19 +427,27 @@ def update_managed_policy(policy_name, policy_document, old_policy_document)
}.sort_by {|pv| pv.version_id[1..-1].to_i }.first

@iam.delete_policy_version(
:policy_arn => policy_arn(policy_name),
:policy_arn => policy_arn(policy_name, policy_path),
:version_id => delete_policy_version.version_id
)
end

@iam.create_policy_version(
:policy_arn => policy_arn(policy_name),
:policy_arn => policy_arn(policy_name, policy_path),
:policy_document => encode_document(policy_document),
set_as_default: true
)
end
end

def password_policy
return @password_policy if instance_variable_defined?(:@password_policy)

@password_policy = @iam.get_account_password_policy.password_policy
rescue Aws::IAM::Errors::NoSuchEntity
@password_policy = nil
end

private

def encode_document(policy_document)
@@ -455,11 +473,13 @@ def unless_dry_run
yield unless @options[:dry_run]
end

def user_id
@user_id ||= @iam.get_user.user.user_id
def account_id
# https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
# http://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
@account_id ||= @sts.get_caller_identity.account
end

def policy_arn(policy_name)
"arn:aws:iam::#{user_id}:policy/#{policy_name}"
def policy_arn(policy_name, policy_path)
File.join("arn:aws:iam::#{account_id}:policy", policy_path, policy_name)
end
end
@@ -193,10 +193,16 @@ def output_managed_policy(policy_name, attrs)
end

def target_matched?(name)
result = true

if @options[:exclude]
result &&= name !~ @options[:exclude]
end

if @options[:target]
name =~ @options[:target]
else
true
result &&= name =~ @options[:target]
end

result
end
end
@@ -1,6 +1,7 @@
# coding: utf-8
class Miam::Exporter
AWS_MANAGED_POLICY_PREFIX = 'arn:aws:iam::aws:'
AWS_CN_MANAGED_POLICY_PREFIX = 'arn:aws-cn:iam::aws:'

def self.export(iam, options = {})
self.new(iam, options).export
@@ -200,7 +201,7 @@ def export_policies(policies)
result = {}

Parallel.each(policies, :in_threads => @concurrency) do |policy|
if policy.arn.start_with?(AWS_MANAGED_POLICY_PREFIX)
if policy.arn.start_with?(AWS_MANAGED_POLICY_PREFIX) or policy.arn.start_with?(AWS_CN_MANAGED_POLICY_PREFIX)
next
end

@@ -262,10 +263,10 @@ def get_account_authorization_details
@iam.get_account_authorization_details.each do |resp|
keys.each do |key|
account_authorization_details[key].concat(resp[key])
end

unless @options[:no_progress]
progressbar.increment
end
unless @options[:no_progress]
progressbar.increment
end
end

@@ -1,13 +1,19 @@
class Miam::PasswordManager
include Miam::Logger::Helper

LOWERCASES = ('a'..'z').to_a
UPPERCASES = ('A'..'Z').to_a
NUMBERS = ('0'..'9').to_a
SYMBOLS = "!@\#$%^&*()_+-=[]{}|'".split(//)

def initialize(output, options = {})
@output = output
@options = options
end

def identify(user, type)
password = mkpasswd
def identify(user, type, policy)
password = mkpasswd(policy)
log(:info, "mkpasswd: #{password}")
puts_password(user, type, password)
password
end
@@ -22,8 +28,21 @@ def puts_password(user, type, password)

private

def mkpasswd(len = 8)
[*1..9, *'A'..'Z', *'a'..'z'].shuffle.slice(0, len).join
def mkpasswd(policy)
chars = []
len = 8

if policy
len = policy.minimum_password_length if policy.minimum_password_length > len
chars << LOWERCASES.shuffle.first if policy.require_lowercase_characters
chars << UPPERCASES.shuffle.first if policy.require_uppercase_characters
chars << NUMBERS.shuffle.first if policy.require_numbers
chars << SYMBOLS.shuffle.first if policy.require_symbols

len -= chars.length
end

(chars + [*1..9, *'A'..'Z', *'a'..'z'].shuffle.slice(0, len)).shuffle.join
end

def open_output
@@ -1,3 +1,3 @@
module Miam
VERSION = '0.2.3'
VERSION = '0.2.4.beta12'
end
@@ -8,8 +8,6 @@ Gem::Specification.new do |spec|
spec.version = Miam::VERSION
spec.authors = ['Genki Sugawara']
spec.email = ['sgwr_dts@yahoo.co.jp']
spec.summary = %q{TODO: Write a short summary. Required.}
spec.description = %q{TODO: Write a longer description. Optional.}
spec.summary = %q{Miam is a tool to manage IAM.}
spec.description = %q{Miam is a tool to manage IAM. It defines the state of IAM using DSL, and updates IAM according to DSL.}
spec.homepage = 'https://github.com/codenize-tools/miam'
@@ -31,4 +29,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'rspec', '>= 3.0.0'
spec.add_development_dependency 'rspec-instafail'
spec.add_development_dependency 'coveralls'
spec.add_development_dependency 'nokogiri'
end

0 comments on commit b803336

Please sign in to comment.
You can’t perform that action at this time.