acts_as_having_string_id 0.1.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: 3ebaa9731f8d0e4405bfc9a05a7a9b9ff0dd9e31
4
+ data.tar.gz: ca0d9fce70599e3b82bdaf3c7321a669669e4dfd
5
+ SHA512:
6
+ metadata.gz: e625c3f0785cda0b1b1a913992fef3ead5751546fb8590bfdea81ad8247ce133b9f4f414339629b72a3019f2a1feca900533ee54768eb01c479274708cc12678
7
+ data.tar.gz: 20a95e36d732d027d009cc11cbbc66c35588d58d7ca7fc4ea874339c1a3d61f75512705b4182baf17045a457f8733010101e72dce9ed20cc9fc4dcf9ba17ee2c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Magnus Hult
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,113 @@
1
+ # ActsAsHavingStringId
2
+ A Rails plugin for exposing non-sequential (Youtube-like) string IDs instead of the sequential integer IDs provided by Rails.
3
+
4
+ Before, your API may look like
5
+
6
+ GET /users/123
7
+ {
8
+ "id": 123,
9
+ "name": "Alice O'User"
10
+ }
11
+
12
+ After
13
+
14
+ GET /users/9w63Hubh4oL
15
+ {
16
+ "id": "9w63Hubh4oL",
17
+ "name": "Alice O'User"
18
+ }
19
+
20
+ Exposing sequential integer IDs has several drawbacks:
21
+
22
+ * Javascript has a 53-bit limit for integers (see https://dev.twitter.com/overview/api/twitter-ids-json-and-snowflake), which is a problem if you have large IDs
23
+ * Perhaps you don't want objects to be easily enumerable, even if they're public (if you know about http://example.com/documents/104, it's way too easy to find document 105)
24
+ * Sequential IDs make it easy to know how much usage your product gets (if my newly created user is http://example.com/users/1337, your product probably has 1,337 users)
25
+
26
+ Rails makes heavy use of sequential integer IDs internally, but there's no need of exposing them. `ActsAsHavingStringId` provides an alternative string representation of your IDs. This representation is
27
+
28
+ base62(tea(id, md5(ModelClass.name + Rails.application.secrets.string_id_key)))
29
+
30
+ The representation looks something like "E0znqip4mRA".
31
+
32
+ `tea` above is the "New variant" of the [Tiny Encryption Algorithm](https://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm). You should probably not consider your id to be forever secret, but it should be pretty hard to figure out from the string representation.
33
+
34
+ Your controllers will continue to work without modification, but will start to accept string IDs. So if http://example.com/orders/104 worked before, something like http://example.com/orders/E0znqip4mRA should magically work.
35
+
36
+ You do however need to take care never to expose the `id` member of your models. Instead, use `id_string`.
37
+
38
+ ## Usage
39
+ First, set up your `secrets.yml`:
40
+
41
+ development:
42
+ string_id_key: notverysecret
43
+
44
+ test:
45
+ string_id_key: notverysecreteither
46
+
47
+ production:
48
+ string_id_key: <%= ENV["STRING_ID_KEY"] %>
49
+
50
+ Then, call the method in your model class:
51
+
52
+ class MyModel < ApplicationRecord
53
+ acts_as_having_string_id
54
+ end
55
+
56
+ The string representation is now available as `id_string` on your model object. As an example:
57
+
58
+ > m = MyModel.create!
59
+ > m.id
60
+ => 1
61
+ > m.id_string
62
+ => "7EajpSfdWIf"
63
+
64
+ All ActiveRecord functions will also accept the string representation as input:
65
+
66
+ > MyModel.find("7EajpSfdWIf")
67
+ => #<MyModel id: 1, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">
68
+ > MyModel.where(id: "7EajpSfdWIf")
69
+ => #<ActiveRecord::Relation [#<MyModel id: 1, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">]>
70
+
71
+ Then, for exposing your string ID, use the `id_string` method. For example, if you're using [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers):
72
+
73
+ class UserSerializer < ActiveModel::Serializer
74
+ attributes :id, :name
75
+
76
+ def id
77
+ object.id_string
78
+ end
79
+ end
80
+
81
+ And that's just about it!
82
+
83
+ ## TODO
84
+ * Publish on rubygems
85
+ * Since the `MyModel.find("7EajpSfdWIf")` functionality depends on the argument now being a string, `MyModel.find("5")` will no longer mean `MyModel.find(5)`, but rather `MyModel.find(4387534)` or something. Is that a problem?
86
+ * It's a potential security problem that we don't force strings from controllers (integer id coming from JSON postdata will make it find by original id)
87
+ * Although TEA handles (and outputs) 64-bit ids, we currently limit the input to 32-bit
88
+
89
+ ## Installation
90
+ Add this line to your application's Gemfile:
91
+
92
+ ```ruby
93
+ gem 'acts_as_having_string_id'
94
+ ```
95
+
96
+ And then execute:
97
+ ```bash
98
+ $ bundle
99
+ ```
100
+
101
+ Or install it yourself as:
102
+ ```bash
103
+ $ gem install acts_as_having_string_id
104
+ ```
105
+
106
+ ## Contributing
107
+ To contribute, fork the repo, edit the code and create a pull request with tests. :)
108
+
109
+ ## Acknowledgements
110
+ The Tiny Encryption Algorithm was created by David Wheeler and Roger Needham of the Cambridge Computer Laboratory. This library's implementation is based on [this code](https://github.com/pmarreck/ruby-snippets/blob/master/TEA.rb) by Jeremy Hinegardner.
111
+
112
+ ## License
113
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ActsAsHavingStringId'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,7 @@
1
+ module ActsAsHavingStringId
2
+ class Railtie < Rails::Railtie
3
+ initializer "railtie.include_in_application_record" do
4
+ ApplicationRecord.include(ActsAsHavingStringId)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module ActsAsHavingStringId
2
+ class StringId < ActiveRecord::Type::Value
3
+ def initialize(tea)
4
+ @tea = tea
5
+ end
6
+
7
+ def serialize(value)
8
+ if value.is_a? String
9
+ i = @tea.decrypt(value.base62_decode)
10
+ if i >= 2**31
11
+ # Since Postgres SERIAL is a signed 32-bit integer, we can
12
+ # only represent integers up until (2**32)-1. If we're
13
+ # serializing a larger id, we want a not found rather than
14
+ # a postgres datatype out of bounds error. WHERE id = -1
15
+ # will definitely not be found.
16
+ return -1
17
+ end
18
+ return i
19
+ else
20
+ value
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,117 @@
1
+ #---------------------------------------------------------------------
2
+ # This is a pure ruby implementation of the Tiny Encryption Algorithm
3
+ # (TEA) by David Wheeler and Roger Needham of the Cambridge Computer
4
+ # Laboratory.
5
+ #
6
+ # For more information:
7
+ #
8
+ # http://www.simonshepherd.supanet.com/tea.htm
9
+ #
10
+ # This is an implementation of the 'New Variant' of the cipher.
11
+ #
12
+ # ====================================================================
13
+ # Copyright (c) 2005, 2006 Jeremy Hinegardner <jeremy@hinegardner.org>
14
+ #
15
+ # This implementation of the TEA New Variant is released under
16
+ # the MIT License:
17
+ #
18
+ # http://www.opensource.org/licenses/mit-license.html
19
+ #
20
+ # ====================================================================
21
+ #
22
+ # Altered for Gem suitability and to support encryption of 64-bit
23
+ # integers by Magnus Hult
24
+ #
25
+ #---------------------------------------------------------------------
26
+ # Ruby 1.8 compatibility
27
+ if RUBY_VERSION.include?('1.8')
28
+ class Fixnum; def ord; return self; end; end
29
+ end
30
+ require 'digest/md5'
31
+ module ActsAsHavingStringId
32
+ class TEA
33
+ DELTA = 0x9e3779b9
34
+ ITERATIONS = 32
35
+
36
+ def initialize(pass_phrase)
37
+ @key = passphrase_to_key(pass_phrase)
38
+ end
39
+
40
+ def encrypt(num)
41
+ nums = to_32bit_ints(num)
42
+ enc = encrypt_chunk(nums[0], nums[1], @key)
43
+ from_32bit_ints(enc[0], enc[1])
44
+ end
45
+
46
+ def decrypt(num)
47
+ nums = to_32bit_ints(num)
48
+ dec = decrypt_chunk(nums[0], nums[1], @key)
49
+ from_32bit_ints(dec[0], dec[1])
50
+ end
51
+
52
+ ############
53
+ private
54
+ ############
55
+
56
+ def to_32bit_ints(num)
57
+ # From a 64-bit integer, return an array of two 32-bit integers
58
+ # high bits first
59
+ [(num & 0xFFFFFFFF00000000) >> 32, num & 0x00000000FFFFFFFF]
60
+ end
61
+
62
+ def from_32bit_ints(num1, num2)
63
+ # From two 32-bit integers, high bits first, return
64
+ # a 64-bit integer
65
+ (num1 << 32) | num2
66
+ end
67
+
68
+ #-------------------------------------------------------------
69
+ # convert the given passphrase to and MD5 sum and get the 128
70
+ # bit key as 4 x 32 bit ints
71
+ #-------------------------------------------------------------
72
+ def passphrase_to_key(pass_phrase)
73
+ Digest::MD5.digest(pass_phrase).unpack('L*')
74
+ end
75
+
76
+
77
+ #-------------------------------------------------------------
78
+ # encrypt 2 of the integers ( 8 characters ) of the input into
79
+ # the cipher text output
80
+ #-------------------------------------------------------------
81
+ def encrypt_chunk(num1,num2,key)
82
+ y,z,sum = num1,num2,0
83
+
84
+ ITERATIONS.times do |i|
85
+ y += ( z << 4 ^ z >> 5) + z ^ sum + key[sum & 3]
86
+ y = y & 0xFFFFFFFF;
87
+
88
+ sum += DELTA
89
+ z += ( y << 4 ^ y >> 5) + y ^ sum + key[sum >> 11 & 3]
90
+ z = z & 0xFFFFFFFF;
91
+
92
+ # ruby can keep on getting bigger because of Bignum so
93
+ # you have to and with 0xFFFFFFFF to get the Fixnum
94
+ # bytes
95
+
96
+ end
97
+ return [y,z]
98
+ end
99
+
100
+
101
+ #-------------------------------------------------------------
102
+ # decrypt 2 of the integer cipher texts into the plaintext
103
+ #-------------------------------------------------------------
104
+ def decrypt_chunk(num1,num2,key)
105
+ y,z = num1,num2
106
+ sum = DELTA << 5
107
+ ITERATIONS.times do |i|
108
+ z -= ( y << 4 ^ y >> 5) + y ^ sum + key[sum >> 11 & 3]
109
+ z = z & 0xFFFFFFFF
110
+ sum -= DELTA
111
+ y -= ( z << 4 ^ z >> 5) + z ^ sum + key[sum & 3]
112
+ y = y & 0xFFFFFFFF
113
+ end
114
+ return [y,z]
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsHavingStringId
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,28 @@
1
+ require 'base62'
2
+ require 'acts_as_having_string_id/tea'
3
+ require 'acts_as_having_string_id/string_id'
4
+ require 'acts_as_having_string_id/railtie' if defined?(Rails)
5
+
6
+ module ActsAsHavingStringId
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def acts_as_having_string_id(options = {})
11
+ class_eval do
12
+ attribute :id, ActsAsHavingStringId::StringId.new(_tea)
13
+ end
14
+ include ActsAsHavingStringId::LocalInstanceMethods
15
+ end
16
+
17
+ def _tea
18
+ pass_phrase = self.class.name + Rails.application.secrets.string_id_key
19
+ @_tea ||= ActsAsHavingStringId::TEA.new(pass_phrase)
20
+ end
21
+ end
22
+
23
+ module LocalInstanceMethods
24
+ def id_string
25
+ self.class._tea.encrypt(id).base62_encode
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :acts_as_having_string_id do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_having_string_id
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Magnus Hult
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-01 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: 5.0.0
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 5.0.0.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 5.0.0
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 5.0.0.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: base62
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 1.0.0
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 1.0.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: sqlite3
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ description: Makes a model accept and expose a seemingly random string id
62
+ email:
63
+ - magnus@magnushult.se
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - MIT-LICENSE
69
+ - README.md
70
+ - Rakefile
71
+ - lib/acts_as_having_string_id.rb
72
+ - lib/acts_as_having_string_id/railtie.rb
73
+ - lib/acts_as_having_string_id/string_id.rb
74
+ - lib/acts_as_having_string_id/tea.rb
75
+ - lib/acts_as_having_string_id/version.rb
76
+ - lib/tasks/acts_as_having_string_id_tasks.rake
77
+ homepage: http://github.com/hult/acts_as_having_string_id
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.5.1
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Makes a model accept and expose a seemingly random string id
101
+ test_files: []