keyrack 0.3.0.pre → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +8 -15
  4. data/Guardfile +6 -0
  5. data/LICENSE.txt +4 -2
  6. data/Rakefile +2 -56
  7. data/keyrack.gemspec +22 -104
  8. data/lib/keyrack.rb +9 -1
  9. data/lib/keyrack/database.rb +64 -98
  10. data/lib/keyrack/event.rb +13 -0
  11. data/lib/keyrack/exceptions.rb +4 -0
  12. data/lib/keyrack/group.rb +225 -0
  13. data/lib/keyrack/migrator.rb +45 -0
  14. data/lib/keyrack/runner.rb +98 -44
  15. data/lib/keyrack/site.rb +92 -0
  16. data/lib/keyrack/ui/console.rb +234 -95
  17. data/lib/keyrack/utils.rb +0 -18
  18. data/lib/keyrack/version.rb +3 -0
  19. data/test/fixtures/database-3.dat +0 -0
  20. data/test/helper.rb +11 -1
  21. data/test/integration/test_interactive_console.rb +139 -0
  22. data/test/unit/store/test_filesystem.rb +21 -0
  23. data/test/unit/store/test_ssh.rb +32 -0
  24. data/test/unit/test_database.rb +201 -0
  25. data/test/unit/test_event.rb +41 -0
  26. data/test/unit/test_group.rb +703 -0
  27. data/test/unit/test_migrator.rb +105 -0
  28. data/test/unit/test_runner.rb +407 -0
  29. data/test/unit/test_site.rb +181 -0
  30. data/test/unit/test_store.rb +13 -0
  31. data/test/unit/test_utils.rb +8 -0
  32. data/test/unit/ui/test_console.rb +495 -0
  33. metadata +98 -94
  34. data/Gemfile.lock +0 -45
  35. data/TODO +0 -4
  36. data/VERSION +0 -1
  37. data/features/console.feature +0 -103
  38. data/features/step_definitions/keyrack_steps.rb +0 -61
  39. data/features/support/env.rb +0 -16
  40. data/test/fixtures/aes +0 -0
  41. data/test/fixtures/config.yml +0 -4
  42. data/test/fixtures/id_rsa +0 -54
  43. data/test/keyrack/store/test_filesystem.rb +0 -25
  44. data/test/keyrack/store/test_ssh.rb +0 -36
  45. data/test/keyrack/test_database.rb +0 -111
  46. data/test/keyrack/test_runner.rb +0 -111
  47. data/test/keyrack/test_store.rb +0 -15
  48. data/test/keyrack/test_utils.rb +0 -41
  49. data/test/keyrack/ui/test_console.rb +0 -308
@@ -0,0 +1,92 @@
1
+ module Keyrack
2
+ class Site < Hash
3
+ def initialize(*args)
4
+ if args[0].is_a?(Hash)
5
+ hash = args[0]
6
+ if !hash.has_key?('name')
7
+ raise ArgumentError, "hash is missing the 'name' key"
8
+ end
9
+ if !hash['name'].is_a?(String)
10
+ raise ArgumentError, "name is not a String"
11
+ end
12
+ if !hash.has_key?('username')
13
+ raise ArgumentError, "hash is missing the 'username' key"
14
+ end
15
+ if !hash['username'].is_a?(String)
16
+ raise ArgumentError, "name is not a String"
17
+ end
18
+ if !hash.has_key?('password')
19
+ raise ArgumentError, "hash is missing the 'password' key"
20
+ end
21
+ if !hash['password'].is_a?(String)
22
+ raise ArgumentError, "name is not a String"
23
+ end
24
+ self.update(hash)
25
+ else
26
+ self['name'] = args[0]
27
+ self['username'] = args[1]
28
+ self['password'] = args[2]
29
+ end
30
+
31
+ @event_hooks = []
32
+ end
33
+
34
+ def change_attribute(name, value)
35
+ event = Event.new(self, 'change')
36
+ event.attribute_name = name
37
+ event.previous_value = self[name]
38
+ event.new_value = value
39
+
40
+ self[name] = value
41
+ trigger(event)
42
+ end
43
+
44
+ def name
45
+ self['name']
46
+ end
47
+
48
+ def name=(name)
49
+ change_attribute('name', name)
50
+ end
51
+
52
+ def username
53
+ self['username']
54
+ end
55
+
56
+ def username=(username)
57
+ change_attribute('username', username)
58
+ end
59
+
60
+ def password
61
+ self['password']
62
+ end
63
+
64
+ def password=(password)
65
+ change_attribute('password', password)
66
+ end
67
+
68
+ def after_event(&block)
69
+ @event_hooks << block
70
+ end
71
+
72
+ def encode_with(coder)
73
+ coder.represent_map(nil, self)
74
+ end
75
+
76
+ def ==(other)
77
+ if other.instance_of?(Site)
78
+ other.name == name && other.username == username
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def trigger(event)
87
+ @event_hooks.each do |block|
88
+ block.call(event)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,7 +1,7 @@
1
1
  module Keyrack
