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
@@ -0,0 +1,13 @@
|
|
1
|
+
module Keyrack
|
2
|
+
class Event
|
3
|
+
attr_reader :owner, :name, :parent
|
4
|
+
attr_accessor :attribute_name, :previous_value, :new_value,
|
5
|
+
:collection_name, :object
|
6
|
+
|
7
|
+
def initialize(owner, name, parent = nil)
|
8
|
+
@owner = owner
|
9
|
+
@name = name
|
10
|
+
@parent = parent
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
module Keyrack
|
2
|
+
class Group < Hash
|
3
|
+
def initialize(arg = nil)
|
4
|
+
case arg
|
5
|
+
when String
|
6
|
+
self['name'] = arg
|
7
|
+
self['sites'] = []
|
8
|
+
self['groups'] = {}
|
9
|
+
when Hash
|
10
|
+
load(arg)
|
11
|
+
when nil
|
12
|
+
@uninitialized = true
|
13
|
+
end
|
14
|
+
|
15
|
+
@after_event = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def load(hash)
|
19
|
+
@loading = true
|
20
|
+
|
21
|
+
if !hash.has_key?('name')
|
22
|
+
raise ArgumentError, "hash is missing the 'name' key"
|
23
|
+
end
|
24
|
+
if !hash['name'].is_a?(String)
|
25
|
+
raise ArgumentError, "name is not a String"
|
26
|
+
end
|
27
|
+
self['name'] = hash['name']
|
28
|
+
|
29
|
+
if !hash.has_key?('sites')
|
30
|
+
raise ArgumentError, "hash is missing the 'sites' key"
|
31
|
+
end
|
32
|
+
if !hash['sites'].is_a?(Array)
|
33
|
+
raise ArgumentError, "sites is not an Array"
|
34
|
+
end
|
35
|
+
|
36
|
+
if !hash.has_key?('groups')
|
37
|
+
raise ArgumentError, "hash is missing the 'groups' key"
|
38
|
+
end
|
39
|
+
if !hash['groups'].is_a?(Hash)
|
40
|
+
raise ArgumentError, "groups is not a Hash"
|
41
|
+
end
|
42
|
+
|
43
|
+
self['sites'] = []
|
44
|
+
hash['sites'].each_with_index do |site_hash, site_index|
|
45
|
+
if !site_hash.is_a?(Hash)
|
46
|
+
raise ArgumentError, "site #{site_index} is not a Hash"
|
47
|
+
end
|
48
|
+
|
49
|
+
begin
|
50
|
+
site = Site.new(site_hash)
|
51
|
+
add_site_without_callbacks(site)
|
52
|
+
rescue SiteError => e
|
53
|
+
raise ArgumentError, "site #{site_index} is not valid: #{e.message}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
self['groups'] = {}
|
58
|
+
hash['groups'].each_pair do |group_name, group_hash|
|
59
|
+
if !group_name.is_a?(String)
|
60
|
+
raise ArgumentError, "group key is not a String"
|
61
|
+
end
|
62
|
+
if !group_hash.is_a?(Hash)
|
63
|
+
raise ArgumentError, "group value for #{group_name.inspect} is not a Hash"
|
64
|
+
end
|
65
|
+
|
66
|
+
begin
|
67
|
+
group = Group.new(group_hash)
|
68
|
+
add_group_without_callbacks(group)
|
69
|
+
rescue ArgumentError => e
|
70
|
+
raise ArgumentError, "group #{group_name.inspect} is not valid: #{e.message}"
|
71
|
+
end
|
72
|
+
|
73
|
+
if group.name != group_name
|
74
|
+
raise ArgumentError, "group name mismatch: #{group_name.inspect} != #{group.name.inspect}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
@loading = false
|
79
|
+
@uninitialized = false
|
80
|
+
end
|
81
|
+
|
82
|
+
def change_attribute(name, value)
|
83
|
+
event = Event.new(self, 'change')
|
84
|
+
event.attribute_name = name
|
85
|
+
event.previous_value = self[name]
|
86
|
+
event.new_value = value
|
87
|
+
|
88
|
+
self[name] = value
|
89
|
+
trigger(event)
|
90
|
+
end
|
91
|
+
|
92
|
+
def name
|
93
|
+
self['name']
|
94
|
+
end
|
95
|
+
|
96
|
+
def name=(name)
|
97
|
+
change_attribute('name', name)
|
98
|
+
end
|
99
|
+
|
100
|
+
def sites
|
101
|
+
self['sites']
|
102
|
+
end
|
103
|
+
|
104
|
+
def groups
|
105
|
+
self['groups']
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_site(site)
|
109
|
+
raise "add_site is not allowed until Group is initialized" if @uninitialized && !@loading
|
110
|
+
add_site_without_callbacks(site)
|
111
|
+
|
112
|
+
event = Event.new(self, 'add')
|
113
|
+
event.collection_name = 'sites'
|
114
|
+
event.object = site
|
115
|
+
trigger(event)
|
116
|
+
end
|
117
|
+
|
118
|
+
def site(index)
|
119
|
+
sites[index]
|
120
|
+
end
|
121
|
+
|
122
|
+
def remove_site(site)
|
123
|
+
raise "remove_site is not allowed until Group is initialized" if @uninitialized && !@loading
|
124
|
+
index = sites.index(site)
|
125
|
+
if index.nil?
|
126
|
+
raise GroupError, "site doesn't exist"
|
127
|
+
end
|
128
|
+
site = sites.delete_at(index)
|
129
|
+
|
130
|
+
event = Event.new(self, 'remove')
|
131
|
+
event.collection_name = 'sites'
|
132
|
+
event.object = site
|
133
|
+
trigger(event)
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_group(group)
|
137
|
+
raise "add_group is not allowed until Group is initialized" if @uninitialized && !@loading
|
138
|
+
add_group_without_callbacks(group)
|
139
|
+
|
140
|
+
event = Event.new(self, 'add')
|
141
|
+
event.collection_name = 'groups'
|
142
|
+
event.object = group
|
143
|
+
trigger(event)
|
144
|
+
end
|
145
|
+
|
146
|
+
def remove_group(group_name)
|
147
|
+
raise "remove_group is not allowed until Group is initialized" if @uninitialized && !@loading
|
148
|
+
if !groups.has_key?(group_name)
|
149
|
+
raise GroupError, "group doesn't exist"
|
150
|
+
end
|
151
|
+
group = groups.delete(group_name)
|
152
|
+
|
153
|
+
event = Event.new(self, 'remove')
|
154
|
+
event.collection_name = 'groups'
|
155
|
+
event.object = group
|
156
|
+
trigger(event)
|
157
|
+
end
|
158
|
+
|
159
|
+
def group(group_name)
|
160
|
+
groups[group_name]
|
161
|
+
end
|
162
|
+
|
163
|
+
def group_names
|
164
|
+
groups.keys
|
165
|
+
end
|
166
|
+
|
167
|
+
def after_event(&block)
|
168
|
+
@after_event << block
|
169
|
+
end
|
170
|
+
|
171
|
+
def encode_with(coder)
|
172
|
+
coder.represent_map(nil, self)
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
def add_site_without_callbacks(site)
|
178
|
+
if !site.is_a?(Site)
|
179
|
+
raise GroupError, "site is not a Site"
|
180
|
+
end
|
181
|
+
if sites.include?(site)
|
182
|
+
raise GroupError,
|
183
|
+
"site (#{site.name.inspect}, #{site.username.inspect}) already exists"
|
184
|
+
end
|
185
|
+
sites << site
|
186
|
+
add_site_hooks_for(site)
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_group_without_callbacks(group)
|
190
|
+
if !group.is_a?(Group)
|
191
|
+
raise GroupError, "group is not a Group"
|
192
|
+
end
|
193
|
+
if groups.has_key?(group.name)
|
194
|
+
raise GroupError, "group already exists"
|
195
|
+
end
|
196
|
+
groups[group.name] = group
|
197
|
+
add_group_hooks_for(group)
|
198
|
+
end
|
199
|
+
|
200
|
+
def trigger(event)
|
201
|
+
@after_event.each do |block|
|
202
|
+
block.call(event)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def add_site_hooks_for(site)
|
207
|
+
site.after_event do |site_event|
|
208
|
+
trigger(Event.new(self, 'change', site_event))
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def add_group_hooks_for(group)
|
213
|
+
group.after_event do |group_event|
|
214
|
+
if group_event.name == 'change' && group_event.attribute_name == 'name'
|
215
|
+
key, value = self.groups.find { |(k, v)| v.equal?(group) }
|
216
|
+
if key
|
217
|
+
self['groups'][group.name] = self['groups'].delete(key)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
trigger(Event.new(self, 'change', group_event))
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Keyrack
|
2
|
+
# Migrate databases from one version to the next.
|
3
|
+
class Migrator
|
4
|
+
def self.run(database)
|
5
|
+
new(database).run
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(database)
|
9
|
+
@database = database
|
10
|
+
@version = database['version']
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
case @version
|
15
|
+
when 3
|
16
|
+
migrate_3_to_4(@database.clone)
|
17
|
+
when 4
|
18
|
+
@database
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def migrate_3_to_4(database)
|
25
|
+
groups = [database['groups']['top']]
|
26
|
+
until groups.empty?
|
27
|
+
group = groups.pop
|
28
|
+
group['sites'] =
|
29
|
+
group['sites'].inject([]) do |arr, (site_name, site_hash)|
|
30
|
+
site_hash['logins'].each_pair do |username, password|
|
31
|
+
arr.push({
|
32
|
+
'name' => site_name,
|
33
|
+
'username' => username,
|
34
|
+
'password' => password
|
35
|
+
})
|
36
|
+
end
|
37
|
+
arr
|
38
|
+
end
|
39
|
+
groups.push(*group['groups'].values)
|
40
|
+
end
|
41
|
+
database['version'] = 4
|
42
|
+
database
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/keyrack/runner.rb
CHANGED
@@ -1,81 +1,135 @@
|
|
1
1
|
module Keyrack
|
2
2
|
class Runner
|
3
3
|
def initialize(argv)
|
4
|
-
|
5
|
-
OptionParser.new do |
|
6
|
-
|
7
|
-
|
4
|
+
opts = { :path => "~/.keyrack" }
|
5
|
+
OptionParser.new do |optparse|
|
6
|
+
optparse.on("-d", "--directory [PATH]", "Specify configuration path (Default: #{@config_path}") do |path|
|
7
|
+
opts[:path] = path
|
8
8
|
end
|
9
9
|
end.parse(argv)
|
10
|
-
@config_path = File.expand_path(
|
10
|
+
@config_path = File.expand_path(opts[:path])
|
11
11
|
@ui = UI::Console.new
|
12
12
|
|
13
|
+
database_exists = false
|
13
14
|
if Dir.exist?(@config_path)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
config_filename = File.join(@config_path, "config.yml")
|
16
|
+
if File.exist?(config_filename)
|
17
|
+
database_exists = true
|
18
|
+
@options = YAML.load_file(config_filename)
|
19
|
+
password = @ui.get_password
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if !database_exists
|
24
|
+
FileUtils.mkdir_p(@config_path)
|
20
25
|
@options = {}
|
21
26
|
@ui.display_first_time_notice
|
22
27
|
|
23
|
-
#
|
24
|
-
|
25
|
-
rsa_key, rsa_pem = Utils.generate_rsa_key(rsa_options['password'])
|
26
|
-
rsa_path = File.expand_path(rsa_options['path'], @config_path)
|
27
|
-
File.open(rsa_path, 'w') { |f| f.write(rsa_pem) }
|
28
|
-
@options['rsa'] = rsa_path
|
29
|
-
|
30
|
-
# AES
|
31
|
-
aes_data = {
|
32
|
-
'key' => Utils.generate_aes_key,
|
33
|
-
'iv' => Utils.generate_aes_key
|
34
|
-
}
|
35
|
-
dump = Marshal.dump(aes_data)
|
36
|
-
aes_path = File.expand_path('aes', @config_path)
|
37
|
-
File.open(aes_path, 'w') { |f| f.write(rsa_key.public_encrypt(dump)) }
|
38
|
-
@options['aes'] = aes_path
|
28
|
+
# Password
|
29
|
+
password = @ui.password_setup
|
39
30
|
|
40
31
|
# Store
|
41
|
-
|
42
|
-
if store_options['type'] == 'filesystem'
|
43
|
-
store_options['path'] = File.expand_path(store_options['path'], @config_path)
|
44
|
-
end
|
45
|
-
@options['store'] = store_options
|
32
|
+
@options['store'] = @ui.store_setup
|
46
33
|
|
47
34
|
# Write out config
|
48
|
-
File.open(File.expand_path('config', @config_path), 'w') { |f| f.print(@options.to_yaml) }
|
35
|
+
File.open(File.expand_path('config.yml', @config_path), 'w') { |f| f.print(@options.to_yaml) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Expand relative paths, using config_path as parent
|
39
|
+
if @options['store']['type'] == 'filesystem' &&
|
40
|
+
@options['store'].has_key?('path')
|
41
|
+
|
42
|
+
@options['store']['path'] =
|
43
|
+
File.expand_path(@options['store']['path'], @config_path)
|
49
44
|
end
|
45
|
+
|
50
46
|
store = Store[@options['store']['type']].new(@options['store'].reject { |k, _| k == 'type' })
|
51
|
-
@database = Database.new(
|
52
|
-
@ui.database = @database
|
47
|
+
@database = Database.new(password, store)
|
53
48
|
main_loop
|
54
49
|
end
|
55
50
|
|
56
51
|
def main_loop
|
57
|
-
|
52
|
+
group_tree = [@database.top_group]
|
58
53
|
loop do
|
59
|
-
|
54
|
+
current_group = group_tree.last
|
55
|
+
menu_options = {
|
56
|
+
:group => current_group,
|
57
|
+
:at_top => at_top?(current_group),
|
58
|
+
:dirty => @database.dirty?,
|
59
|
+
:enable_up => group_tree.length > 2
|
60
|
+
}
|
61
|
+
choice = @ui.menu(menu_options)
|
60
62
|
|
61
63
|
case choice
|
62
64
|
when :new
|
63
65
|
result = @ui.get_new_entry
|
64
|
-
|
65
|
-
|
66
|
-
|
66
|
+
next if result.nil?
|
67
|
+
|
68
|
+
new_site = Site.new(*result.values_at(:site, :username, :password))
|
69
|
+
if site = current_group.sites.find { |s| s == new_site }
|
70
|
+
if @ui.confirm_overwrite_entry(site)
|
71
|
+
site.password = new_site.password
|
72
|
+
end
|
73
|
+
else
|
74
|
+
current_group.add_site(new_site)
|
75
|
+
end
|
76
|
+
when :edit
|
77
|
+
result = @ui.choose_entry_to_edit(current_group)
|
78
|
+
next if result.nil?
|
79
|
+
site = result[:site]
|
80
|
+
|
81
|
+
loop do
|
82
|
+
which = @ui.edit_entry(site)
|
83
|
+
case which
|
84
|
+
when :change_username
|
85
|
+
new_username = @ui.change_username(site.username)
|
86
|
+
if new_username
|
87
|
+
site.username = new_username
|
88
|
+
end
|
89
|
+
when :change_password
|
90
|
+
new_password = @ui.get_new_password
|
91
|
+
if new_password
|
92
|
+
site.password = new_password
|
93
|
+
end
|
94
|
+
when :delete
|
95
|
+
if @ui.confirm_delete_entry(site)
|
96
|
+
current_group.remove_site(site)
|
97
|
+
break
|
98
|
+
end
|
99
|
+
when nil
|
100
|
+
break
|
101
|
+
end
|
102
|
+
end
|
67
103
|
when :new_group
|
68
|
-
|
104
|
+
group_name = @ui.get_new_group
|
105
|
+
group = Group.new(group_name)
|
106
|
+
current_group.add_group(group)
|
107
|
+
group_tree << group
|
69
108
|
when :save
|
70
|
-
@
|
109
|
+
password = @ui.get_password
|
110
|
+
if !@database.save(password)
|
111
|
+
@ui.display_invalid_password_notice
|
112
|
+
end
|
71
113
|
when :quit
|
72
114
|
break
|
73
115
|
when Hash
|
74
|
-
|
116
|
+
if choice[:group]
|
117
|
+
group_tree << choice[:group]
|
118
|
+
end
|
75
119
|
when :top
|
76
|
-
|
120
|
+
while group_tree.length > 1
|
121
|
+
group_tree.pop
|
122
|
+
end
|
123
|
+
when :up
|
124
|
+
group_tree.pop
|
77
125
|
end
|
78
126
|
end
|
79
127
|
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def at_top?(group)
|
132
|
+
group.equal?(@database.top_group)
|
133
|
+
end
|
80
134
|
end
|
81
135
|
end
|