keyrack 0.3.0.pre → 0.3.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.
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