2
2
  module UI
3
3
  class Console
4
- attr_accessor :database, :mode
4
+ attr_accessor :mode
5
5
 
6
6
  def initialize
7
7
  @highline = HighLine.new
@@ -9,64 +9,51 @@ module Keyrack
9
9
  end
10
10
 
11
11
  def get_password
12
- @highline.ask("Keyrack password: ") { |q| q.echo = false }
12
+ @highline.ask("Keyrack password: ") { |q| q.echo = false }.to_s
13
13
  end
14
14
 
15
- def menu(options = {})
15
+ def menu(options)
16
+ current_group = options[:group]
17
+ dirty = options[:dirty]
18
+ at_top = options[:at_top]
19
+
16
20
  choices = {'n' => :new, 'q' => :quit, 'm' => :mode}
17
- index = 1
18
-
19
- sites = @database.sites(options)
20
- count = sites.length
21
- count += @database.groups.length if !options[:group]
22
- width = count / 10
23
-
24
- if !options[:group]
25
- # Can't have subgroups (yet?).
26
- @highline.say("=== #{@highline.color("Keyrack Main Menu", :yellow)} ===")
27
- @database.groups.each do |group|
28
- choices[index.to_s] = {:group => group}
29
- @highline.say(" %#{width}d. %s" % [index, @highline.color(group, :green)])
30
- index += 1
31
- end
32
- else
33
- @highline.say("===== #{@highline.color(options[:group], :green)} =====")
34
- end
21
+ entry_choices = print_entries(current_group,
22
+ at_top ? "Keyrack Main Menu" : current_group.name)
23
+ choices.update(entry_choices)
35
24
 
36
- sites.each do |site|
37
- site_entry = @database.get(site, options)
38
- site_entry = [site_entry] unless site_entry.is_a?(Array)
39
- site_entry.each do |entry|
40
- choices[index.to_s] = entry
41
- @highline.say(" %#{width}d. %s [%s]" % [index, site, entry[:username]])
42
- index += 1
43
- end
25
+ @highline.say("Mode: #{@mode}")
26
+ commands = "Commands: [n]ew"
27
+ if !current_group.sites.empty?
28
+ choices['e'] = :edit
29
+ commands << " [e]dit"
44
30
  end
45
31
 
46
- @highline.say("Mode: #{mode}")
47
- commands = "Commands: [n]ew"
48
- if !sites.empty?
49
- choices['d'] = :delete
50
- commands << " [d]elete"
32
+ choices['g'] = :new_group
33
+ commands << " [g]roup"
34
+
35
+ if options[:enable_up]
36
+ choices['u'] = :up
37
+ commands << " [u]p"
51
38
  end
52
- if !options[:group]
53
- choices['g'] = :new_group
54
- commands << " [g]roup"
55
- else
39
+
40
+ if !at_top
56
41
  choices['t'] = :top
57
42
  commands << " [t]op"
58
43
  end
59
- if @database.dirty?
44
+
45
+ if dirty
60
46
  choices['s'] = :save
61
47
  commands << " [s]ave"
62
48
  end
