himeko 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ require 'aws-sdk-dynamodb'
2
+ require 'himeko/user_mimicking_role'
3
+
4
+ module Himeko
5
+ class RoleManager
6
+ def initialize(iam:, prefix:, path:, ttl: 86400, dynamodb_table:)
7
+ @iam = iam
8
+ @prefix = prefix
9
+ @path = path
10
+ @table = dynamodb_table
11
+ @ttl = ttl
12
+ end
13
+
14
+ attr_reader :iam, :prefix, :path, :table, :ttl
15
+
16
+ def fetch(username, recreate: false)
17
+ item = table.query(
18
+ limit: 1,
19
+ select: 'ALL_ATTRIBUTES',
20
+ key_condition_expression: 'username = :username',
21
+ expression_attribute_values: {":username" => username},
22
+ ).items.first
23
+
24
+ if recreate || item.nil?
25
+ begin
26
+ return create(username)
27
+ rescue Aws::IAM::Errors::EntityAlreadyExists
28
+ remove(username, delete_record: false)
29
+ retry
30
+ end
31
+ end
32
+
33
+ table.update_item(
34
+ key: {
35
+ 'username' => username,
36
+ },
37
+ update_expression: 'SET expires_at = :expires_at',
38
+ expression_attribute_values: {
39
+ ':expires_at' => (Time.now + ttl).to_i,
40
+ },
41
+ )
42
+
43
+ item.fetch('role_arn')
44
+ end
45
+
46
+ def remove(username, role_name: nil, delete_record: true)
47
+ role_name ||= role_name_for_username(username)
48
+
49
+ iam.list_attached_role_policies(role_name: role_name).each.flat_map(&:attached_policies).map(&:policy_arn).each do |policy_arn|
50
+ iam.detach_role_policy(role_name: role_name, policy_arn: policy_arn)
51
+ end
52
+ iam.list_role_policies(role_name: role_name).policy_names.each do |policy_name|
53
+ iam.delete_role_policy(role_name: role_name, policy_name: policy_name)
54
+ end
55
+ iam.delete_role(role_name: role_name)
56
+
57
+ if delete_record
58
+ table.delete_item(
59
+ key: {
60
+ 'username' => username,
61
+ },
62
+ )
63
+ end
64
+ rescue Aws::IAM::Errors::NoSuchEntity
65
+ # do nothing
66
+ end
67
+
68
+ def create(username)
69
+ role_arn = UserMimickingRole.new(iam, username, role_name_for_username(username), path).create
70
+
71
+ table.update_item(
72
+ key: {
73
+ 'username' => username,
74
+ },
75
+ update_expression: 'SET expires_at = :expires_at, role_arn = :role_arn',
76
+ expression_attribute_values: {
77
+ ':expires_at' => (Time.now + ttl).to_i,
78
+ ':role_arn' => role_arn,
79
+ },
80
+ )
81
+
82
+ role_arn
83
+ end
84
+
85
+ def prune
86
+ table.client.scan(
87
+ table_name: table.table_name,
88
+ select: 'ALL_ATTRIBUTES',
89
+ filter_expression: 'expires_at < :now',
90
+ expression_attribute_values: {
91
+ ':now' => Time.now.to_i,
92
+ },
93
+ ).each do |page|
94
+ page.items.each do |item|
95
+ puts "==> #{item['username']} (#{item['role_arn']})"
96
+ remove(item['username'], role_name: item['role_arn'].split(?/)[-1])
97
+ table.delete_item(key: {'username' => item['username']})
98
+ end
99
+ end
100
+ end
101
+
102
+ def role_name_for_username(username)
103
+ "#{prefix}#{username}"
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,148 @@
1
+ require 'uri'
2
+ require 'json'
3
+ require 'aws-sdk-iam'
4
+
5
+ module Himeko
6
+ class UserMimickingRole
7
+ def initialize(iam, username, role_name, path = nil, driver: nil)
8
+ @driver = driver || Driver.new(iam)
9
+ @username = username
10
+ @role_name = role_name
11
+ @path = path
12
+ end
13
+
14
+ attr_reader :driver, :username, :role_name, :path
15
+
16
+ # @return [String] role arn
17
+ def create
18
+ arn = driver.create_role(
19
+ path: path,
20
+ role_name: role_name,
21
+ assume_role_policy_document: assume_role_policy_document,
22
+ )
23
+
24
+ managed_policies.each do |policy_arn|
25
+ driver.attach_role_policy(role_name, policy_arn)
26
+ end
27
+
28
+ policies.each do |policy_name, policy|
29
+ driver.put_role_policy(role_name, policy_name, policy)
30
+ end
31
+
32
+ return arn
33
+ end
34
+
35
+ def user
36
+ @user ||= driver.get_user(username)
37
+ end
38
+
39
+ def account_id
40
+ user.arn.split(?:)[4]
41
+ end
42
+
43
+ def assume_role_policy_document
44
+ {
45
+ "Version"=>"2012-10-17",
46
+ "Statement"=>[
47
+ {
48
+ "Effect"=>"Allow",
49
+ "Principal"=>{
50
+ "AWS"=>[
51
+ "arn:aws:iam::#{account_id}:root",
52
+ ]
53
+ },
54
+ "Action"=>"sts:AssumeRole",
55
+ "Condition"=>{},
56
+ },
57
+ ],
58
+ }
59
+ end
60
+
61
+ def groups
62
+ @groups ||= driver.list_groups_for_user(username)
63
+ end
64
+
65
+ def managed_policies
66
+ @managed_policies ||= [
67
+ *driver.list_attached_user_policies(username),
68
+ *groups.flat_map do |group_name|
69
+ driver.list_attached_group_policies(group_name)
70
+ end,
71
+ ].sort.uniq
72
+ end
73
+
74
+ def policies
75
+ @policies ||= [
76
+ *driver.list_user_policies(username).map do |policy_name|
77
+ [policy_name, driver.get_user_policy(username, policy_name)]
78
+ end,
79
+ *groups.flat_map do |group_name|
80
+ driver.list_group_policies(group_name).map do |policy_name|
81
+ ["#{group_name}_#{policy_name}"[0..127], driver.get_group_policy(group_name, policy_name)]
82
+ end
83
+ end,
84
+ ].to_h
85
+ end
86
+
87
+ class Driver
88
+ def initialize(iam)
89
+ @iam = iam
90
+ end
91
+
92
+ attr_reader :iam
93
+
94
+ def get_user(username)
95
+ iam.get_user(user_name: username).user
96
+ end
97
+
98
+ def list_attached_user_policies(username)
99
+ iam.list_attached_user_policies(user_name: username).each.flat_map(&:attached_policies).map(&:policy_arn)
100
+ end
101
+
102
+ def list_user_policies(username)
103
+ iam.list_user_policies(user_name: username).policy_names
104
+ end
105
+
106
+ def get_user_policy(username, policy_name)
107
+ URI.decode_www_form_component(iam.get_user_policy(user_name: username, policy_name: policy_name).policy_document)
108
+ end
109
+
110
+ def list_groups_for_user(username)
111
+ iam.list_groups_for_user(user_name: username).groups.map(&:group_name)
112
+ end
113
+
114
+ def list_group_policies(group_name)
115
+ iam.list_group_policies(group_name: group_name).policy_names
116
+ end
117
+
118
+ def get_group_policy(group_name, policy_name)
119
+ URI.decode_www_form_component(iam.get_group_policy(group_name: group_name, policy_name: policy_name).policy_document)
120
+ end
121
+
122
+ def list_attached_group_policies(group_name)
123
+ iam.list_attached_group_policies(group_name: group_name).each.flat_map(&:attached_policies).map(&:policy_arn)
124
+ end
125
+
126
+ def create_role(path:, role_name:, assume_role_policy_document:, max_session_duration: 43200)
127
+ iam.create_role(
128
+ path: path,
129
+ role_name: role_name,
130
+ assume_role_policy_document: assume_role_policy_document.to_json,
131
+ max_session_duration: max_session_duration
132
+ ).role.arn
133
+ end
134
+
135
+ def attach_role_policy(role_name, policy_arn)
136
+ iam.attach_role_policy(role_name: role_name, policy_arn: policy_arn)
137
+ end
138
+
139
+ def put_role_policy(role_name, policy_name, policy)
140
+ iam.put_role_policy(
141
+ role_name: role_name,
142
+ policy_name: policy_name,
143
+ policy_document: policy,
144
+ )
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,3 @@
1
+ module Himeko
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,196 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: himeko
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sorah Fukumori
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-09-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-iam
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-dynamodb
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack-protection
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: erubi
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ description:
140
+ email:
141
+ - sorah@cookpad.com
142
+ executables:
143
+ - himeko-clean-roles
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - ".gitignore"
148
+ - ".rspec"
149
+ - ".travis.yml"
150
+ - Gemfile
151
+ - Gemfile.lock
152
+ - LICENSE.txt
153
+ - README.md
154
+ - Rakefile
155
+ - app/public/style.css
156
+ - app/views/iam_limit_exceeded_error.erb
157
+ - app/views/index.erb
158
+ - app/views/keys.erb
159
+ - app/views/layout.erb
160
+ - app/views/new_key.erb
161
+ - app/views/no_user_error.erb
162
+ - bin/console
163
+ - bin/setup
164
+ - config.ru
165
+ - exe/himeko-clean-roles
166
+ - himeko.gemspec
167
+ - lib/himeko.rb
168
+ - lib/himeko/app.rb
169
+ - lib/himeko/role_manager.rb
170
+ - lib/himeko/user_mimicking_role.rb
171
+ - lib/himeko/version.rb
172
+ homepage: https://github.com/sorah/himeko
173
+ licenses:
174
+ - MIT
175
+ metadata: {}
176
+ post_install_message:
177
+ rdoc_options: []
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ requirements: []
191
+ rubyforge_project:
192
+ rubygems_version: 2.7.7
193
+ signing_key:
194
+ specification_version: 4
195
+ summary: AWS IAM access key self service & management console federated login
196
+ test_files: []