trocla 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ require 'trocla/store'
2
+ # store management
3
+ class Trocla::Stores
4
+ class << self
5
+ def [](store)
6
+ stores[store.to_s.downcase]
7
+ end
8
+
9
+ def all
10
+ @all ||= Dir[ path '*' ].collect do |store|
11
+ File.basename(store, '.rb').downcase
12
+ end
13
+ end
14
+
15
+ def available?(store)
16
+ all.include?(store.to_s.downcase)
17
+ end
18
+
19
+ private
20
+ def stores
21
+ @@stores ||= Hash.new do |hash, store|
22
+ store = store.to_s.downcase
23
+ if File.exists?(path(store))
24
+ require "trocla/stores/#{store}"
25
+ class_name = "Trocla::Stores::#{store.capitalize}"
26
+ hash[store] = (eval class_name)
27
+ else
28
+ raise "Store #{store} is not supported!"
29
+ end
30
+ end
31
+ end
32
+
33
+ def path(store)
34
+ File.expand_path(
35
+ File.join(File.dirname(__FILE__), 'stores', "#{store}.rb")
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ # a simple in memory store just as an example
2
+ class Trocla::Stores::Memory < Trocla::Store
3
+ attr_reader :memory
4
+ def initialize(config,trocla)
5
+ super(config,trocla)
6
+ @memory = Hash.new({})
7
+ end
8
+
9
+ def get(key,format)
10
+ unless expired?(key)
11
+ memory[key][format]
12
+ else
13
+ delete_all(key)
14
+ nil
15
+ end
16
+ end
17
+ def set(key,format,value,options={})
18
+ super(key,format,value,options)
19
+ set_expires(key,options['expires'])
20
+ end
21
+
22
+ private
23
+ def set_plain(key,value,options)
24
+ memory[key] = { 'plain' => value }
25
+ end
26
+
27
+ def set_format(key,format,value,options)
28
+ memory[key].merge!({ format => value })
29
+ end
30
+
31
+ def delete_all(key)
32
+ memory.delete(key)
33
+ end
34
+ def delete_format(key,format)
35
+ old_val = (h = memory[key]).delete(format)
36
+ h.empty? ? memory.delete(key) : memory[key] = h
37
+ set_expires(key,nil)
38
+ old_val
39
+ end
40
+ private
41
+ def set_expires(key,expires)
42
+ expires = memory[key]['_expires'] if expires.nil?
43
+ if expires && expires > 0
44
+ memory[key]['_expires'] = expires
45
+ memory[key]['_expires_at'] = Time.now + expires
46
+ else
47
+ memory[key].delete('_expires')
48
+ memory[key].delete('_expires_at')
49
+ end
50
+ end
51
+ def expired?(key)
52
+ memory.key?(key) &&
53
+ (a = memory[key]['_expires_at']).is_a?(Time) && \
54
+ (a < Time.now)
55
+ end
56
+ end
@@ -0,0 +1,54 @@
1
+ # the default moneta based store
2
+ class Trocla::Stores::Moneta < Trocla::Store
3
+ attr_reader :moneta
4
+ def initialize(config,trocla)
5
+ super(config,trocla)
6
+ require 'moneta'
7
+ # load expire support by default
8
+ adapter_options = { :expires => true }.merge(
9
+ store_config['adapter_options']||{})
10
+ @moneta = Moneta.new(store_config['adapter'],adapter_options)
11
+ end
12
+
13
+ def get(key,format)
14
+ moneta.fetch(key, {})[format]
15
+ end
16
+
17
+ private
18
+ def set_plain(key,value,options)
19
+ h = { 'plain' => value }
20
+ mo = moneta_options(key,options)
21
+ if options['expires'] && options['expires'] > 0
22
+ h['_expires'] = options['expires']
23
+ else
24
+ # be sure that we disable the existing
25
+ # expires if nothing is set.
26
+ mo[:expires] = false
27
+ end
28
+ moneta.store(key,h,mo)
29
+ end
30
+
31
+ def set_format(key,format,value,options)
32
+ moneta.store(key,
33
+ moneta.fetch(key,{}).merge({ format => value }),
34
+ moneta_options(key,options))
35
+ end
36
+
37
+ def delete_all(key)
38
+ moneta.delete(key)
39
+ end
40
+ def delete_format(key,format)
41
+ old_val = (h = moneta.fetch(key,{})).delete(format)
42
+ h.empty? ? moneta.delete(key) : moneta.store(key,h,moneta_options(key,{}))
43
+ old_val
44
+ end
45
+ def moneta_options(key,options)
46
+ res = {}
47
+ if options.key?('expires')
48
+ res[:expires] = options['expires']
49
+ elsif e = moneta.fetch(key, {})['_expires']
50
+ res[:expires] = e
51
+ end
52
+ res
53
+ end
54
+ end
data/lib/trocla/util.rb CHANGED
@@ -15,13 +15,18 @@ class Trocla
15
15
  private
16
16
 
17
17
  def charsets
18
- @charsets ||= {
19
- 'default' => chars,
20
- 'alphanumeric' => alphanumeric,
21
- 'shellsafe' => shellsafe,
22
- 'windowssafe' => windowssafe,
23
- 'numeric' => numeric,
24
- }
18
+ @charsets ||= begin
19
+ h = {
20
+ 'default' => chars,
21
+ 'alphanumeric' => alphanumeric,
22
+ 'shellsafe' => shellsafe,
23
+ 'windowssafe' => windowssafe,
24
+ 'numeric' => numeric,
25
+ 'hexadecimal' => hexadecimal,
26
+ 'consolesafe' => consolesafe,
27
+ }
28
+ h.each { |k, v| h[k] = v.uniq }
29
+ end
25
30
  end
26
31
 
27
32
  def chars
@@ -33,8 +38,14 @@ class Trocla
33
38
  def windowssafe
34
39
  @windowssafe ||= alphanumeric + windowssafe_chars
35
40
  end
41
+ def consolesafe
42
+ @consolesafe ||= alphanumeric + consolesafe_chars
43
+ end
44
+ def hexadecimal
45
+ @hexadecimal ||= numeric + ('a'..'f').to_a
46
+ end
36
47
  def alphanumeric
37
- @alphanumeric ||= ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
48
+ @alphanumeric ||= ('a'..'z').to_a + ('A'..'Z').to_a + numeric
38
49
  end
39
50
  def numeric
40
51
  @numeric ||= ('0'..'9').to_a
@@ -48,6 +59,9 @@ class Trocla
48
59
  def windowssafe_chars
49
60
  @windowssafe_chars ||= "+%/@=?_.,".split(//)
50
61
  end
62
+ def consolesafe_chars
63
+ @consolesafe_chars ||= '+.-,_'.split(//)
64
+ end
51
65
  end
52
66
  end
53
67
  end
data/spec/spec_helper.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
3
  require 'rspec'
4
- require 'mocha'
4
+ require 'rspec/pending_for'
5
5
  require 'yaml'
6
6
  require 'trocla'
7
7
 
@@ -9,8 +9,219 @@ require 'trocla'
9
9
  # in ./support/ and its subdirectories.
10
10
  Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
11
11
 
12
- RSpec.configure do |config|
13
-
12
+ RSpec.shared_examples "encryption_basics" do
13
+ describe 'storing' do
14
+ it "random passwords" do
15
+ expect(@trocla.password('random1', 'plain').length).to eql(16)
16
+ end
17
+
18
+ it "long random passwords" do
19
+ expect(@trocla.set_password('random1_long','plain',4096.times.collect{|s| 'x' }.join('')).length).to eql(4096)
20
+ end
21
+ end
22
+
23
+ describe 'retrieve' do
24
+ it "random passwords" do
25
+ stored = @trocla.password('random1', 'plain')
26
+ retrieved = @trocla.password('random1', 'plain')
27
+ retrieved_again = @trocla.password('random1', 'plain')
28
+ expect(retrieved).to eql(stored)
29
+ expect(retrieved_again).to eql(stored)
30
+ expect(retrieved_again).to eql(retrieved)
31
+ end
32
+
33
+ it "encrypted passwords" do
34
+ @trocla.set_password('some_pass', 'plain', 'super secret')
35
+ expect(@trocla.get_password('some_pass', 'plain')).to eql('super secret')
36
+ end
37
+
38
+ end
39
+ describe 'deleting' do
40
+ it "plain" do
41
+ @trocla.set_password('some_pass', 'plain', 'super secret')
42
+ expect(@trocla.delete_password('some_pass', 'plain')).to eql('super secret')
43
+ end
44
+ it "delete formats" do
45
+ plain = @trocla.password('some_mysqlpass', 'plain')
46
+ mysql = @trocla.password('some_mysqlpass', 'mysql')
47
+ expect(@trocla.delete_password('some_mysqlpass', 'mysql')).to eql(mysql)
48
+ expect(@trocla.delete_password('some_mysqlpass', 'plain')).to eql(plain)
49
+ expect(@trocla.get_password('some_mysqlpass','plain')).to be_nil
50
+ expect(@trocla.get_password('some_mysqlpass','mysql')).to be_nil
51
+ end
52
+
53
+ it "all passwords" do
54
+ plain = @trocla.password('some_mysqlpass', 'plain')
55
+ mysql = @trocla.password('some_mysqlpass', 'mysql')
56
+ deleted = @trocla.delete_password('some_mysqlpass')
57
+ expect(deleted).to be_a_kind_of(Hash)
58
+ expect(deleted['plain']).to eql(plain)
59
+ expect(deleted['mysql']).to eql(mysql)
60
+ end
61
+ end
62
+ end
63
+ RSpec.shared_examples "verify_encryption" do
64
+ it "does not store plaintext passwords" do
65
+ @trocla.set_password('noplain', 'plain', 'plaintext_password')
66
+ expect(File.readlines(trocla_yaml_file).grep(/plaintext_password/)).to be_empty
67
+ end
68
+
69
+ it "makes sure identical passwords do not match when stored" do
70
+ @trocla.set_password('one_key', 'plain', 'super secret')
71
+ @trocla.set_password('another_key', 'plain', 'super secret')
72
+ yaml = YAML.load_file(trocla_yaml_file)
73
+ expect(yaml['one_key']['plain']).not_to eq(yaml['another_key']['plain'])
74
+ end
75
+ end
76
+
77
+ RSpec.shared_examples 'store_validation' do |store|
78
+ describe '.get' do
79
+ it { expect(store.get('some_key','plain')).to be_nil }
80
+ end
81
+ describe '.set' do
82
+ it 'stores nil values' do
83
+ store.set('some_nil_value','plain',nil)
84
+ expect(store.get('some_nil_value','plain')).to be_nil
85
+ end
86
+ it 'stores plain format' do
87
+ store.set('some_value','plain','value')
88
+ expect(store.get('some_value','plain')).to eql('value')
89
+ end
90
+ it 'stores other formats' do
91
+ store.set('some_value','foo','bla')
92
+ expect(store.get('some_value','foo')).to eql('bla')
93
+ end
94
+ it 'resets other formats on setting plain' do
95
+ store.set('some_value','foo','bla')
96
+ store.set('some_value','plain','value')
97
+ expect(store.get('some_value','plain')).to eql('value')
98
+ expect(store.get('some_value','foo')).to be_nil
99
+ end
100
+ end
101
+ describe '.delete' do
102
+ it { expect(store.delete('something','foo')).to be_nil }
103
+ it { expect(store.delete('something')).to be_empty }
104
+ it 'deletes the value of a format' do
105
+ store.set('some_value','foo','bla')
106
+ expect(store.delete('some_value','foo')).to eql('bla')
107
+ expect(store.get('some_value','foo')).to be_nil
108
+ end
109
+ it 'deletes only the value of a format' do
110
+ store.set('some_value','plain','value')
111
+ store.set('some_value','foo','bla')
112
+ expect(store.delete('some_value','plain')).to eql('value')
113
+ expect(store.get('some_value','plain')).to be_nil
114
+ expect(store.get('some_value','foo')).to eql('bla')
115
+ end
116
+ it 'deletes all values without a format' do
117
+ store.set('some_value','plain','value')
118
+ store.set('some_value','foo','bla')
119
+ hash = store.delete('some_value')
120
+ expect(hash).to be_a_kind_of(Hash)
121
+ expect(hash['plain']).to eql('value')
122
+ expect(hash['foo']).to eql('bla')
123
+ expect(store.get('some_value','plain')).to be_nil
124
+ expect(store.get('some_value','foo')).to be_nil
125
+ end
126
+ end
127
+ describe 'expiration' do
128
+ it 'will not return an expired key' do
129
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
130
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
131
+ sleep 3
132
+ expect(store.get('some_expiring_value','plain')).to be_nil
133
+ end
134
+ it 'increases expiration when setting anything for that key' do
135
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
136
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
137
+ sleep 1
138
+ store.set('some_expiring_value','bla','bla_to_be_expired',{ 'expires' => 3 })
139
+ sleep 2
140
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
141
+ sleep 2
142
+ expect(store.get('some_expiring_value','plain')).to be_nil
143
+ end
144
+ it 'keeps expiration when setting another value' do
145
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
146
+ store.set('some_expiring_value','foo','to_be_expired_foo')
147
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
148
+ sleep 3
149
+ expect(store.get('some_expiring_value','plain')).to be_nil
150
+ expect(store.get('some_expiring_value','foo')).to be_nil
151
+ end
152
+ it 'setting plain clears everything including expiration' do
153
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 2 })
154
+ sleep 1
155
+ store.set('some_expiring_value','plain','to_be_expired2')
156
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
157
+ sleep 3
158
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
159
+ end
160
+ it 'extends expiration when setting another value' do
161
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 4 })
162
+ sleep 2
163
+ store.set('some_expiring_value','foo','to_be_expired_foo')
164
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
165
+ sleep 3
166
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
167
+ sleep 2
168
+ expect(store.get('some_expiring_value','plain')).to be_nil
169
+ end
170
+ it 'extends expiration when deleting a format' do
171
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 4 })
172
+ store.set('some_expiring_value','foo','to_be_expired2')
173
+ sleep 2
174
+ expect(store.delete('some_expiring_value','foo')).to eql('to_be_expired2')
175
+ sleep 3
176
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
177
+ sleep 2
178
+ expect(store.get('some_expiring_value','plain')).to be_nil
179
+ end
180
+ it 'keeps expiration although we\'re fetching a value' do
181
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 3 })
182
+ sleep 2
183
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
184
+ sleep 2
185
+ expect(store.get('some_expiring_value','plain')).to be_nil
186
+ end
187
+ it 'readding a value with an expiration makes it expiring in the future' do
188
+ store.set('some_expiring_value','plain','to_be_expired')
189
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
190
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
191
+ sleep 3
192
+ expect(store.get('some_expiring_value','plain')).to be_nil
193
+ end
194
+ it 'setting an expires of false removes expiration' do
195
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
196
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
197
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => false })
198
+ sleep 3
199
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
200
+ end
201
+ it 'setting an expires of 0 removes expiration' do
202
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
203
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
204
+ store.set('some_expiring_value','plain','to_be_expired',{ 'expires' => 0 })
205
+ sleep 3
206
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired')
207
+ end
208
+ it 'setting an expires of false removes expiration even if it\'s for a different format' do
209
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
210
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
211
+ store.set('some_expiring_value','foo','to_be_expired_foo',{ 'expires' => false })
212
+ sleep 3
213
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
214
+ expect(store.get('some_expiring_value','foo')).to eql('to_be_expired_foo')
215
+ end
216
+ it 'setting an expires of 0 removes expiration even if it\'s for a different format' do
217
+ store.set('some_expiring_value','plain','to_be_expired2',{ 'expires' => 2 })
218
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
219
+ store.set('some_expiring_value','foo','to_be_expired_foo',{ 'expires' => 0 })
220
+ sleep 3
221
+ expect(store.get('some_expiring_value','plain')).to eql('to_be_expired2')
222
+ expect(store.get('some_expiring_value','foo')).to eql('to_be_expired_foo')
223
+ end
224
+ end
14
225
  end
