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,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,4 @@
1
+ module Keyrack
2
+ class SiteError < Exception; end
3
+ class GroupError < Exception; end
4
+ 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
@@ -1,81 +1,135 @@
1
1
  module Keyrack
2
2
  class Runner
3
3
  def initialize(argv)
4
- @config_path = "~/.keyrack"
5
- OptionParser.new do |opts|
6
- opts.on("-d", "--directory [PATH]", "Specify configuration path (Default: #{@config_path}") do |f|
7
- @config_path = f
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(@config_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
- @options = YAML.load_file(File.join(@config_path, "config"))
15
- password = @ui.get_password
16
- rsa_key = Utils.open_rsa_key(File.expand_path(@options['rsa'], @config_path), password)
17
- aes_data = Utils.open_aes_data(File.expand_path(@options['aes'], @config_path), rsa_key)
18
- else
19
- Dir.mkdir(@config_path)
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
- # RSA
24
- rsa_options = @ui.rsa_setup
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
- store_options = @ui.store_setup
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(aes_data['key'], aes_data['iv'], store)
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
- options = {}
52
+ group_tree = [@database.top_group]
58
53
  loop do
59
- choice = @ui.menu(options)
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
- @database.add(result[:site], result[:username], result[:password], options)
65
- when :delete
66
- @ui.delete_entry(options)
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
- options = @ui.get_new_group
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
- @database.save
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
- options = choice
116
+ if choice[:group]
117
+ group_tree << choice[:group]
118
+ end
75
119
  when :top
76
- options = {}
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