acts_as_having_string_id 0.1.0

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