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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +8 -15
- data/Guardfile +6 -0
- data/LICENSE.txt +4 -2
- data/Rakefile +2 -56
- data/keyrack.gemspec +22 -104
- data/lib/keyrack.rb +9 -1
- data/lib/keyrack/database.rb +64 -98
- data/lib/keyrack/event.rb +13 -0
- data/lib/keyrack/exceptions.rb +4 -0
- data/lib/keyrack/group.rb +225 -0
- data/lib/keyrack/migrator.rb +45 -0
- data/lib/keyrack/runner.rb +98 -44
- data/lib/keyrack/site.rb +92 -0
- data/lib/keyrack/ui/console.rb +234 -95
- data/lib/keyrack/utils.rb +0 -18
- data/lib/keyrack/version.rb +3 -0
- data/test/fixtures/database-3.dat +0 -0
- data/test/helper.rb +11 -1
- data/test/integration/test_interactive_console.rb +139 -0
- data/test/unit/store/test_filesystem.rb +21 -0
- data/test/unit/store/test_ssh.rb +32 -0
- data/test/unit/test_database.rb +201 -0
- data/test/unit/test_event.rb +41 -0
- data/test/unit/test_group.rb +703 -0
- data/test/unit/test_migrator.rb +105 -0
- data/test/unit/test_runner.rb +407 -0
- data/test/unit/test_site.rb +181 -0
- data/test/unit/test_store.rb +13 -0
- data/test/unit/test_utils.rb +8 -0
- data/test/unit/ui/test_console.rb +495 -0
- metadata +98 -94
- data/Gemfile.lock +0 -45
- data/TODO +0 -4
- data/VERSION +0 -1
- data/features/console.feature +0 -103
- data/features/step_definitions/keyrack_steps.rb +0 -61
- data/features/support/env.rb +0 -16
- data/test/fixtures/aes +0 -0
- data/test/fixtures/config.yml +0 -4
- data/test/fixtures/id_rsa +0 -54
- data/test/keyrack/store/test_filesystem.rb +0 -25
- data/test/keyrack/store/test_ssh.rb +0 -36
- data/test/keyrack/test_database.rb +0 -111
- data/test/keyrack/test_runner.rb +0 -111
- data/test/keyrack/test_store.rb +0 -15
- data/test/keyrack/test_utils.rb +0 -41
- data/test/keyrack/ui/test_console.rb +0 -308
data/lib/keyrack/site.rb
ADDED
@@ -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
|
data/lib/keyrack/ui/console.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Keyrack
|
2
2
|
module UI
|
3
3
|
class Console
|
4
|
-
attr_accessor :
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
47
|
-
commands
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
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
|
-
|
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 &&
|
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
|
-
|
82
|
-
|
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(
|
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
|
-
|
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
|
-
|
109
|
-
|
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
|
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
|
-
|
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
|
134
|
+
def choose_entry_to_edit(group)
|
165
135
|
choices = {'c' => :cancel}
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
201
|
+
result
|
202
|
+
end
|
178
203
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|