effective_obfuscation 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2893d0d095c6f1b6fbdc1d972a225987d42b6c76
4
+ data.tar.gz: 38c42ee37f546e9c982fba0b02c8ac420fa1d811
5
+ SHA512:
6
+ metadata.gz: b651ce15257c280d26618bd743c59ec3bd7dbf3b21d9c0eb758440b40296d873fbb2878e93a37ccd086888dcbd1d2ee9a893c357537f797b6d9bf76537879d3d
7
+ data.tar.gz: 557b20e2d11dc0eea445efd9dd39d21e48eb4ae4be04b357fbd8e85bd8c667d4299ece5c538532303ace640e2a02d62cbb6d70f33d93da0bf539ee3f876159fc
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 Code and Effect Inc.
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.md ADDED
@@ -0,0 +1,190 @@
1
+ # Effective Obfuscation
2
+
3
+ Display unique 10-digit numbers instead of ActiveRecord IDs. Hides the ID param so curious website visitors are unable to determine your user or order count.
4
+
5
+ Turn a URL like:
6
+
7
+ ```ruby
8
+ http://example.com/users/3
9
+ ```
10
+
11
+ into something like:
12
+
13
+ ```ruby
14
+ http://example.com/users/2356513904
15
+ ```
16
+
17
+ Sequential ActiveRecord ids become non-sequential, random looking, numeric ids.
18
+
19
+ ```ruby
20
+ # user 7000
21
+ http://example.com/users/5270192353
22
+ # user 7001
23
+ http://example.com/users/7107163820
24
+ # user 7002
25
+ http://example.com/user/3296163828
26
+ ```
27
+
28
+ This is a Rails 4 compatible version of obfuscate_id (https://github.com/namick/obfuscate_id) which also adds totally automatic integration with Rails finder methods.
29
+
30
+
31
+ ## Getting Started
32
+
33
+ Add to Gemfile:
34
+
35
+ ```ruby
36
+ gem 'effective_obfuscation', :git => 'https://github.com/code-and-effect/effective_obfuscation.git'
37
+ ```
38
+
39
+ Run the bundle command to install it:
40
+
41
+ ```console
42
+ bundle install
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ### Basic
48
+
49
+ Add the mixin to an existing model:
50
+
51
+ ```ruby
52
+ class User
53
+ acts_as_obfuscated
54
+ end
55
+ ```
56
+
57
+ Thats it. Now URLs for a User will be generated as
58
+
59
+ ```ruby
60
+ http://example.com/users/2356513904
61
+ ```
62
+
63
+ As well, any find(), exists?(), find_by_id(), find_by(), where(:id => params[:id]) and all Arel table finder methods will be automatically translated to lookup the proper underlying ID.
64
+
65
+ You shouldn't require any changes to your view or controller code. Just Works with InherittedResources and ActiveAdmin.
66
+
67
+ ### Formatting
68
+
69
+ Because of the underlying ScatterSwap algorithm, the obfuscated IDs must be exactly 10 digits in length.
70
+
71
+ However, if you'd like to add some formatting to make the 10-digit number more human readable and over-the-phone friendly
72
+
73
+ ```ruby
74
+ class User
75
+ acts_as_obfuscated :format => '###-####-###'
76
+ end
77
+ ```
78
+
79
+ will generate URLs that look like
80
+
81
+ ```ruby
82
+ http://example.com/users/235-6513-904
83
+ ```
84
+
85
+ Any String.parameterize-able characters will work as long as there are exactly 10 # (hash symbol) characters in the format string somewhere.
86
+
87
+
88
+ ### ScatterSwap Spin
89
+
90
+ The Spin value is basically a salt used by the ScatterSwap algorithm to randomize integers.
91
+
92
+ In this gem, the default spin value is set on a per-model basis.
93
+
94
+ There is really no reason to change it; however, you can specify the spin value directly if you wish
95
+
96
+ ```ruby
97
+ class User
98
+ acts_as_obfuscated :spin => 123456789
99
+ end
100
+ ```
101
+
102
+ ### General Obfuscation
103
+
104
+ So maybe you just want access to the underlying ScatterSwap obfuscation algorithm including the additional model-specific formatting.
105
+
106
+ To obfuscate, pass any number as a string, or an integer
107
+
108
+ ```ruby
109
+ User.obfuscate(43) # Using acts_as_obfuscated :format => '###-####-###'
110
+ => "990-5826-174"
111
+ ```
112
+
113
+ And to de-obfuscate, pass any number as a string or an integer
114
+
115
+ ```ruby
116
+ User.deobfuscate("990-5826-174")
117
+ => 43
118
+
119
+ User.deobfuscate(9905826174)
120
+ => 43
121
+ ```
122
+
123
+ ### Searching by the Real (Database) ID
124
+
125
+ By default, all finder method except `find()` will work with both obfuscated and database IDs.
126
+
127
+ This means,
128
+
129
+ ```ruby
130
+ User.where(:id => "990-5826-174")
131
+ => User<id: 43>
132
+ ```
133
+
134
+ returns the same User as
135
+
136
+ ```ruby
137
+ User.where(:id => 43)
138
+ => User<id: 43>
139
+ ```
140
+
141
+ This behaviour is not applied to `find()` because it would allow a user to visit:
142
+
143
+ http://example.com/users/1
144
+ http://example.com/users/2
145
+ ...etc...
146
+
147
+ and enumerate all users.
148
+
149
+ Please continue to use @user = User.find(params[:id]) in your controller to prevent route enumeration.
150
+
151
+ Any other internally used finder methods, `where` and `find_by_id` should respond to both obfuscated and database IDs for maximum compatibility.
152
+
153
+ ## License
154
+
155
+ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
156
+
157
+ Code and Effect is the product arm of [AgileStyle](http://www.agilestyle.com/), an Edmonton-based shop that specializes in building custom web applications with Ruby on Rails.
158
+
159
+
160
+ ## Credits
161
+
162
+ This project was inspired by
163
+
164
+ ObfuscateID (https://github.com/namick/obfuscate_id)
165
+
166
+ and uses the same (simply genius!) underlying algorithm
167
+
168
+ ScatterSwap (https://github.com/namick/scatter_swap)
169
+
170
+
171
+ ## Testing
172
+
173
+ The test suite for this gem is unfortunately not yet complete.
174
+
175
+ Run tests by:
176
+
177
+ ```ruby
178
+ rake spec
179
+ ```
180
+
181
+
182
+ ## Contributing
183
+
184
+ 1. Fork it
185
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
186
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
187
+ 4. Push to the branch (`git push origin my-new-feature`)
188
+ 5. Bonus points for test coverage
189
+ 6. Create new Pull Request
190
+
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ # Testing tasks
9
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
10
+ load 'rails/tasks/engine.rake'
11
+
12
+ Bundler::GemHelper.install_tasks
13
+
14
+ require 'rspec/core'
15
+ require 'rspec/core/rake_task'
16
+
17
+ desc "Run all specs in spec directory (excluding plugin specs)"
18
+ RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
19
+
20
+ task :default => :spec
@@ -0,0 +1,174 @@
1
+ # ActsAsObfuscated
2
+ #
3
+ # This module automatically obfuscates IDs
4
+
5
+ # Mark your model with 'acts_as_obfuscated'
6
+
7
+ module ActsAsObfuscated
8
+ extend ActiveSupport::Concern
9
+
10
+ module ActiveRecord
11
+ def acts_as_obfuscated(options = nil)
12
+ @acts_as_obfuscated_opts = options || {}
13
+
14
+ # Guard against an improperly passed :format => '...' option
15
+ if @acts_as_obfuscated_opts[:format]
16
+ if @acts_as_obfuscated_opts[:format].to_s.count('#') != 10
17
+ raise Exception.new("acts_as_obfuscated :format => '...' value must contain exactly 10 # characters. Use something like :format => '###-####-###'")
18
+ end
19
+
20
+ format = @acts_as_obfuscated_opts[:format].gsub('#', '0')
21
+
22
+ if format.parameterize != format
23
+ raise Exception.new("acts_as_obfuscated :format => '...' value must contain only String.parameterize-able characters and the # character. Use something like :format => '###-####-###'")
24
+ end
25
+ end
26
+
27
+ include ::ActsAsObfuscated
28
+ end
29
+ end
30
+
31
+ included do
32
+ cattr_accessor :acts_as_obfuscated_opts
33
+ self.acts_as_obfuscated_opts = @acts_as_obfuscated_opts
34
+
35
+ # Set the spin based on model name if not explicity defined
36
+ self.acts_as_obfuscated_opts[:spin] ||= (
37
+ alphabet = Array('a'..'z')
38
+ self.name.split('').map { |char| alphabet.index(char) }.first(12).join.to_i
39
+ )
40
+
41
+ # We need to track the Maximum ID of this Table
42
+ self.acts_as_obfuscated_opts[:max_id] ||= (self.unscoped.maximum(:id) rescue 2147483647)
43
+
44
+ after_commit :on => :create do
45
+ self.class.acts_as_obfuscated_opts[:max_id] = nil
46
+ end
47
+
48
+ # Work with Ransack if available
49
+ if self.respond_to?(:ransacker)
50
+ ransacker :id, :formatter => Proc.new { |v| deobfuscate(v) } { |parent| parent.table[:id] }
51
+ end
52
+ end
53
+
54
+ module ClassMethods
55
+ def obfuscate(original)
56
+ obfuscated = EffectiveObfuscation.hide(original, acts_as_obfuscated_opts[:spin])
57
+
58
+ if acts_as_obfuscated_opts[:format] # Transform 1234567890 from ###-####-### into 123-4567-890 as per :format option
59
+ acts_as_obfuscated_opts[:format].dup.tap do |formatted|
60
+ 10.times { |x| formatted.sub!('#', obfuscated[x]) }
61
+ end
62
+ else
63
+ obfuscated
64
+ end
65
+ end
66
+
67
+ # If rescue_with_original_id is set to true the original ID will be returned when its Obfuscated Id is not found
68
+ #
69
+ # We use this as the default behaviour on everything except Class.find()
70
+ def deobfuscate(original, rescue_with_original_id = true)
71
+ if original.kind_of?(Array)
72
+ return original.map { |value| deobfuscate(value, true) } # Always rescue with original ID
73
+ elsif !(original.kind_of?(Integer) || original.kind_of?(String))
74
+ return original
75
+ end
76
+
77
+ if acts_as_obfuscated_opts[:format]
78
+ obfuscated_id = original.to_s.delete('^0-9')
79
+ else
80
+ obfuscated_id = original.to_s
81
+ end
82
+
83
+ # 2147483647 is PostgreSQL's Integer Max Value. If we return a value higher than this, we get weird DB errors
84
+ revealed = [EffectiveObfuscation.show(obfuscated_id, acts_as_obfuscated_opts[:spin]).to_i, 2147483647].min
85
+
86
+ if rescue_with_original_id && (revealed >= 2147483647 || revealed > deobfuscated_maximum_id)
87
+ original
88
+ else
89
+ revealed
90
+ end
91
+ end
92
+
93
+ def deobfuscator(left)
94
+ acts_as_obfuscated_opts[:deobfuscators] ||= Hash.new().tap do |deobfuscators|
95
+ deobfuscators['id'] = Proc.new { |right| self.deobfuscate(right) }
96
+
97
+ reflect_on_all_associations(:belongs_to).each do |reflection|
98
+ if reflection.klass.respond_to?(:deobfuscate)
99
+ deobfuscators[reflection.foreign_key] = Proc.new { |right| reflection.klass.deobfuscate(right) }
100
+ end
101
+ end
102
+ end
103
+
104
+ acts_as_obfuscated_opts[:deobfuscators][left.to_s]
105
+ end
106
+
107
+ def deobfuscated_maximum_id
108
+ acts_as_obfuscated_opts[:max_id] ||= (self.unscoped.maximum(:id) rescue 2147483647)
109
+ end
110
+
111
+ def relation
112
+ super.tap { |relation| relation.extend(FinderMethods) }
113
+ end
114
+ end
115
+
116
+ module FinderMethods
117
+ def find(*args)
118
+ super(deobfuscate(args.first, false))
119
+ end
120
+
121
+ def exists?(*args)
122
+ super(deobfuscate(args.first))
123
+ end
124
+
125
+ def find_by_id(*args)
126
+ super(deobfuscate(args.first))
127
+ end
128
+
129
+ def find_by(*args)
130
+ if args.first.kind_of?(Hash)
131
+ args.first.each do |left, right|
132
+ next unless (d = deobfuscator(left))
133
+ args.first[left] = d.call(right)
134
+ end
135
+ end
136
+
137
+ super(*args)
138
+ end
139
+
140
+ def where(*args)
141
+ if args.first.kind_of?(Hash)
142
+ args.first.each do |left, right|
143
+ next unless (d = deobfuscator(left))
144
+ args.first[left] = d.call(right)
145
+ end
146
+ elsif args.first.class.parent == Arel::Nodes
147
+ deobfuscate_arel!(args.first)
148
+ end
149
+
150
+ super(*args)
151
+ end
152
+
153
+ def deobfuscate_arel!(node)
154
+ nodes = node.kind_of?(Array) ? node : [node]
155
+
156
+ nodes.each do |node|
157
+ if node.respond_to?(:children)
158
+ deobfuscate_arel!(node.children)
159
+ elsif node.respond_to?(:expr)
160
+ deobfuscate_arel!(node.expr)
161
+ elsif node.respond_to?(:left)
162
+ next unless (d = deobfuscator(node.left.name))
163
+ node.right = d.call(node.right) unless (node.right.kind_of?(String) && node.right.include?('$'))
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ def to_param
170
+ self.class.obfuscate(self.id)
171
+ end
172
+
173
+ end
174
+
@@ -0,0 +1,12 @@
1
+ module EffectiveObfuscation
2
+ class Engine < ::Rails::Engine
3
+ config.autoload_paths += Dir["#{config.root}/app/models/concerns"]
4
+
5
+ # Include acts_as_addressable concern and allow any ActiveRecord object to call it
6
+ initializer 'effective_obfuscation.active_record' do |app|
7
+ ActiveSupport.on_load :active_record do
8
+ ActiveRecord::Base.extend(ActsAsObfuscated::ActiveRecord)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module EffectiveObfuscation
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,13 @@
1
+ require "effective_obfuscation/engine"
2
+ require "effective_obfuscation/version"
3
+ require 'scatter_swap'
4
+
5
+ module EffectiveObfuscation
6
+ def self.hide(id, spin)
7
+ ::ScatterSwap.hash(id, spin)
8
+ end
9
+
10
+ def self.show(id, spin)
11
+ ::ScatterSwap.reverse_hash(id, spin).to_i
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe EffectiveObfuscation do
4
+ it 'should be a module' do
5
+ assert_kind_of Module, EffectiveObfuscation
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ ENV["RAILS_ENV"] ||= 'test'
2
+
3
+ require File.expand_path("../dummy/config/environment", __FILE__)
4
+
5
+ require 'rspec/rails'
6
+ require 'rspec/autorun'
7
+
8
+ # Requires supporting ruby files with custom matchers and macros, etc,
9
+ # in spec/support/ and its subdirectories.
10
+ Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f }
11
+
12
+ RSpec.configure do |config|
13
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
14
+
15
+ Rails.logger.level = 4 # Output only minimal stuff to test.log
16
+
17
+ config.use_transactional_fixtures = true # Make this false to once again use DatabaseCleaner
18
+ config.infer_base_class_for_anonymous_controllers = false
19
+ config.order = 'random'
20
+ end
21
+
22
+ class ActiveRecord::Base
23
+ mattr_accessor :shared_connection
24
+ @@shared_connection = nil
25
+
26
+ def self.connection
27
+ @@shared_connection || retrieve_connection
28
+ end
29
+ end
30
+
31
+ # Forces all threads to share the same connection. This works on
32
+ # Capybara because it starts the web server in a thread.
33
+ ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: effective_obfuscation
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Code and Effect
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: scatter_swap
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.3
41
+ description: Display unique 10-digit numbers instead of ActiveRecord IDs. Hides the
42
+ ID param so curious website visitors are unable to determine your user or order
43
+ count.
44
+ email:
45
+ - info@codeandeffect.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - MIT-LICENSE
51
+ - README.md
52
+ - Rakefile
53
+ - app/models/concerns/acts_as_obfuscated.rb
54
+ - lib/effective_obfuscation.rb
55
+ - lib/effective_obfuscation/engine.rb
56
+ - lib/effective_obfuscation/version.rb
57
+ - spec/effective_obfuscation_spec.rb
58
+ - spec/spec_helper.rb
59
+ homepage: https://github.com/code-and-effect/effective_obfuscation
60
+ licenses:
61
+ - MIT
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.4.3
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Display unique 10-digit numbers instead of ActiveRecord IDs. Hides the ID
83
+ param so curious website visitors are unable to determine your user or order count.
84
+ test_files:
85
+ - spec/effective_obfuscation_spec.rb
86
+ - spec/spec_helper.rb