globalid 1.0.1 → 1.2.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 +4 -4
- data/MIT-LICENSE +1 -2
- data/README.md +21 -2
- data/lib/global_id/fixture_set.rb +13 -5
- data/lib/global_id/global_id.rb +15 -12
- data/lib/global_id/identification.rb +99 -0
- data/lib/global_id/locator.rb +69 -18
- data/lib/global_id/railtie.rb +4 -1
- data/lib/global_id/signed_global_id.rb +18 -16
- data/lib/global_id/uri/gid.rb +35 -8
- data/lib/global_id/verifier.rb +2 -3
- data/lib/global_id.rb +5 -1
- metadata +10 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15140d3d09fd236ea096c178a473440eb3d2fab55503978d646f3f13a863589c
|
4
|
+
data.tar.gz: 65351562c30c6c4e68d5b21ac8a502a7f7f5c8995cf538844395d123c685afba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2a642776d9233ad70854e49b1dde8287397b94321a158747abc6738c4a976a4bb93a5de9fa72d8e3f7792a5667cc01eb938200b0fb8da675bce28af8488c93d
|
7
|
+
data.tar.gz: c390cbaf842ce68dfd97e4a6d94ae40f9b83fa694ee5931975d96d1b8bd4c8795fbb308a12d3e0ba54f5e8b87d16229a665abe86aa9d00ec0e5e98bb5777ad23
|
data/MIT-LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2014-
|
1
|
+
Copyright (c) 2014-2023 David Heinemeier Hansson
|
2
2
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining
|
4
4
|
a copy of this software and associated documentation files (the
|
@@ -18,4 +18,3 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
18
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
19
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
20
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
-
|
data/README.md
CHANGED
@@ -161,6 +161,25 @@ GlobalID::Locator.locate_many gids
|
|
161
161
|
|
162
162
|
Note the order is maintained in the returned results.
|
163
163
|
|
164
|
+
### Options
|
165
|
+
|
166
|
+
Either `GlobalID::Locator.locate` or `GlobalID::Locator.locate_many` supports a hash of options as second parameter. The supported options are:
|
167
|
+
|
168
|
+
* :includes - A Symbol, Array, Hash or combination of them
|
169
|
+
The same structure you would pass into a `includes` method of Active Record.
|
170
|
+
See [Active Record eager loading associations](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations)
|
171
|
+
If present, `locate` or `locate_many` will eager load all the relationships specified here.
|
172
|
+
Note: It only works if all the gids models have that relationships.
|
173
|
+
* :only - A class, module or Array of classes and/or modules that are
|
174
|
+
allowed to be located. Passing one or more classes limits instances of returned
|
175
|
+
classes to those classes or their subclasses. Passing one or more modules in limits
|
176
|
+
instances of returned classes to those including that module. If no classes or
|
177
|
+
modules match, +nil+ is returned.
|
178
|
+
* :ignore_missing (Only for `locate_many`) - By default, `locate_many` will call `#find` on the model to locate the
|
179
|
+
ids extracted from the GIDs. In Active Record (and other data stores following the same pattern),
|
180
|
+
`#find` will raise an exception if a named ID can't be found. When you set this option to true,
|
181
|
+
we will use `#where(id: ids)` instead, which does not raise on missing records.
|
182
|
+
|
164
183
|
### Custom App Locator
|
165
184
|
|
166
185
|
A custom locator can be set for an app by calling `GlobalID::Locator.use` and providing an app locator to use for that app.
|
@@ -172,7 +191,7 @@ A custom locator can either be a block or a class.
|
|
172
191
|
Using a block:
|
173
192
|
|
174
193
|
```ruby
|
175
|
-
GlobalID::Locator.use :foo do |gid|
|
194
|
+
GlobalID::Locator.use :foo do |gid, options|
|
176
195
|
FooRemote.const_get(gid.model_name).find(gid.model_id)
|
177
196
|
end
|
178
197
|
```
|
@@ -182,7 +201,7 @@ Using a class:
|
|
182
201
|
```ruby
|
183
202
|
GlobalID::Locator.use :bar, BarLocator.new
|
184
203
|
class BarLocator
|
185
|
-
def locate(gid)
|
204
|
+
def locate(gid, options = {})
|
186
205
|
@search_client.search name: gid.model_name, id: gid.model_id
|
187
206
|
end
|
188
207
|
end
|
@@ -2,12 +2,20 @@
|
|
2
2
|
|
3
3
|
class GlobalID
|
4
4
|
module FixtureSet
|
5
|
-
def
|
6
|
-
|
7
|
-
|
8
|
-
uri = URI::GID.build([GlobalID.app, model_name, identifier, {}])
|
5
|
+
def global_id(fixture_set_name, label, column_type: :integer, **options)
|
6
|
+
create_global_id(fixture_set_name, label, column_type: column_type, klass: GlobalID, **options)
|
7
|
+
end
|
9
8
|
|
10
|
-
|
9
|
+
def signed_global_id(fixture_set_name, label, column_type: :integer, **options)
|
10
|
+
create_global_id(fixture_set_name, label, column_type: column_type, klass: SignedGlobalID, **options)
|
11
11
|
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def create_global_id(fixture_set_name, label, klass:, column_type: :integer, **options)
|
15
|
+
identifier = identify(label, column_type)
|
16
|
+
model_name = default_fixture_model_name(fixture_set_name)
|
17
|
+
uri = URI::GID.build([GlobalID.app, model_name, identifier, {}])
|
18
|
+
klass.new(uri, **options)
|
19
|
+
end
|
12
20
|
end
|
13
21
|
end
|
data/lib/global_id/global_id.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'active_support'
|
2
1
|
require 'active_support/core_ext/string/inflections' # For #model_class constantize
|
3
2
|
require 'active_support/core_ext/array/access'
|
4
3
|
require 'active_support/core_ext/object/try' # For #find
|
@@ -35,18 +34,12 @@ class GlobalID
|
|
35
34
|
|
36
35
|
private
|
37
36
|
def parse_encoded_gid(gid, options)
|
38
|
-
new(Base64.urlsafe_decode64(
|
39
|
-
end
|
40
|
-
|
41
|
-
# We removed the base64 padding character = during #to_param, now we're adding it back so decoding will work
|
42
|
-
def repad_gid(gid)
|
43
|
-
padding_chars = gid.length.modulo(4).zero? ? 0 : (4 - gid.length.modulo(4))
|
44
|
-
gid + ('=' * padding_chars)
|
37
|
+
new(Base64.urlsafe_decode64(gid), options) rescue nil
|
45
38
|
end
|
46
39
|
end
|
47
40
|
|
48
41
|
attr_reader :uri
|
49
|
-
delegate :app, :model_name, :model_id, :params, :to_s, to: :uri
|
42
|
+
delegate :app, :model_name, :model_id, :params, :to_s, :deconstruct_keys, to: :uri
|
50
43
|
|
51
44
|
def initialize(gid, options = {})
|
52
45
|
@uri = gid.is_a?(URI::GID) ? gid : URI::GID.parse(gid)
|
@@ -57,7 +50,14 @@ class GlobalID
|
|
57
50
|
end
|
58
51
|
|
59
52
|
def model_class
|
60
|
-
|
53
|
+
@model_class ||= begin
|
54
|
+
model = model_name.constantize
|
55
|
+
|
56
|
+
if model <= GlobalID
|
57
|
+
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
|
58
|
+
end
|
59
|
+
model
|
60
|
+
end
|
61
61
|
end
|
62
62
|
|
63
63
|
def ==(other)
|
@@ -70,7 +70,10 @@ class GlobalID
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def to_param
|
73
|
-
|
74
|
-
|
73
|
+
Base64.urlsafe_encode64(to_s, padding: false)
|
74
|
+
end
|
75
|
+
|
76
|
+
def as_json(*)
|
77
|
+
to_s
|
75
78
|
end
|
76
79
|
end
|
@@ -1,19 +1,118 @@
|
|
1
1
|
class GlobalID
|
2
|
+
# Mix `GlobalID::Identification` into any model with a `#find(id)` class
|
3
|
+
# method. Support is automatically included in Active Record.
|
4
|
+
#
|
5
|
+
# class Person
|
6
|
+
# include ActiveModel::Model
|
7
|
+
# include GlobalID::Identification
|
8
|
+
#
|
9
|
+
# attr_accessor :id
|
10
|
+
#
|
11
|
+
# def self.find(id)
|
12
|
+
# new id: id
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def ==(other)
|
16
|
+
# id == other.try(:id)
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# person_gid = Person.find(1).to_global_id
|
21
|
+
# # => #<GlobalID ...
|
22
|
+
# person_gid.uri
|
23
|
+
# # => #<URI ...
|
24
|
+
# person_gid.to_s
|
25
|
+
# # => "gid://app/Person/1"
|
26
|
+
# GlobalID::Locator.locate person_gid
|
27
|
+
# # => #<Person:0x007fae94bf6298 @id="1">
|
2
28
|
module Identification
|
29
|
+
|
30
|
+
# Returns the Global ID of the model.
|
31
|
+
#
|
32
|
+
# model = Person.new id: 1
|
33
|
+
# global_id = model.to_global_id
|
34
|
+
# global_id.modal_class # => Person
|
35
|
+
# global_id.modal_id # => "1"
|
36
|
+
# global_id.to_param # => "Z2lkOi8vYm9yZGZvbGlvL1BlcnNvbi8x"
|
3
37
|
def to_global_id(options = {})
|
4
38
|
GlobalID.create(self, options)
|
5
39
|
end
|
6
40
|
alias to_gid to_global_id
|
7
41
|
|
42
|
+
# Returns the Global ID parameter of the model.
|
43
|
+
#
|
44
|
+
# model = Person.new id: 1
|
45
|
+
# model.to_gid_param # => ""Z2lkOi8vYm9yZGZvbGlvL1BlcnNvbi8x"
|
8
46
|
def to_gid_param(options = {})
|
9
47
|
to_global_id(options).to_param
|
10
48
|
end
|
11
49
|
|
50
|
+
# Returns the Signed Global ID of the model.
|
51
|
+
# Signed Global IDs ensure that the data hasn't been tampered with.
|
52
|
+
#
|
53
|
+
# model = Person.new id: 1
|
54
|
+
# signed_global_id = model.to_signed_global_id
|
55
|
+
# signed_global_id.modal_class # => Person
|
56
|
+
# signed_global_id.modal_id # => "1"
|
57
|
+
# signed_global_id.to_param # => "BAh7CEkiCGdpZAY6BkVUSSIiZ2..."
|
58
|
+
#
|
59
|
+
# ==== Expiration
|
60
|
+
#
|
61
|
+
# Signed Global IDs can expire some time in the future. This is useful if
|
62
|
+
# there's a resource people shouldn't have indefinite access to, like a
|
63
|
+
# share link.
|
64
|
+
#
|
65
|
+
# expiring_sgid = Document.find(5).to_sgid(expires_in: 2.hours, for: 'sharing')
|
66
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
67
|
+
# # Within 2 hours...
|
68
|
+
# GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
|
69
|
+
# # => #<Document:0x007fae94bf6298 @id="5">
|
70
|
+
# # More than 2 hours later...
|
71
|
+
# GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
|
72
|
+
# # => nil
|
73
|
+
#
|
74
|
+
# In Rails, an auto-expiry of 1 month is set by default.
|
75
|
+
#
|
76
|
+
# You need to explicitly pass `expires_in: nil` to generate a permanent
|
77
|
+
# SGID that will not expire,
|
78
|
+
#
|
79
|
+
# never_expiring_sgid = Document.find(5).to_sgid(expires_in: nil)
|
80
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
81
|
+
#
|
82
|
+
# # Any time later...
|
83
|
+
# GlobalID::Locator.locate_signed never_expiring_sgid
|
84
|
+
# # => #<Document:0x007fae94bf6298 @id="5">
|
85
|
+
#
|
86
|
+
# It's also possible to pass a specific expiry time
|
87
|
+
#
|
88
|
+
# explicit_expiring_sgid = SecretAgentMessage.find(5).to_sgid(expires_at: Time.now.advance(hours: 1))
|
89
|
+
# # => #<SignedGlobalID:0x008fde45df8937 ...>
|
90
|
+
#
|
91
|
+
# # 1 hour later...
|
92
|
+
# GlobalID::Locator.locate_signed explicit_expiring_sgid.to_s
|
93
|
+
# # => nil
|
94
|
+
#
|
95
|
+
# Note that an explicit `:expires_at` takes precedence over a relative `:expires_in`.
|
96
|
+
#
|
97
|
+
# ==== Purpose
|
98
|
+
#
|
99
|
+
# You can even bump the security up some more by explaining what purpose a
|
100
|
+
# Signed Global ID is for. In this way evildoers can't reuse a sign-up
|
101
|
+
# form's SGID on the login page. For example.
|
102
|
+
#
|
103
|
+
# signup_person_sgid = Person.find(1).to_sgid(for: 'signup_form')
|
104
|
+
# # => #<SignedGlobalID:0x007fea1984b520
|
105
|
+
# GlobalID::Locator.locate_signed(signup_person_sgid.to_s, for: 'signup_form')
|
106
|
+
# => #<Person:0x007fae94bf6298 @id="1">
|
12
107
|
def to_signed_global_id(options = {})
|
13
108
|
SignedGlobalID.create(self, options)
|
14
109
|
end
|
15
110
|
alias to_sgid to_signed_global_id
|
16
111
|
|
112
|
+
# Returns the Signed Global ID parameter.
|
113
|
+
#
|
114
|
+
# model = Person.new id: 1
|
115
|
+
# model.to_sgid_param # => "BAh7CEkiCGdpZAY6BkVUSSIiZ2..."
|
17
116
|
def to_sgid_param(options = {})
|
18
117
|
to_signed_global_id(options).to_param
|
19
118
|
end
|
data/lib/global_id/locator.rb
CHANGED
@@ -1,20 +1,34 @@
|
|
1
|
-
require 'active_support'
|
2
1
|
require 'active_support/core_ext/enumerable' # For Enumerable#index_by
|
3
2
|
|
4
3
|
class GlobalID
|
5
4
|
module Locator
|
5
|
+
class InvalidModelIdError < StandardError; end
|
6
|
+
|
6
7
|
class << self
|
7
8
|
# Takes either a GlobalID or a string that can be turned into a GlobalID
|
8
9
|
#
|
9
10
|
# Options:
|
11
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them.
|
12
|
+
# The same structure you would pass into a +includes+ method of Active Record.
|
13
|
+
# If present, locate will load all the relationships specified here.
|
14
|
+
# See https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations.
|
10
15
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
11
16
|
# allowed to be located. Passing one or more classes limits instances of returned
|
12
17
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
13
18
|
# instances of returned classes to those including that module. If no classes or
|
14
19
|
# modules match, +nil+ is returned.
|
15
20
|
def locate(gid, options = {})
|
16
|
-
|
17
|
-
|
21
|
+
gid = GlobalID.parse(gid)
|
22
|
+
|
23
|
+
return unless gid && find_allowed?(gid.model_class, options[:only])
|
24
|
+
|
25
|
+
locator = locator_for(gid)
|
26
|
+
|
27
|
+
if locator.method(:locate).arity == 1
|
28
|
+
GlobalID.deprecator.warn "It seems your locator is defining the `locate` method only with one argument. Please make sure your locator is receiving the options argument as well, like `locate(gid, options = {})`."
|
29
|
+
locator.locate(gid)
|
30
|
+
else
|
31
|
+
locator.locate(gid, options.except(:only))
|
18
32
|
end
|
19
33
|
end
|
20
34
|
|
@@ -29,6 +43,11 @@ class GlobalID
|
|
29
43
|
# per model class, but still interpolate the results to match the order in which the gids were passed.
|
30
44
|
#
|
31
45
|
# Options:
|
46
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
47
|
+
# The same structure you would pass into a includes method of Active Record.
|
48
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
49
|
+
# If present, locate_many will load all the relationships specified here.
|
50
|
+
# Note: It only works if all the gids models have that relationships.
|
32
51
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
33
52
|
# allowed to be located. Passing one or more classes limits instances of returned
|
34
53
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
@@ -50,6 +69,10 @@ class GlobalID
|
|
50
69
|
# Takes either a SignedGlobalID or a string that can be turned into a SignedGlobalID
|
51
70
|
#
|
52
71
|
# Options:
|
72
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
73
|
+
# The same structure you would pass into a includes method of Active Record.
|
74
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
75
|
+
# If present, locate_signed will load all the relationships specified here.
|
53
76
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
54
77
|
# allowed to be located. Passing one or more classes limits instances of returned
|
55
78
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
@@ -67,6 +90,11 @@ class GlobalID
|
|
67
90
|
# the results to match the order in which the gids were passed.
|
68
91
|
#
|
69
92
|
# Options:
|
93
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
94
|
+
# The same structure you would pass into a includes method of Active Record.
|
95
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
96
|
+
# If present, locate_many_signed will load all the relationships specified here.
|
97
|
+
# Note: It only works if all the gids models have that relationships.
|
70
98
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
71
99
|
# allowed to be located. Passing one or more classes limits instances of returned
|
72
100
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
@@ -83,7 +111,7 @@ class GlobalID
|
|
83
111
|
#
|
84
112
|
# Using a block:
|
85
113
|
#
|
86
|
-
# GlobalID::Locator.use :foo do |gid|
|
114
|
+
# GlobalID::Locator.use :foo do |gid, options|
|
87
115
|
# FooRemote.const_get(gid.model_name).find(gid.model_id)
|
88
116
|
# end
|
89
117
|
#
|
@@ -92,7 +120,7 @@ class GlobalID
|
|
92
120
|
# GlobalID::Locator.use :bar, BarLocator.new
|
93
121
|
#
|
94
122
|
# class BarLocator
|
95
|
-
# def locate(gid)
|
123
|
+
# def locate(gid, options = {})
|
96
124
|
# @search_client.search name: gid.model_name, id: gid.model_id
|
97
125
|
# end
|
98
126
|
# end
|
@@ -126,32 +154,55 @@ class GlobalID
|
|
126
154
|
@locators = {}
|
127
155
|
|
128
156
|
class BaseLocator
|
129
|
-
def locate(gid)
|
130
|
-
|
157
|
+
def locate(gid, options = {})
|
158
|
+
return unless model_id_is_valid?(gid)
|
159
|
+
model_class = gid.model_class
|
160
|
+
model_class = model_class.includes(options[:includes]) if options[:includes]
|
161
|
+
|
162
|
+
model_class.find gid.model_id
|
131
163
|
end
|
132
164
|
|
133
165
|
def locate_many(gids, options = {})
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
166
|
+
ids_by_model = Hash.new { |hash, key| hash[key] = [] }
|
167
|
+
|
168
|
+
gids.each do |gid|
|
169
|
+
next unless model_id_is_valid?(gid)
|
170
|
+
ids_by_model[gid.model_class] << gid.model_id
|
171
|
+
end
|
172
|
+
|
173
|
+
records_by_model_name_and_id = {}
|
139
174
|
|
140
|
-
|
175
|
+
ids_by_model.each do |model, ids|
|
176
|
+
records = find_records(model, ids, ignore_missing: options[:ignore_missing], includes: options[:includes])
|
177
|
+
|
178
|
+
records_by_id = records.index_by do |record|
|
179
|
+
record.id.is_a?(Array) ? record.id.map(&:to_s) : record.id.to_s
|
180
|
+
end
|
181
|
+
|
182
|
+
records_by_model_name_and_id[model.name] = records_by_id
|
183
|
+
end
|
184
|
+
|
185
|
+
gids.filter_map { |gid| records_by_model_name_and_id[gid.model_name][gid.model_id] }
|
141
186
|
end
|
142
187
|
|
143
188
|
private
|
144
189
|
def find_records(model_class, ids, options)
|
190
|
+
model_class = model_class.includes(options[:includes]) if options[:includes]
|
191
|
+
|
145
192
|
if options[:ignore_missing]
|
146
|
-
model_class.where(
|
193
|
+
model_class.where(model_class.primary_key => ids)
|
147
194
|
else
|
148
195
|
model_class.find(ids)
|
149
196
|
end
|
150
197
|
end
|
198
|
+
|
199
|
+
def model_id_is_valid?(gid)
|
200
|
+
Array(gid.model_id).size == Array(gid.model_class.primary_key).size
|
201
|
+
end
|
151
202
|
end
|
152
203
|
|
153
204
|
class UnscopedLocator < BaseLocator
|
154
|
-
def locate(gid)
|
205
|
+
def locate(gid, options = {})
|
155
206
|
unscoped(gid.model_class) { super }
|
156
207
|
end
|
157
208
|
|
@@ -175,12 +226,12 @@ class GlobalID
|
|
175
226
|
@locator = block
|
176
227
|
end
|
177
228
|
|
178
|
-
def locate(gid)
|
179
|
-
@locator.call(gid)
|
229
|
+
def locate(gid, options = {})
|
230
|
+
@locator.call(gid, options)
|
180
231
|
end
|
181
232
|
|
182
233
|
def locate_many(gids, options = {})
|
183
|
-
gids.map { |gid| locate(gid) }
|
234
|
+
gids.map { |gid| locate(gid, options) }
|
184
235
|
end
|
185
236
|
end
|
186
237
|
end
|
data/lib/global_id/railtie.rb
CHANGED
@@ -3,7 +3,6 @@ require 'rails/railtie'
|
|
3
3
|
rescue LoadError
|
4
4
|
else
|
5
5
|
require 'global_id'
|
6
|
-
require 'active_support'
|
7
6
|
require 'active_support/core_ext/string/inflections'
|
8
7
|
require 'active_support/core_ext/integer/time'
|
9
8
|
|
@@ -43,6 +42,10 @@ class GlobalID
|
|
43
42
|
send :extend, GlobalID::FixtureSet
|
44
43
|
end
|
45
44
|
end
|
45
|
+
|
46
|
+
initializer "web_console.deprecator" do |app|
|
47
|
+
app.deprecators[:global_id] = GlobalID.deprecator if app.respond_to?(:deprecators)
|
48
|
+
end
|
46
49
|
end
|
47
50
|
end
|
48
51
|
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'global_id'
|
2
1
|
require 'active_support/message_verifier'
|
3
2
|
require 'time'
|
4
3
|
|
@@ -6,7 +5,7 @@ class SignedGlobalID < GlobalID
|
|
6
5
|
class ExpiredMessage < StandardError; end
|
7
6
|
|
8
7
|
class << self
|
9
|
-
attr_accessor :verifier
|
8
|
+
attr_accessor :verifier, :expires_in
|
10
9
|
|
11
10
|
def parse(sgid, options = {})
|
12
11
|
super verify(sgid.to_s, options), options
|
@@ -20,8 +19,6 @@ class SignedGlobalID < GlobalID
|
|
20
19
|
end
|
21
20
|
end
|
22
21
|
|
23
|
-
attr_accessor :expires_in
|
24
|
-
|
25
22
|
DEFAULT_PURPOSE = "default"
|
26
23
|
|
27
24
|
def pick_purpose(options)
|
@@ -30,11 +27,22 @@ class SignedGlobalID < GlobalID
|
|
30
27
|
|
31
28
|
private
|
32
29
|
def verify(sgid, options)
|
30
|
+
verify_with_verifier_validated_metadata(sgid, options) ||
|
31
|
+
verify_with_legacy_self_validated_metadata(sgid, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def verify_with_verifier_validated_metadata(sgid, options)
|
35
|
+
pick_verifier(options).verify(sgid, purpose: pick_purpose(options))
|
36
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def verify_with_legacy_self_validated_metadata(sgid, options)
|
33
41
|
metadata = pick_verifier(options).verify(sgid)
|
34
42
|
|
35
43
|
raise_if_expired(metadata['expires_at'])
|
36
44
|
|
37
|
-
metadata['gid'] if pick_purpose(options) == metadata['purpose']
|
45
|
+
metadata['gid'] if pick_purpose(options)&.to_s == metadata['purpose']&.to_s
|
38
46
|
rescue ActiveSupport::MessageVerifier::InvalidSignature, ExpiredMessage
|
39
47
|
nil
|
40
48
|
end
|
@@ -56,25 +64,19 @@ class SignedGlobalID < GlobalID
|
|
56
64
|
end
|
57
65
|
|
58
66
|
def to_s
|
59
|
-
@sgid ||= @verifier.generate(
|
67
|
+
@sgid ||= @verifier.generate(@uri.to_s, purpose: purpose, expires_at: expires_at)
|
60
68
|
end
|
61
69
|
alias to_param to_s
|
62
70
|
|
63
|
-
def to_h
|
64
|
-
# Some serializers decodes symbol keys to symbols, others to strings.
|
65
|
-
# Using string keys remedies that.
|
66
|
-
{ 'gid' => @uri.to_s, 'purpose' => purpose, 'expires_at' => encoded_expiration }
|
67
|
-
end
|
68
|
-
|
69
71
|
def ==(other)
|
70
72
|
super && @purpose == other.purpose
|
71
73
|
end
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
75
|
+
def inspect # :nodoc:
|
76
|
+
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
|
77
|
+
end
|
77
78
|
|
79
|
+
private
|
78
80
|
def pick_expiration(options)
|
79
81
|
return options[:expires_at] if options.key?(:expires_at)
|
80
82
|
|
data/lib/global_id/uri/gid.rb
CHANGED
@@ -30,6 +30,11 @@ module URI
|
|
30
30
|
|
31
31
|
# Raised when creating a Global ID for a model without an id
|
32
32
|
class MissingModelIdError < URI::InvalidComponentError; end
|
33
|
+
class InvalidModelIdError < URI::InvalidComponentError; end
|
34
|
+
|
35
|
+
# Maximum size of a model id segment
|
36
|
+
COMPOSITE_MODEL_ID_MAX_SIZE = 20
|
37
|
+
COMPOSITE_MODEL_ID_DELIMITER = "/"
|
33
38
|
|
34
39
|
class << self
|
35
40
|
# Validates +app+'s as URI hostnames containing only alphanumeric characters
|
@@ -83,7 +88,8 @@ module URI
|
|
83
88
|
def build(args)
|
84
89
|
parts = Util.make_components_hash(self, args)
|
85
90
|
parts[:host] = parts[:app]
|
86
|
-
|
91
|
+
model_id_segment = Array(parts[:model_id]).map { |p| CGI.escape(p.to_s) }.join(COMPOSITE_MODEL_ID_DELIMITER)
|
92
|
+
parts[:path] = "/#{parts[:model_name]}/#{model_id_segment}"
|
87
93
|
|
88
94
|
if parts[:params] && !parts[:params].empty?
|
89
95
|
parts[:query] = URI.encode_www_form(parts[:params])
|
@@ -98,6 +104,10 @@ module URI
|
|
98
104
|
"gid://#{app}#{path}#{'?' + query if query}"
|
99
105
|
end
|
100
106
|
|
107
|
+
def deconstruct_keys(_keys)
|
108
|
+
{app: app, model_name: model_name, model_id: model_id, params: params}
|
109
|
+
end
|
110
|
+
|
101
111
|
protected
|
102
112
|
def set_path(path)
|
103
113
|
set_model_components(path) unless defined?(@model_name) && @model_id
|
@@ -135,7 +145,7 @@ module URI
|
|
135
145
|
|
136
146
|
def check_scheme(scheme)
|
137
147
|
if scheme == 'gid'
|
138
|
-
|
148
|
+
true
|
139
149
|
else
|
140
150
|
raise URI::BadURIError, "Not a gid:// URI scheme: #{inspect}"
|
141
151
|
end
|
@@ -143,12 +153,22 @@ module URI
|
|
143
153
|
|
144
154
|
def set_model_components(path, validate = false)
|
145
155
|
_, model_name, model_id = path.split('/', 3)
|
146
|
-
validate_component(model_name) && validate_model_id(model_id, model_name) if validate
|
147
|
-
|
148
|
-
model_id = CGI.unescape(model_id) if model_id
|
149
156
|
|
157
|
+
validate_component(model_name) && validate_model_id_section(model_id, model_name) if validate
|
150
158
|
@model_name = model_name
|
151
|
-
|
159
|
+
|
160
|
+
if model_id
|
161
|
+
model_id_parts = model_id
|
162
|
+
.split(COMPOSITE_MODEL_ID_DELIMITER, COMPOSITE_MODEL_ID_MAX_SIZE)
|
163
|
+
.reject(&:blank?)
|
164
|
+
|
165
|
+
model_id_parts.map! do |id|
|
166
|
+
validate_model_id(id)
|
167
|
+
CGI.unescape(id)
|
168
|
+
end
|
169
|
+
|
170
|
+
@model_id = model_id_parts.length == 1 ? model_id_parts.first : model_id_parts
|
171
|
+
end
|
152
172
|
end
|
153
173
|
|
154
174
|
def validate_component(component)
|
@@ -158,13 +178,20 @@ module URI
|
|
158
178
|
"Expected a URI like gid://app/Person/1234: #{inspect}"
|
159
179
|
end
|
160
180
|
|
161
|
-
def
|
162
|
-
return model_id unless model_id.blank?
|
181
|
+
def validate_model_id_section(model_id, model_name)
|
182
|
+
return model_id unless model_id.blank?
|
163
183
|
|
164
184
|
raise MissingModelIdError, "Unable to create a Global ID for " \
|
165
185
|
"#{model_name} without a model id."
|
166
186
|
end
|
167
187
|
|
188
|
+
def validate_model_id(model_id_part)
|
189
|
+
return unless model_id_part.include?('/')
|
190
|
+
|
191
|
+
raise InvalidModelIdError, "Unable to create a Global ID for " \
|
192
|
+
"#{model_name} with a malformed model id."
|
193
|
+
end
|
194
|
+
|
168
195
|
def parse_query_params(query)
|
169
196
|
Hash[URI.decode_www_form(query)].with_indifferent_access if query
|
170
197
|
end
|
data/lib/global_id/verifier.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
|
-
require 'active_support'
|
2
1
|
require 'active_support/message_verifier'
|
3
2
|
|
4
3
|
class GlobalID
|
5
4
|
class Verifier < ActiveSupport::MessageVerifier
|
6
5
|
private
|
7
|
-
def encode(data)
|
6
|
+
def encode(data, **)
|
8
7
|
::Base64.urlsafe_encode64(data)
|
9
8
|
end
|
10
9
|
|
11
|
-
def decode(data)
|
10
|
+
def decode(data, **)
|
12
11
|
::Base64.urlsafe_decode64(data)
|
13
12
|
end
|
14
13
|
end
|
data/lib/global_id.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require 'global_id/global_id'
|
2
1
|
require 'active_support'
|
2
|
+
require 'global_id/global_id'
|
3
3
|
|
4
4
|
autoload :SignedGlobalID, 'global_id/signed_global_id'
|
5
5
|
|
@@ -16,4 +16,8 @@ class GlobalID
|
|
16
16
|
super
|
17
17
|
require 'global_id/signed_global_id'
|
18
18
|
end
|
19
|
+
|
20
|
+
def self.deprecator # :nodoc:
|
21
|
+
@deprecator ||= ActiveSupport::Deprecation.new("2.1", "GlobalID")
|
22
|
+
end
|
19
23
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: globalid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '6.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -59,8 +59,9 @@ files:
|
|
59
59
|
homepage: http://www.rubyonrails.org
|
60
60
|
licenses:
|
61
61
|
- MIT
|
62
|
-
metadata:
|
63
|
-
|
62
|
+
metadata:
|
63
|
+
rubygems_mfa_required: 'true'
|
64
|
+
post_install_message:
|
64
65
|
rdoc_options: []
|
65
66
|
require_paths:
|
66
67
|
- lib
|
@@ -75,8 +76,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
76
|
- !ruby/object:Gem::Version
|
76
77
|
version: '0'
|
77
78
|
requirements: []
|
78
|
-
rubygems_version: 3.
|
79
|
-
signing_key:
|
79
|
+
rubygems_version: 3.4.1
|
80
|
+
signing_key:
|
80
81
|
specification_version: 4
|
81
82
|
summary: 'Refer to any model with a URI: gid://app/class/id'
|
82
83
|
test_files: []
|