mournful_settings 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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