63
49
  commands << " [m]ode [q]uit"
64
50
  @highline.say(commands)
65
- answer = @highline.ask(" ? ") { |q| q.in = choices.keys }
51
+
52
+ answer = @highline.ask("? ") { |q| q.in = choices.keys }.to_s
66
53
  result = choices[answer]
67
54
  case result
68
55
  when Symbol
69
- if result == :quit && @database.dirty? && !@highline.agree("Really quit? You have unsaved changes! [yn] ")
56
+ if result == :quit && dirty && !@highline.agree("Really quit? You have unsaved changes! [yn] ")
70
57
  nil
71
58
  elsif result == :mode
72
59
  @mode = @mode == :copy ? :print : :copy
@@ -76,13 +63,15 @@ module Keyrack
76
63
  end
77
64
  when Hash
78
65
  if result.has_key?(:group)
79
- result
66
+ {:group => current_group.group(result[:group])}
80
67
  else
81
- if mode == :copy
82
- Copier(result[:password])
68
+ password = result[:site].password
69
+
70
+ if @mode == :copy
71
+ Clipboard.copy(password)
83
72
  @highline.say("The password has been copied to your clipboard.")
84
- elsif mode == :print
85
- password = @highline.color(result[:password], :cyan)
73
+ elsif @mode == :print
74
+ password = @highline.color(password, :cyan)
86
75
  @highline.ask("Here you go: #{password}. Done? ") do |question|
87
76
  question.echo = false
88
77
  if HighLine::SystemExtensions::CHARACTER_MODE != 'stty'
@@ -96,50 +85,31 @@ module Keyrack
96
85
  end
97
86
  end
98
87
 
99
- def get_new_group
100
- group = @highline.ask("Group: ") { |q| q.validate = /^\w[\w\s]*$/ }
101
- {:group => group}
88
+ def get_new_group(options = {})
89
+ @highline.ask("Group: ") { |q| q.validate = /^\w[\w\s]*$/ }.to_s
102
90
  end
103
91
 
104
92
  def get_new_entry
105
93
  result = {}
106
- result[:site] = @highline.ask("Label: ")
107
- result[:username] = @highline.ask("Username: ")
108
- if @highline.agree("Generate password? [yn] ")
109
- loop do
110
- password = Utils.generate_password
111
- if @highline.agree("Generated #{@highline.color(password, :cyan)}. Sound good? [yn] ")
112
- result[:password] = password
113
- break
114
- end
115
- end
116
- else
117
- loop do
118
- password = @highline.ask("Password: ") { |q| q.echo = false }
119
- confirmation = @highline.ask("Password (again): ") { |q| q.echo = false }
120
- if password == confirmation
121
- result[:password] = password
122
- break
123
- end
124
- @highline.say("Passwords didn't match. Try again!")
125
- end
126
- end
127
- result
94
+ result[:site] = @highline.ask("Label: ").to_s
95
+ result[:username] = @highline.ask("Username: ").to_s
96
+ result[:password] = get_new_password
97
+ result[:password].nil? ? nil : result
128
98
  end
129
99
 
130
100
  def display_first_time_notice
131
101
  @highline.say("This looks like your first time using Keyrack. I'll need to ask you a few questions first.")
132
102
  end
133
103
 
134
- def rsa_setup
104
+ def password_setup
135
105
  password = confirmation = nil
136
106
  loop do
137
- password = @highline.ask("New passphrase: ") { |q| q.echo = false }
138
- confirmation = @highline.ask("Confirm passphrase: ") { |q| q.echo = false }
107
+ password = @highline.ask("New passphrase: ") { |q| q.echo = false }.to_s
108
+ confirmation = @highline.ask("Confirm passphrase: ") { |q| q.echo = false }.to_s
139
109
  break if password == confirmation
140
110
  @highline.say("Passphrases didn't match.")
141
111
  end
142
- { 'password' => password, 'path' => 'rsa' }
112
+ password
143
113
  end
144
114
 
145
115
  def store_setup
@@ -153,35 +123,204 @@ module Keyrack
153
123
  when 'filesystem'
