zepplen_aws 0.0.2 → 0.0.3

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,395 @@
1
+ #Copyright 2013 Mark Trimmer
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module ZepplenAWS
16
+
17
+ # = Manage Server User
18
+ #
19
+ # This class is intended to be used by both the CLI scripts provided, and 3rd party tools
20
+ # written by you!
21
+ #
22
+ class ServerUser
23
+
24
+ # = ServerUser
25
+ # If you are using the default DynamoDB table name 'users' only user_name is required to create
26
+ # a ServerUser object. If you have deviated from this standard you must pass dynamo_table name in
27
+ # as either a paramater, or via the Env class.
28
+ #
29
+ # @param [String] User Name
30
+ # @param optional [String] Name of DynamoDB to read settings and users from
31
+ # @param optional [AWS::DynamoDB::Item] DynamoDB Item reflecting the requested user (used to prevent multiple DB Hits)
32
+ # @param optional [AWS::DynamoDB::Item] DynamoDB Item reflecting the metadata setings (used to prevent multiple DB Hits)
33
+ # @param optional [ZepplenAWS::ServerUsers] ServerUsers object (used to prevent multiple DB Hits)
34
+ def initialize(user_name, dynamo_table = nil, user_row=nil, metadata_row=nil, server_users=nil)
35
+ @user_name = user_name
36
+ @user_data = {}
37
+ @remove_s3_files = []
38
+ @dirty = false
39
+ @marshaled_columns = [/^TAG__/, /^files$/]
40
+
41
+ @dynamo_table = dynamo_table || Env[:dynamo_table] || 'users'
42
+
43
+ if(@dynamo_table == nil)
44
+ raise Exceptions::Users::MissingOption, "DynamoDB Table Name Required"
45
+ end
46
+
47
+ @dynamo = AWS::DynamoDB.new()
48
+ @table = @dynamo.tables[@dynamo_table]
49
+ @table.hash_key = {:type => :string}
50
+ @table.range_key = {:user_name => :string}
51
+ if(user_row != nil && user_row.attributes[:user_name] == user_name)
52
+ @user_row = user_row
53
+ else
54
+ @user_row = @table.items['USER', @user_name]
55
+ end
56
+ get_user_data(@user_row.attributes)
57
+
58
+ if(metadata_row != nil && metadata_row.attributes[:user_name] == @metadata_label)
59
+ @metadata_row = metadata_row
60
+ else
61
+ @metadata_row = @table.items['METADATA', '__metadata__']
62
+ end
63
+
64
+ if(server_users != nil)
65
+ @server_users = server_users
66
+ else
67
+ @server_users = ServerUsers.new(@dynamo_table)
68
+ end
69
+
70
+ if(!@user_row.exists?())
71
+ init_user()
72
+ end
73
+ end
74
+
75
+ # User Name
76
+ #
77
+ # @return [String]
78
+ def user_name()
79
+ return @user_name
80
+ end
81
+
82
+ # Hash of files associated with user's profile
83
+ #
84
+ # @return [Hash]
85
+ def files()
86
+ return @user_data['files']
87
+ end
88
+
89
+ # Remove file from user's profile
90
+ #
91
+ # Note: the file will be removed from S3 Before object is saved!
92
+ #
93
+ # @param [String] Remote file location of file to remove
94
+ def remove_file(file_name)
95
+ if(@user_data['files'].has_key?(file_name))
96
+ s3 = AWS::S3.new()
97
+ bucket = s3.buckets[@server_users.user_file_bucket]
98
+ bucket.objects[@user_data['files'][file_name]['s3_path']].delete()
99
+ @user_data['files'].delete(file_name)
100
+ end
101
+ @dirty = true
102
+ return nil
103
+ end
104
+
105
+ # Add File Path
106
+ #
107
+ # Note: This will add the file to S3 Before the object is saved!
108
+ #
109
+ # @param [String] Location of file to add
110
+ # @param [String] Destination in the user's home dir to place file on remove servers
111
+ # @param [String] Linux file mode (eg: '600')
112
+ def add_file_path(local_path, remote_path, file_mode)
113
+ if(!File.readable?(local_path))
114
+ raise "Can Not Read #{local_path}"
115
+ end
116
+ data = File.read(local_path)
117
+ add_file_data(data, remote_path, file_mode)
118
+ return nil
119
+ end
120
+
121
+ # Add File Path
122
+ #
123
+ # Note: This will add the file to S3 Before the object is saved!
124
+ #
125
+ # @param [String] File data
126
+ # @param [String] Destination in the user's home dir to place file on remove servers
127
+ # @param [String] Linux file mode (eg: '600')
128
+ def add_file_data(data, remote_path, file_mode)
129
+ s3_path = @server_users.user_file_bucket()
130
+ if(!s3_path)
131
+ raise 'User Files are not enabled. Please re-run --configure'
132
+ end
133
+ s3 = AWS::S3.new()
134
+ bucket = s3.buckets[@server_users.user_file_bucket]
135
+ file_path = "#{@user_name}/#{remote_path.sub(/^\//, '')}"
136
+ bucket.objects[file_path].write(data)
137
+ file_size = bucket.objects[file_path].content_length.to_i
138
+ @user_data['files'][remote_path] = {'s3_path' => file_path, 'mode' => file_mode, 'content_length' => file_size}
139
+ @dirty = true
140
+ return nil
141
+ end
142
+
143
+ # Shell
144
+ #
145
+ # @return [String] User's shell
146
+ def shell()
147
+ return @user_data['shell']
148
+ end
149
+
150
+ # Set Shell
151
+ #
152
+ # @param [String] Set user's shell. (Default: /bin/bash)
153
+ def shell=(shell)
154
+ @user_data['shell'] = shell
155
+ @dirty = true
156
+ return nil
157
+ end
158
+
159
+ # User State
160
+ #
161
+ # @return [String] User State (ACTIVE/INACTIVE)
162
+ def state()
163
+ return @user_data['state']
164
+ end
165
+
166
+ # Set User State
167
+ #
168
+ # @param [String/Symbol] User State (ACTIVE/INACTIVE)
169
+ def state=(state)
170
+ @user_data['state'] = (state.upcase.to_s == 'ACTIVE' ? 'ACTIVE' : 'INACTIVE')
171
+ @dirty = true
172
+ return nil
173
+ end
174
+
175
+ # Public Key
176
+ #
177
+ # @return [String] SSH Public Key
178
+ def public_key()
179
+ return @user_data['public_key']
180
+ end
181
+
182
+ # Set Public Key
183
+ #
184
+ # Setting the public key also sets the public_key_expire date.
185
+ #
186
+ # @param [String] SSH Public Key
187
+ def public_key=(key)
188
+ if(@user_data['public_key'] != key)
189
+ @user_data['public_key'] = key
190
+ @user_data['public_key_expire'] = (Date.today + @server_users.max_key_age).to_s
191
+ @dirty = true
192
+ end
193
+ return nil
194
+ end
195
+
196
+ # Public Key Expire Date
197
+ #
198
+ # This is only set when the public_key value is set
199
+ #
200
+ # @return [String] Public Key Expire Date (String)
201
+ def public_key_expire()
202
+ return @user_data['public_key_expire']
203
+ end
204
+
205
+ # Full Name
206
+ #
207
+ # @return [String] User's full name
208
+ def full_name()
209
+ return @user_data['full_name']
210
+ end
211
+
212
+ # Set Full Name
213
+ #
214
+ # @param [String] User's full name
215
+ def full_name=(name)
216
+ if(@user_data['full_name'] != name)
217
+ @user_data['full_name'] = name
218
+ @dirty = true
219
+ end
220
+ return nil
221
+ end
222
+
223
+ # User ID
224
+ #
225
+ # Linux User ID (set automaticaly at user creation)
226
+ #
227
+ # @return [Integer] Linux user id
228
+ def user_id()
229
+ return @user_data['user_id']
230
+ end
231
+
232
+ # Identity
233
+ #
234
+ # Used to identify if the user entry has changed. Incremented on each write to DynamoDB for this user.
235
+ #
236
+ # @return [Integer] Identity
237
+ def identity()
238
+ return @user_data['identity']
239
+ end
240
+
241
+ # Remove Access
242
+ #
243
+ # Remove's a users access from a Tag Name => Tag Value combination
244
+ #
245
+ # @param [String] EC2 Tag Name to target (Case Sensitive)
246
+ # @param [String] EC2 Tag Value to target (Case Sensitive)
247
+ def remove_access(tag_name, tag_value)
248
+ if(!@metadata_row.attributes[:tags].include?(tag_name))
249
+ raise "User Access Tag Name #{tag_name} Not In [#{@metadata_row.attributes[:tags].to_a.join(', ')}]"
250
+ end
251
+ tag_key_name = "TAG__#{tag_name}"
252
+ if(!@user_data.has_key?(tag_key_name))
253
+ return
254
+ end
255
+ if(!@user_data[tag_key_name].has_key?(tag_value))
256
+ return
257
+ end
258
+ @user_data[tag_key_name].delete(tag_value)
259
+ @dirty = true
260
+ return nil
261
+ end
262
+
263
+ # Add Access
264
+ #
265
+ # Users are targeted to a server by EC2 Tags. The list of valid tags is listed at the environmen level.
266
+ # run --configure to change the list of valid tags
267
+ #
268
+ # @param [String] EC2 Tag Name
269
+ # @param [String] EC2 Tag Value
270
+ # @param [Object] Grant Sudo access. (nil: no sudo, not_nil: sudo access)
271
+ def add_access(tag_name, tag_value, sudo)
272
+ if(!@metadata_row.attributes[:tags].include?(tag_name))
273
+ raise "User Access Tag Name #{tag_name} Not In [#{@metadata_row.attributes[:tags].to_a.join(', ')}]"
274
+ end
275
+ tag_key_name = "TAG__#{tag_name}"
276
+ if(!@user_data.has_key?(tag_key_name))
277
+ @user_data[tag_key_name] = {}
278
+ end
279
+ value_data = {'sudo' => sudo != nil}
280
+ if(@user_data[tag_key_name][tag_value] != value_data)
281
+ @user_data[tag_key_name][tag_value] = value_data
282
+ @dirty = true
283
+ end
284
+ return nil
285
+ end
286
+
287
+ # Save
288
+ #
289
+ # This call will only write to Dynamo if a change has been made.
290
+ # When we update the row we also update the identity of the row. This will allows us to save
291
+ # calls to Dynamo/S3 when we write the users to the remote servers
292
+ def save()
293
+ if(!@dirty)
294
+ return
295
+ end
296
+ @user_row.attributes.update do |u|
297
+ @user_data.each_pair do |key,value|
298
+ if(key != 'user_name' && key != 'identity' && key != 'type')
299
+ if(value == nil)
300
+ u.delete(key)
301
+ else
302
+ marshal = false
303
+ @marshaled_columns.each do |regex|
304
+ if(key.match(regex))
305
+ marshal = true
306
+ break
307
+ end
308
+ end
309
+ if(marshal)
310
+ value = value.to_json.force_encoding('UTF-8')
311
+ end
312
+ u.set(key => value)
313
+ end
314
+ end
315
+ end
316
+ if(!@user_data['user_id'])
317
+ user_id = @metadata_row.attributes.add({:next_uid => 1}, :return => :updated_old)
318
+ u.set(:user_id => user_id['next_uid'])
319
+ end
320
+ u.add(:identity => 1)
321
+ end
322
+ @metadata_row.attributes.add(:identity => 1)
323
+ @dirty = false
324
+ return nil
325
+ end
326
+
327
+ # Display User
328
+ #
329
+ # Prints the user's profile to the screen with console colors enabled
330
+ def display()
331
+ puts "Full Name: ".green()+"#{@user_data['full_name']}".light_blue
332
+ puts "UserName: ".green()+"#{@user_name}".light_blue
333
+ puts "State: ".green()+"#{@user_data['state']}".send(@user_data['state'] == 'ACTIVE' ? :light_blue : :red)
334
+ puts "Key Expire: ".green()+"#{@user_data['public_key_expire']}".light_blue
335
+ puts "Shell: ".green()+"#{@user_data['shell']}".light_blue
336
+ puts
337
+ puts "User Access".green
338
+ access_tags.each_pair do |tag_name, tag_data|
339
+ tag_data.each_pair do |tag_value, server_data|
340
+ puts " #{tag_name}".green+" => ".light_white+"#{tag_value}".send(server_data['sudo'] ? :red : :light_cyan)
341
+ end
342
+ end
343
+ puts
344
+ puts "Files".green
345
+ @user_data['files'].each_pair do |remote_path, file_data|
346
+ puts " ~/#{remote_path}".light_cyan+" (#{file_data['content_length']})".yellow
347
+ end
348
+ puts
349
+ return nil
350
+ end
351
+
352
+ # Access Tags
353
+ #
354
+ # Returns a list of the EC2 Tags user has associated to their profile
355
+ #
356
+ # @return [Hash] EC2 Tag names and their values to match on
357
+ def access_tags()
358
+ tag_columns = {}
359
+ @user_data.each_pair do |key, value|
360
+ key.match(/^TAG__(.*)/) do |match|
361
+ tag_columns[match[1]] = value
362
+ end
363
+ end
364
+ return tag_columns
365
+ end
366
+
367
+ private
368
+
369
+ def init_user()
370
+ @user_data['type'] = 'USER'
371
+ @user_data['shell'] = '/bin/bash'
372
+ @user_data['state'] = 'ACTIVE'
373
+ @user_data['files'] = {}
374
+ @user_data['identity'] = 0
375
+ @dirty = true
376
+ end
377
+
378
+ def get_user_data(attributes)
379
+ attributes.to_h.each_pair do |key, value|
380
+ marshaled = false
381
+ @marshaled_columns.each do |regex|
382
+ if(key.match(regex))
383
+ marshaled = true
384
+ break
385
+ end
386
+ end
387
+ if(marshaled)
388
+ value = JSON.load(value)
389
+ end
390
+ @user_data[key] = value
391
+ end
392
+ end
393
+
394
+ end
395
+ end
@@ -0,0 +1,259 @@
1
+ #Copyright 2013 Mark Trimmer
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module ZepplenAWS
16
+
17
+ # = Manage Server Users
18
+ #
19
+ # This class is intended to be used by both the CLI scripts provided, and 3rd party tools
20
+ # written by you!
21
+ #
22
+ # The metadata for the environment (options set using the configure method), are stored
23
+ # in a DynamoDB row along with the users. The default name for this row is '__metadata__'.
24
+ #
25
+ #
26
+ class ServerUsers
27
+
28
+ # Server Users
29
+ #
30
+ # @param optional [String] Dynamo table name, if not provided will pull from Env, or default to 'users'
31
+ def initialize(dynamo_table = nil)
32
+ @dynamo_table = dynamo_table || Env[:dynamo_table] || 'users'
33
+
34
+ # I don't there there is any way to reach this error state....
35
+ if(@dynamo_table == nil)
36
+ raise Exceptions::Users::MissingOption, "DynamoDB Table Name Required"
37
+ end
38
+
39
+ @dynamo = AWS::DynamoDB.new()
40
+ @table = @dynamo.tables[@dynamo_table]
41
+ @table.hash_key = {:type => :string}
42
+ @table.range_key = {:user_name => :string}
43
+ if(!@table.exists?)
44
+ raise Exceptions::Users::NoDynamoTable, "Could Not Access DynamoDB Table: #{@dynamo_table}"
45
+ end
46
+ @local_user_data = {}
47
+
48
+ @metadata = @table.items['METADATA', '__metadata__']
49
+ end
50
+
51
+ # Exists?
52
+ #
53
+ # Reflects if there is a _metadata_ row availible. If not use needs to configure environment.
54
+ def exists?()
55
+ return @metadata.exists?
56
+ end
57
+
58
+ # Identity
59
+ #
60
+ # This number is incremented any time a configuration changes, or a user profile is updated.
61
+ # We use this to reduce the number of dynamo calls each client has to make.
62
+ #
63
+ # @return [Integer] Identity
64
+ def identity()
65
+ return @metadata.attributes[:identity].to_i
66
+ end
67
+
68
+ # User File Bucket
69
+ #
70
+ # @return [String] Name of S3 Bucket user files are stored in
71
+ def user_file_bucket()
72
+ return @metadata.attributes[:user_file_bucket]
73
+ end
74
+
75
+ # Set User File Bucket
76
+ #
77
+ # @param [String] Name of S3 Bucket to store user files in. Nil to disable feature
78
+ def user_file_bucket=(s3_path)
79
+ update_metadata(:user_file_bucket => s3_path)
80
+ return nil
81
+ end
82
+
83
+ # Max Key Age
84
+ #
85
+ # Number of Days to continue using an SSH Key before it is expired and removed from all servers
86
+ #
87
+ # @return [Integer] Max key age (days)
88
+ def max_key_age()
89
+ return @metadata.attributes[:max_key_age].to_i
90
+ end
91
+
92
+ # Set Max Key Age
93
+ #
94
+ # Number of Days to continue using an SSH Key before it is expired and removed from all servers
95
+ #
96
+ # @param [Integer] Max key age (days)
97
+ def max_key_age=(key_age)
98
+ update_metadata(:key_age => key_age)
99
+ return nil
100
+ end
101
+
102
+ # Next UID
103
+ #
104
+ # Next linux uid to use. We make sure that each user's uid is consistant accross all servers. This
105
+ # prevents users from having broken permissions when they are removed and re-added to an instance.
106
+ #
107
+ # @return [Integet] Next uid
108
+ def next_uid()
109
+ return @metadata.attributes[:next_uid].to_i
110
+ end
111
+
112
+ # Set Next UID
113
+ #
114
+ # Next linux uid to use. We make sure that each user's uid is consistant accross all servers. This
115
+ # prevents users from having broken permissions when they are removed and re-added to an instance.
116
+ #
117
+ # Warning: Be sure not to set to a range already used by existing users, or existing accounts on your servers.
118
+ #
119
+ # @param [Integer] Next uid
120
+ def next_uid=(next_uid)
121
+ update_metadata(:next_uid => next_uid)
122
+ return nil
123
+ end
124
+
125
+ # Sudo Group
126
+ #
127
+ # Group that user will be added to grant sudo access to an instance.
128
+ #
129
+ # @return [String] sudo group
130
+ def sudo_group()
131
+ return @metadata.attributes[:sudo_group]
132
+ end
133
+
134
+ # Set Sudo Group
135
+ #
136
+ # Group that user will be added to grant sudo access to an instance. Please be sure this group is configured
137
+ # correctly on all of your instances. Group should grant NOPASSWD access, as this script will NOT set a
138
+ # passwor for any user.
139
+ #
140
+ # @param [String] sudo group
141
+ def sudo_group=(sudo_group)
142
+ update_metadata(:sudo_group => sudo_group)
143
+ return nil
144
+ end
145
+
146
+ # Tags
147
+ #
148
+ # Returns the list of EC2 tags accessible to taget users's access.
149
+ #
150
+ # @return [Array] List of EC2 tags valid for granting user access to servers.
151
+ def tags()
152
+ return @metadata.attributes[:tags].to_a
153
+ end
154
+
155
+ # Set Tags
156
+ #
157
+ # Overwrite existing tags with new Array of EC2 Tags
158
+ #
159
+ # @param [Array[String]] EC2 Tags that are valid for granting user access to servers.
160
+ def tags=(tags)
161
+ update_metadata(:tags => tags)
162
+ @metadata.attributes.update do |u|
163
+ u.set(:tags => tags)
164
+ u.add(:idenity => 1)
165
+ end
166
+ return nil
167
+ end
168
+
169
+ # Add Tags
170
+ #
171
+ # Add tags witn Array of EC2 Tags
172
+ #
173
+ # @param [Array[String]] EC2 Tags that are valid for granting user access to servers.
174
+ def add_tags(tags)
175
+ @metadata.attributes.update do |u|
176
+ u.add(:tags => tags)
177
+ u.add(:idenity => 1)
178
+ end
179
+ return nil
180
+ end
181
+
182
+ # Remove Tags
183
+ #
184
+ # Revmove tags witn Array of EC2 Tags
185
+ #
186
+ # @param [Array[String]] EC2 Tags that are no longer valid for granting user access to servers.
187
+ def remove_tags(tags)
188
+ @metadata.attributes.update do |u|
189
+ u.delete(:tags => tags)
190
+ u.add(:idenity => 1)
191
+ end
192
+ return nil
193
+ end
194
+
195
+ # Configure Envoronment
196
+ #
197
+ # Allows users to set multiple parameters at once
198
+ # == Valid Parameters
199
+ # :next_uid, :max_key_age, :tags, :sudo_group, :user_file_bucket
200
+ #
201
+ # @param [Hash] Parameter values to set
202
+ def configure(config)
203
+ valid_configs = [:next_uid, :max_key_age, :tags, :sudo_group]
204
+ to_use_config = config.select{|k,v| valid_configs.include?(k)}
205
+ @metadata.attributes.update do |item_data|
206
+ item_data.set(to_use_config)
207
+ if(config.has_key?(:user_file_bucket))
208
+ if(config[:user_file_bucket])
209
+ item_data.set(:user_file_bucket => config[:user_file_bucket])
210
+ else
211
+ item_data.delete(:user_file_bucket)
212
+ end
213
+ end
214
+ if(@metadata.attributes[:identity] == nil)
215
+ item_data.set(:identity => 0)
216
+ else
217
+ item_data.add(:identity => 1)
218
+ end
219
+ end
220
+ end
221
+
222
+ # Users
223
+ #
224
+ # Returns an array of ServerUser objects
225
+ #
226
+ # @return [Hash[ServerUser]]
227
+ def users()
228
+ users = {}
229
+ @table.items.where(:type => 'USER').each do |user_row|
230
+ users[user_row.attributes[:user_name]] = ServerUser.new(user_row.attributes[:user_name], @dynamo_table, user_row, @metadata, self)
231
+ end
232
+ return users
233
+ end
234
+
235
+ private
236
+
237
+ def update_metadata(data)
238
+ @metadata.attributes.update do |u|
239
+ data.each_pair do |key, value|
240
+ if(value)
241
+ u.set(key => value)
242
+ else
243
+ u.delete(key)
244
+ end
245
+ end
246
+ u.add(:identity => 1)
247
+ end
248
+ end
249
+
250
+ def update_required?(local_users)
251
+ if(!File.readable?(local_users))
252
+ return true
253
+ end
254
+ @local_user_data = Yaml.load(local_users)
255
+ return false
256
+ end
257
+
258
+ end
259
+ end