15
226
 
16
227
  def default_config
@@ -18,26 +229,30 @@ def default_config
18
229
  end
19
230
 
20
231
  def test_config
21
- return @config unless @config.nil?
22
- @config = default_config
23
- @config.delete('adapter_options')
24
- @config['adapter'] = :Memory
25
- @config
232
+ @config ||= default_config.merge({
233
+ 'store' => :memory,
234
+ })
235
+ end
236
+
237
+ def test_config_persistent
238
+ @config ||= default_config.merge({
239
+ 'store_options' => {
240
+ 'adapter' => :YAML,
241
+ 'adapter_options' => {
242
+ :file => trocla_yaml_file
243
+ },
244
+ },
245
+ })
26
246
  end
27
247
 
28
248
  def ssl_test_config
29
- return @ssl_config unless @ssl_config.nil?
30
- @ssl_config = test_config
31
- @ssl_config['encryption'] = :ssl
32
- @ssl_config['ssl_options'] = {
33
- :private_key => data_dir('trocla.key'),
34
- :public_key => data_dir('trocla.pub')
35
- }
36
- @ssl_config['adapter'] = :YAML
37
- @ssl_config['adapter_options'] = {
38
- :file => trocla_yaml_file
39
- }
40
- @ssl_config
249
+ @ssl_config ||= test_config_persistent.merge({
250
+ 'encryption' => :ssl,
251
+ 'encryption_options' => {
252
+ :private_key => data_dir('trocla.key'),
253
+ :public_key => data_dir('trocla.pub'),
254
+ },
255
+ })
41
256
  end
42
257
 
43
258
  def base_dir