154
124
  result['path'] = 'database'
155
125
  when 'ssh'
156
- result['host'] = @highline.ask("Host: ")
157
- result['user'] = @highline.ask("User: ")
158
- result['path'] = @highline.ask("Remote path: ")
126
+ result['host'] = @highline.ask("Host: ").to_s
127
+ result['user'] = @highline.ask("User: ").to_s
128
+ result['path'] = @highline.ask("Remote path: ").to_s
159
129
  end
160
130
 
161
131
  result
162
132
  end
163
133
 
164
- def delete_entry(options = {})
134
+ def choose_entry_to_edit(group)
165
135
  choices = {'c' => :cancel}
166
- index = 1
167
- @highline.say("Choose entry to delete:")
168
- @database.sites(options).each do |site|
169
- site_entry = @database.get(site, options)
170
- site_entry = [site_entry] unless site_entry.is_a?(Array)
171
- site_entry.each do |entry|
172
- choices[index.to_s] = {:site => site, :username => entry[:username]}
173
- @highline.say("% 2d. %s [%s]" % [index, site, entry[:username]])
174
- index += 1
136
+ entry_choices = print_entries(group, "Choose entry")
137
+ choices.update(entry_choices)
138
+
139
+ @highline.say("c. Cancel")
140
+
141
+ answer = @highline.ask("? ") { |q| q.in = choices.keys }.to_s
142
+ result = choices[answer]
143
+ if result == :cancel
144
+ nil
145
+ else
146
+ result
147
+ end
148
+ end
149
+
150
+ def edit_entry(site)
151
+ colored_entry = @highline.color("#{site.name} [#{site.username}]", :cyan)
152
+ @highline.say("Editing entry: #{colored_entry}")
153
+ @highline.say("u. Change username")
154
+ @highline.say("p. Change password")
155
+ @highline.say("d. Delete")
156
+ @highline.say("c. Cancel")
157
+
158
+ case @highline.ask("? ") { |q| q.in = %w{u p d c} }.to_s
159
+ when "u"
160
+ :change_username
161
+ when "p"
162
+ :change_password
163
+ when "d"
164
+ :delete
165
+ when "c"
166
+ nil
167
+ end
168
+ end
169
+
170
+ def change_username(old_username)
171
+ colored_old_username = @highline.color(old_username, :cyan)
172
+ @highline.say("Current username: #{colored_old_username}")
173
+ @highline.ask("New username (blank to cancel): ") { |q| q.validate = /\S/ }.to_s
174
+ end
175
+
176
+ def confirm_overwrite_entry(site)
177
+ entry_name = @highline.color("#{site.name} [#{site.username}]", :cyan)
178
+ @highline.agree("There's already an entry for: #{entry_name}. Do you want to overwrite it? [yn] ")
179
+ end
180
+
181
+ def confirm_delete_entry(site)
182
+ entry_name = @highline.color("#{site.name} [#{site.username}]", :red)
183
+ @highline.agree("You're about to delete #{entry_name}. Are you sure? [yn] ")
184
+ end
185
+
186
+ def display_invalid_password_notice
187
+ @highline.say("Invalid password.")
188
+ end
189
+
190
+ def get_new_password
191
+ result = nil
192
+ case @highline.ask("Generate password? [ync] ") { |q| q.in = %w{y n c} }.to_s
193
+ when "y"
194
+ result = get_generated_password
195
+ if result.nil?
196
+ result = get_manual_password
175
197
  end
198
+ when "n"
199
+ result = get_manual_password
176
200
  end
177
- @highline.say(" c. Cancel")
201
+ result
202
+ end
178
203
 
