mournful_settings 0.0.1

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.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Rob Nichols
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,74 @@
1
+ = Mournful Settings
2
+
3
+ Adds a settings class to a rails app. The settings are mournful because
4
+ they can be stored encrypted. Aren't puns wonderful.
5
+
6
+ == Installation
7
+
8
+ gem mournful_settings
9
+
10
+ Setting are stored in a database table 'mournful_settings_settings'. To add
11
+ mournful_settings migrations to the host app run this rake task:
12
+
13
+ rake mournful_settings:install:migrations
14
+
15
+ Then run 'rake db:migrate' to create the 'mournful_settings_settings' table
16
+
17
+ == Usage
18
+
19
+ In the host rails app, create a class you wish to use as the object to hold
20
+ settings, and have it inherit from MournfulSettings::Setting. For example
21
+ (/app/models/settings.rb)
22
+
23
+ class Setting < MournfulSettings::Setting
24
+ end
25
+
26
+ === Fields
27
+ Each setting has five fields:
28
+
29
+ [name] Identifies the setting. Used in 'for' (see below)
30
+
31
+ [value] The value being stored.
32
+
33
+ [value_type] Values are stored as strings. value_type defines how that string
34
+ should be presented. For example, '1.23' with value_type 'number'
35
+ will be presented as numeric 1.23. If the value_type was 'text'
36
+ the value returned would be '1.23'.
37
+
38
+ [description] Information about the setting being stored
39
+
40
+ [encrypted] Boolean: If set to true, the value will be stored in an encrypted
41
+ format. Otherwise the value will be stored as plain text.
42
+
43
+ === Retrieving a setting
44
+
45
+ To use a stored setting, use the 'for' class method:
46
+
47
+ Setting.create(:name => 'pi', :value => '3.14159', :value_type => 'number')
48
+
49
+ Setting.for(:pi) --> 3.14159
50
+
51
+ == Encryption
52
+
53
+ By default mournful settings uses a blowfish cipher to encrypt settings, and
54
+ its own key string.
55
+
56
+ === Set key
57
+
58
+ If you wish to use your own encryption key, you can define the key in
59
+ an initializer, like this:
60
+
61
+ Setting::Cipher.key = 'your key'
62
+
63
+ === Change cipher
64
+
65
+ Mournful settings uses Ruby's OpenSSL::Cipher. If you wish to use to change
66
+ the cipher from blowfish, you can alter it like this:
67
+
68
+ Setting::Cipher.config = 'aes-128-cbc'
69
+
70
+ To see a list of the available options use:
71
+
72
+ puts OpenSSL::Cipher.ciphers
73
+
74
+ See: http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/Cipher.html
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rdoc/task'
5
+ require 'rake/testtask'
6
+ require 'logger'
7
+
8
+ Rake::RDocTask.new do |rdoc|
9
+ files =['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
10
+ rdoc.rdoc_files.add(files)
11
+ rdoc.main = "README.rdoc" # page to start on
12
+ rdoc.title = "Dibber Docs"
13
+ rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
14
+ rdoc.options << '--line-numbers'
15
+ end
16
+
17
+ Rake::TestTask.new do |t|
18
+ t.test_files = FileList['test/**/*.rb']
19
+ end
20
+
21
+ namespace :mournful_settings do
22
+
23
+ namespace :db do
24
+ task :environment do
25
+ require 'active_record'
26
+ environment = ENV['RAILS_ENV'] || 'development'
27
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => "test/dummy/db/#{environment}.sqlite3.db"
28
+ end
29
+
30
+ desc "Migrate the database"
31
+ task(:migrate => :environment) do
32
+ ActiveRecord::Migrator.migrate("db/migrate", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
33
+ end
34
+
35
+ desc "Roll back the database"
36
+ task(:rollback => :environment) do
37
+ ActiveRecord::Migrator.rollback("db/migrate")
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,11 @@
1
+ require 'mournful_settings'
2
+ require 'rails'
3
+ module MournfulSettings
4
+ class Railtie < Rails::Railtie
5
+ # makes mournful_settings rake tasks available to host app
6
+ rake_tasks do
7
+ Dir[File.join(File.dirname(__FILE__),'../tasks/*.rake')].each { |f| load f }
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ module MournfulSettings
2
+ class Setting < ActiveRecord::Base
3
+
4
+ # Based on http://philtoland.com/post/807114394/simple-blowfish-encryption-with-ruby
5
+ module Cipher
6
+ def self.cipher(mode, data)
7
+ cipher = OpenSSL::Cipher::Cipher.new(config).send(mode)
8
+ cipher.key = Digest::SHA256.digest(key)
9
+ cipher.update(data) << cipher.final
10
+ end
11
+
12
+ def self.encrypt(data)
13
+ cipher(:encrypt, data)
14
+ end
15
+
16
+ def self.decrypt(text)
17
+ cipher(:decrypt, text)
18
+ end
19
+
20
+ def self.key=(text)
21
+ @key = text
22
+ end
23
+
24
+ def self.key
25
+ @key ||= 'Set your own with Setting::Cipher.key = your_key'
26
+ end
27
+
28
+ def self.config=(text)
29
+ raise "'#{text}' is not a value cipher" unless OpenSSL::Cipher::Cipher.ciphers.include?(text)
30
+ @config = text
31
+ end
32
+
33
+ def self.config
34
+ @config ||= blowfish_cipher
35
+ end
36
+
37
+ def self.blowfish_cipher
38
+ 'bf-cbc'
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,106 @@
1
+ require 'base64'
2
+ require_relative 'setting/cipher'
3
+ module MournfulSettings
4
+ class Setting < ActiveRecord::Base
5
+
6
+ self.table_name = 'mournful_settings_settings'
7
+
8
+ VALUE_TYPES = ['text', 'number', 'decimal']
9
+
10
+ before_save :encrypt_value
11
+
12
+ validates :value_type, :presence => true, :inclusion => {:in => VALUE_TYPES}
13
+ validates :value, :presence => true
14
+ validates :name, :uniqueness => true, :presence => true
15
+
16
+ def self.value_types
17
+ VALUE_TYPES
18
+ end
19
+
20
+ def self.for(name)
21
+ setting = find_by_name(name)
22
+ setting.value if setting
23
+ end
24
+
25
+ def value
26
+ if value_type.present?
27
+ parent_value = encrypted? ? decrypt(super) : super
28
+
29
+ case value_type.to_s
30
+ when 'number'
31
+ parent_value.to_f
32
+ when 'decimal'
33
+ BigDecimal.new(parent_value.to_s)
34
+ else
35
+ parent_value
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+ def encrypt(text)
42
+ add_separators Base64.encode64 Cipher.encrypt text.to_s
43
+ end
44
+
45
+ def decrypt(text)
46
+ if is_encrypted?(text)
47
+ Cipher.decrypt Base64.decode64 remove_separators text
48
+ else
49
+ text
50
+ end
51
+ end
52
+
53
+ def is_encrypted?(text)
54
+ inside_separators_and_is_base64_encoded?(text)
55
+ end
56
+
57
+ def inside_separators_and_is_base64_encoded?(text)
58
+ return unless text.kind_of? String
59
+ bytes = text.bytes.to_a
60
+ return unless bytes[0] == separator_byte
61
+ return unless bytes[-1] == separator_byte
62
+ return unless bytes[-2] == last_byte_of_base_64_encoded_text
63
+ non_white_space_with_equal_sign_packing =~ text[1..-3]
64
+ end
65
+
66
+ def non_white_space_with_equal_sign_packing
67
+ /\S+=*/
68
+ end
69
+
70
+
71
+ def encrypt_value
72
+ if encrypted?
73
+ self.value = encrypt(self.value)
74
+ else
75
+ self.value = decrypt(self.value)
76
+ end
77
+ end
78
+
79
+ def add_separators(text)
80
+ [separator, text, separator].join
81
+ end
82
+
83
+ def remove_separators(text)
84
+ text.gsub(separator, "")
85
+ end
86
+
87
+ # Used to delimit encrypted values to make identification more reliable
88
+ def separator
89
+ separator_byte.chr
90
+ end
91
+
92
+
93
+ def separator_byte
94
+ 31 # ASCII unit separator
95
+ end
96
+
97
+ def last_byte_of_base_64_encoded_text
98
+ line_feed_byte
99
+ end
100
+
101
+ def line_feed_byte
102
+ 10
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,3 @@
1
+ module MournfulSettings
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_record'
2
+ require_relative 'mournful_settings/setting'
3
+ require_relative "mournful_settings/railtie" if defined?(Rails) # needed for rake tasks to be loaded into host app
4
+
5
+ module MournfulSettings
6
+
7
+ end
@@ -0,0 +1,28 @@
1
+ namespace :mournful_settings do
2
+
3
+ desc 'Outputs a mournful test message'
4
+ task(:task_test => :environment) do
5
+ puts "Able to access mournful tasks located at #{File.dirname(__FILE__)}"
6
+ end
7
+
8
+ namespace :install do
9
+ # TODO - register within 'rake railties:install:migrations'
10
+ desc 'Copies mournful_settings migrations to host rails app'
11
+ task(:migrations => :environment) do
12
+ mournful_migrate_path = File.expand_path("../../db/migrate", File.dirname(__FILE__))
13
+ rails_migrate_path = File.expand_path("db/migrate", Rails.root)
14
+ scope = :mournful_settings
15
+ migration = ActiveRecord::Migration.new
16
+ output = migration.copy rails_migrate_path, {scope => mournful_migrate_path}
17
+ if output.empty?
18
+ puts "No migrations copied to #{rails_migrate_path}"
19
+ else
20
+ puts "Migrations created at #{rails_migrate_path}:"
21
+ files = output.collect{|m| m.filename.sub rails_migrate_path, ""}
22
+ files.each{|m| puts "\t#{m}"}
23
+ end
24
+ end
25
+ end
26
+
27
+
28
+ end
Binary file
@@ -0,0 +1,5 @@
1
+ require_relative '../../../lib/mournful_settings'
2
+
3
+ class Setting < MournfulSettings::Setting
4
+
5
+ end
@@ -0,0 +1,118 @@
1
+ require_relative '../../test_helper'
2
+
3
+ class SettingTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @value = 'A secret'
7
+ Setting::Cipher.config = 'aes-128-cbc'
8
+ Setting::Cipher.key = 'something else'
9
+ end
10
+
11
+ def teardown
12
+ Setting.delete_all
13
+ end
14
+
15
+ def test_inheritence
16
+ assert_kind_of(MournfulSettings::Setting, text_setting)
17
+ end
18
+
19
+ def test_number_value
20
+ assert_kind_of(Float, number_setting.value)
21
+ end
22
+
23
+ def test_text_value
24
+ assert_kind_of(String, text_setting.value)
25
+ end
26
+
27
+ def test_decimal_value
28
+ assert_kind_of(BigDecimal, decimal_setting.value)
29
+ end
30
+
31
+ def test_encrypted_value
32
+ assert_kind_of(String, encrypted_setting.value)
33
+ assert_equal(@value, encrypted_setting.value)
34
+ end
35
+
36
+ def test_encrypted_value_is_encrypted_in_database
37
+ database_value = database_value_for(encrypted_setting)
38
+ assert_not_equal(database_value, encrypted_setting.value)
39
+ end
40
+
41
+ def test_encrypted_with_different_value_types
42
+ {
43
+ 'text' => 'this is a load of text',
44
+ 'number' => 1.33333333,
45
+ 'decimal' => 1.44
46
+ }.each do |value_type, value|
47
+ setting = Setting.create(:name => value_type, :value => value, :value_type => value_type, :encrypted => true)
48
+ assert_equal(value, setting.value)
49
+ assert_not_equal(database_value_for(setting), setting.value)
50
+ assert_not_equal(database_value_for(setting).to_s, setting.value.to_s)
51
+ end
52
+ end
53
+
54
+ def test_encrypting_an_existing_setting
55
+ value = number_setting.value
56
+ number_setting.encrypted = true
57
+ assert number_setting.save, "Should be able to save a setting after changing it to encrypted"
58
+ assert_equal(value, number_setting.value)
59
+ end
60
+
61
+ def test_unencrypting_an_encrypted_setting
62
+ encrypted_setting.encrypted = false
63
+ assert encrypted_setting.save, "Should be able to save a setting after changing it to unencrypted"
64
+ assert_equal(@value, encrypted_setting.value)
65
+ end
66
+
67
+ def test_valid_types
68
+ Setting::VALUE_TYPES.each do |valid_type|
69
+ number_setting.value_type = valid_type
70
+ assert(number_setting.valid?, "number_setting should be valid when value_type = #{valid_type}")
71
+ end
72
+ end
73
+
74
+ def test_invalid_type
75
+ number_setting.value_type = 'invalid'
76
+ assert(number_setting.invalid?, "number_setting should be invalid")
77
+ end
78
+
79
+ def test_for
80
+ [number_setting, text_setting, decimal_setting, encrypted_setting].each do |setting|
81
+ assert_equal(setting.value, Setting.for(setting.name.to_sym))
82
+ end
83
+ end
84
+
85
+ def test_for_when_no_matching_setting
86
+ assert_nil(Setting.for(:nothing), "Should return nil when setting doesn't exist")
87
+ end
88
+
89
+ def test_setting_an_invalid_cipher_config
90
+ assert_raises RuntimeError do
91
+ Setting::Cipher.config = 'invalid'
92
+ end
93
+ end
94
+
95
+ private
96
+ def text_setting
97
+ @text_setting ||= Setting.create(:name => 'text_setting', :value => 'foo', :value_type => 'text')
98
+ end
99
+
100
+ def number_setting
101
+ @number_setting ||= Setting.create(:name => 'number_setting', :value => '1.33333333333333', :value_type => 'number')
102
+ end
103
+
104
+ def decimal_setting
105
+ @decimal_setting ||= Setting.create(:name => 'decimal_setting', :value => '4.55', :value_type => 'decimal')
106
+ end
107
+
108
+ def encrypted_setting
109
+ @encrypted_setting ||= Setting.create(:name => 'encrypted_setting', :value => @value, :value_type => 'text', :encrypted => true)
110
+ end
111
+
112
+ def database_value_for(setting)
113
+ sql = "SELECT value FROM mournful_settings_settings WHERE id = #{setting.id}"
114
+ Setting.connection.select_value(sql)
115
+ end
116
+
117
+
118
+ end
@@ -0,0 +1,6 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'test/unit'
4
+
5
+ require 'active_record'
6
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => "test/dummy/db/test.sqlite3.db"
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mournful_settings
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rob Nichols
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sqlite3
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Packages up code needed to pull data from YAML files when seeding, and
47
+ adds a process log.
48
+ email:
49
+ - rob@undervale.co.uk
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/mournful_settings/version.rb
55
+ - lib/mournful_settings/setting/cipher.rb
56
+ - lib/mournful_settings/setting.rb
57
+ - lib/mournful_settings/railtie.rb
58
+ - lib/mournful_settings.rb
59
+ - lib/tasks/mournful_settings.rake
60
+ - MIT-LICENSE
61
+ - Rakefile
62
+ - README.rdoc
63
+ - test/dummy/lib/setting.rb
64
+ - test/dummy/db/development.sqlite3.db
65
+ - test/dummy/db/test.sqlite3.db
66
+ - test/dummy/test/setting_test.rb
67
+ - test/test_helper.rb
68
+ homepage: https://github.com/reggieb/mournful_settings
69
+ licenses: []
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 1.8.24
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: Tool for adding encrypted settings to an app.
92
+ test_files:
93
+ - test/dummy/lib/setting.rb
94
+ - test/dummy/db/development.sqlite3.db
95
+ - test/dummy/db/test.sqlite3.db
96
+ - test/dummy/test/setting_test.rb
97
+ - test/test_helper.rb