179
- answer = @highline.ask(" ? ") { |q| q.in = choices.keys }
180
- result = choices[answer]
181
- if result != :cancel
182
- entry = @highline.color("#{result[:site]} [#{result[:username]}]", :red)
183
- if @highline.agree("You're about to delete #{entry}. Are you sure? [yn] ")
184
- @database.delete(result[:site], result[:username], options)
204
+ def get_generated_password
205
+ password = nil
206
+ loop do
207
+ password = Utils.generate_password
208
+ colored_password = @highline.color(password, :cyan)
209
+ case @highline.ask("Generated #{colored_password}. Sound good? [ync] ") { |q| q.in = %w{y n c} }.to_s
210
+ when "y"
211
+ break
212
+ when "c"
213
+ password = nil
214
+ break
215
+ end
216
+ end
217
+ password
218
+ end
219
+
220
+ def get_manual_password
221
+ password = nil
222
+ loop do
223
+ password = @highline.ask("Password: ") { |q| q.echo = false }.to_s
224
+ confirmation = @highline.ask("Password (again): ") { |q| q.echo = false }.to_s
225
+ if password == confirmation
226
+ break
227
+ end
228
+ @highline.say("Passwords didn't match. Try again!")
229
+ end
230
+ password
231
+ end
232
+
233
+ private
234
+
235
+ def print_entries(group, title)
236
+ selections = []
237
+ max_width = 0
238
+ choices = {}
239
+ selection_index = 1
240
+ group.group_names.each do |group_name|
241
+ choices[selection_index.to_s] = {:group => group_name}
242
+
243
+ text = @highline.color(group_name, :green)
244
+ width = group_name.length
245
+ selections.push({:width => width, :text => text})
246
+
247
+ max_width = width if width > max_width
248
+ selection_index += 1
249
+ end
250
+
251
+ group.sites.each do |site|
252
+ choices[selection_index.to_s] = {:site => site}
253
+
254
+ text = "%s [%s]" % [site.name, site.username]
255
+ width = text.length
256
+ selections.push({:width => width, :text => text})
257
+
258
+ max_width = width if width > max_width
259
+ selection_index += 1
260
+ end
261
+
262
+ title = {
263
+ :text => @highline.color(title, :yellow),
264
+ :width => title.length
265
+ }
266
+
267
+ columnize_menu(selections, max_width, title)
268
+ choices
269
+ end
270
+
271
+ def columnize_menu(selections, max_width, title = nil)
272
+ terminal_size = HighLine::SystemExtensions.terminal_size
273
+
274
+ if selections.empty?
275
+ if title
276
+ @highline.say("=== #{title[:text]} ===")
277
+ end
278
+ return
279
+ end
280
+
281
+ # add in width for numbers
282
+ number_width = Math.log10(selections.count).floor + 1
283
+ max_width += number_width + 2
284
+
285
+ multiples = max_width == 0 ? 1 : terminal_size[0] / max_width
286
+ num_columns =
287
+ if multiples > 1
288
+ if (terminal_size[0] - (multiples * max_width)) < (multiples - 1)
289
+ # If there aren't sufficient spaces, decrease column count
290
+ multiples - 1
291
+ else
292
+ multiples
293
+ end
294
+ else
295
+ 1
296
+ end
297
+ #puts "Terminal width: %d; Max width: %d; Multiples: %d; Columns: %d" %
298
+ #[ terminal_size[0], max_width, multiples, num_columns ]
299
+ total_width = num_columns * max_width + (num_columns - 1)
300
+
301
+ if title
302
+ padding_total = total_width - title[:width] - 2
303
+ padding_left = [padding_total / 2, 3].max
304
+ padding_right = [padding_total - padding_left, 3].max
305
+ @highline.say(("=" * padding_left) + " #{title[:text]} " + ("=" * padding_right))
306
+ end
307
+
308
+ selection_index = 0
309
+ catch(:stop) do
310
+ loop do
311
+ num_columns.downto(1) do |i|
312
+ selection = selections[selection_index]
313
+ throw(:stop) if selection.nil?
314
+
315
+ label = "%#{number_width}d. " % (selection_index + 1)
316
+ if i == 1 || selection_index == (selections.count - 1)
317
+ @highline.say(label + selection[:text])
318
+ else
319
+ spaces = max_width - (selection[:width] + number_width + 2) + 1
320
+ @highline.say(label + selection[:text] + (" " * spaces))
321
+ end
322
+ selection_index += 1
323
+ end
185
324
  end
186
325
  end
187
326
  end