globalid 1.1.0 → 1.3.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/README.md +48 -7
- data/lib/global_id/global_id.rb +9 -4
- data/lib/global_id/identification.rb +99 -0
- data/lib/global_id/locator.rb +79 -19
- data/lib/global_id/railtie.rb +4 -0
- data/lib/global_id/signed_global_id.rb +18 -15
- data/lib/global_id/uri/gid.rb +33 -8
- data/lib/global_id/verifier.rb +2 -2
- data/lib/global_id.rb +4 -0
- 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: e7f6fd8b8b0b07aad73739e2004b5c4229e48dfa64960dcfa243f6c1b6276b44
|
|
4
|
+
data.tar.gz: a1cc24c9968a2236d2974f4ecac4d3fbb8513e4ac74d18685185afa684fad41d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68bd4cc42217eb59cff8b1f73c989e9f17ddce332f7d65f533e5b2fa6e429a10328d9c5b02f165e778cd83c02afa095103984910c098e75c9dcab788a4c97024
|
|
7
|
+
data.tar.gz: 652bc775cb1da110f6eca011d5a6ef3d907f79ddbfda755206d84eddca06c2153b734befb5b5dc6ea09ed911b87c72fcae6f9bfb5554da1b4616d60d22a298fb
|
data/README.md
CHANGED
|
@@ -57,7 +57,7 @@ GlobalID::Locator.locate_signed person_sgid
|
|
|
57
57
|
|
|
58
58
|
**Expiration**
|
|
59
59
|
|
|
60
|
-
Signed Global IDs can expire
|
|
60
|
+
Signed Global IDs can expire sometime in the future. This is useful if there's a resource
|
|
61
61
|
people shouldn't have indefinite access to, like a share link.
|
|
62
62
|
|
|
63
63
|
```ruby
|
|
@@ -73,8 +73,8 @@ GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
|
|
|
73
73
|
# => nil
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
**In Rails, an auto-expiry of 1 month is set by default.** You can alter that
|
|
77
|
-
in an initializer with:
|
|
76
|
+
**In Rails, an auto-expiry of 1 month is set by default.** You can alter that
|
|
77
|
+
default in an initializer with:
|
|
78
78
|
|
|
79
79
|
```ruby
|
|
80
80
|
# config/initializers/global_id.rb
|
|
@@ -87,7 +87,7 @@ You can assign a default SGID lifetime like so:
|
|
|
87
87
|
SignedGlobalID.expires_in = 1.month
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
This way any generated SGID will use that relative expiry.
|
|
90
|
+
This way, any generated SGID will use that relative expiry.
|
|
91
91
|
|
|
92
92
|
It's worth noting that _expiring SGIDs are not idempotent_ because they encode the current timestamp; repeated calls to `to_sgid` will produce different results. For example, in Rails
|
|
93
93
|
|
|
@@ -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 an `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 those 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,15 +201,37 @@ 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
|
|
189
208
|
```
|
|
190
209
|
|
|
191
|
-
After defining locators as above, URIs like
|
|
210
|
+
After defining locators as above, URIs like `gid://foo/Person/1` and `gid://bar/Person/1` will now use the foo block locator and `BarLocator` respectively.
|
|
192
211
|
Other apps will still keep using the default locator.
|
|
193
212
|
|
|
213
|
+
### Custom Default Locator
|
|
214
|
+
|
|
215
|
+
A custom default locator can be set for an app by calling `GlobalID::Locator.default_locator=` and providing a default locator to use for that app.
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
class MyCustomLocator < UnscopedLocator
|
|
219
|
+
def locate(gid, options = {})
|
|
220
|
+
ActiveRecord::Base.connected_to(role: :reading) do
|
|
221
|
+
super(gid, options)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def locate_many(gids, options = {})
|
|
226
|
+
ActiveRecord::Base.connected_to(role: :reading) do
|
|
227
|
+
super(gids, options)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
GlobalID::Locator.default_locator = MyCustomLocator.new
|
|
233
|
+
```
|
|
234
|
+
|
|
194
235
|
## Contributing to GlobalID
|
|
195
236
|
|
|
196
237
|
GlobalID is work of many contributors. You're encouraged to submit pull requests, propose
|
data/lib/global_id/global_id.rb
CHANGED
|
@@ -32,6 +32,10 @@ class GlobalID
|
|
|
32
32
|
@app = URI::GID.validate_app(app)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def default_locator(default_locator)
|
|
36
|
+
Locator.default_locator = default_locator
|
|
37
|
+
end
|
|
38
|
+
|
|
35
39
|
private
|
|
36
40
|
def parse_encoded_gid(gid, options)
|
|
37
41
|
new(Base64.urlsafe_decode64(gid), options) rescue nil
|
|
@@ -50,12 +54,13 @@ class GlobalID
|
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def model_class
|
|
53
|
-
|
|
57
|
+
@model_class ||= begin
|
|
58
|
+
model = model_name.constantize
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
if model <= GlobalID
|
|
61
|
+
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
|
|
62
|
+
end
|
|
56
63
|
model
|
|
57
|
-
else
|
|
58
|
-
raise ArgumentError, "GlobalID and SignedGlobalID cannot be used as model_class."
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
|
|
@@ -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.model_class # => Person
|
|
35
|
+
# global_id.model_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.model_class # => Person
|
|
56
|
+
# signed_global_id.model_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
|
@@ -2,18 +2,36 @@ require 'active_support/core_ext/enumerable' # For Enumerable#index_by
|
|
|
2
2
|
|
|
3
3
|
class GlobalID
|
|
4
4
|
module Locator
|
|
5
|
+
class InvalidModelIdError < StandardError; end
|
|
6
|
+
|
|
5
7
|
class << self
|
|
8
|
+
# The default locator used when no app-specific locator is found.
|
|
9
|
+
attr_accessor :default_locator
|
|
10
|
+
|
|
6
11
|
# Takes either a GlobalID or a string that can be turned into a GlobalID
|
|
7
12
|
#
|
|
8
13
|
# Options:
|
|
14
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them.
|
|
15
|
+
# The same structure you would pass into a +includes+ method of Active Record.
|
|
16
|
+
# If present, locate will load all the relationships specified here.
|
|
17
|
+
# See https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations.
|
|
9
18
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
|
10
19
|
# allowed to be located. Passing one or more classes limits instances of returned
|
|
11
20
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
|
12
21
|
# instances of returned classes to those including that module. If no classes or
|
|
13
22
|
# modules match, +nil+ is returned.
|
|
14
23
|
def locate(gid, options = {})
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
gid = GlobalID.parse(gid)
|
|
25
|
+
|
|
26
|
+
return unless gid && find_allowed?(gid.model_class, options[:only])
|
|
27
|
+
|
|
28
|
+
locator = locator_for(gid)
|
|
29
|
+
|
|
30
|
+
if locator.method(:locate).arity == 1
|
|
31
|
+
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 = {})`."
|
|
32
|
+
locator.locate(gid)
|
|
33
|
+
else
|
|
34
|
+
locator.locate(gid, options.except(:only))
|
|
17
35
|
end
|
|
18
36
|
end
|
|
19
37
|
|
|
@@ -28,6 +46,11 @@ class GlobalID
|
|
|
28
46
|
# per model class, but still interpolate the results to match the order in which the gids were passed.
|
|
29
47
|
#
|
|
30
48
|
# Options:
|
|
49
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
|
50
|
+
# The same structure you would pass into a includes method of Active Record.
|
|
51
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
|
52
|
+
# If present, locate_many will load all the relationships specified here.
|
|
53
|
+
# Note: It only works if all the gids models have that relationships.
|
|
31
54
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
|
32
55
|
# allowed to be located. Passing one or more classes limits instances of returned
|
|
33
56
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
|
@@ -49,6 +72,10 @@ class GlobalID
|
|
|
49
72
|
# Takes either a SignedGlobalID or a string that can be turned into a SignedGlobalID
|
|
50
73
|
#
|
|
51
74
|
# Options:
|
|
75
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
|
76
|
+
# The same structure you would pass into a includes method of Active Record.
|
|
77
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
|
78
|
+
# If present, locate_signed will load all the relationships specified here.
|
|
52
79
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
|
53
80
|
# allowed to be located. Passing one or more classes limits instances of returned
|
|
54
81
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
|
@@ -66,6 +93,11 @@ class GlobalID
|
|
|
66
93
|
# the results to match the order in which the gids were passed.
|
|
67
94
|
#
|
|
68
95
|
# Options:
|
|
96
|
+
# * <tt>:includes</tt> - A Symbol, Array, Hash or combination of them
|
|
97
|
+
# The same structure you would pass into a includes method of Active Record.
|
|
98
|
+
# @see https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
|
|
99
|
+
# If present, locate_many_signed will load all the relationships specified here.
|
|
100
|
+
# Note: It only works if all the gids models have that relationships.
|
|
69
101
|
# * <tt>:only</tt> - A class, module or Array of classes and/or modules that are
|
|
70
102
|
# allowed to be located. Passing one or more classes limits instances of returned
|
|
71
103
|
# classes to those classes or their subclasses. Passing one or more modules in limits
|
|
@@ -82,7 +114,7 @@ class GlobalID
|
|
|
82
114
|
#
|
|
83
115
|
# Using a block:
|
|
84
116
|
#
|
|
85
|
-
# GlobalID::Locator.use :foo do |gid|
|
|
117
|
+
# GlobalID::Locator.use :foo do |gid, options|
|
|
86
118
|
# FooRemote.const_get(gid.model_name).find(gid.model_id)
|
|
87
119
|
# end
|
|
88
120
|
#
|
|
@@ -91,7 +123,7 @@ class GlobalID
|
|
|
91
123
|
# GlobalID::Locator.use :bar, BarLocator.new
|
|
92
124
|
#
|
|
93
125
|
# class BarLocator
|
|
94
|
-
# def locate(gid)
|
|
126
|
+
# def locate(gid, options = {})
|
|
95
127
|
# @search_client.search name: gid.model_name, id: gid.model_id
|
|
96
128
|
# end
|
|
97
129
|
# end
|
|
@@ -105,7 +137,7 @@ class GlobalID
|
|
|
105
137
|
|
|
106
138
|
private
|
|
107
139
|
def locator_for(gid)
|
|
108
|
-
@locators.fetch(normalize_app(gid.app)) {
|
|
140
|
+
@locators.fetch(normalize_app(gid.app)) { default_locator }
|
|
109
141
|
end
|
|
110
142
|
|
|
111
143
|
def find_allowed?(model_class, only = nil)
|
|
@@ -125,32 +157,59 @@ class GlobalID
|
|
|
125
157
|
@locators = {}
|
|
126
158
|
|
|
127
159
|
class BaseLocator
|
|
128
|
-
def locate(gid)
|
|
129
|
-
|
|
160
|
+
def locate(gid, options = {})
|
|
161
|
+
return unless model_id_is_valid?(gid)
|
|
162
|
+
model_class = gid.model_class
|
|
163
|
+
model_class = model_class.includes(options[:includes]) if options[:includes]
|
|
164
|
+
|
|
165
|
+
model_class.find gid.model_id
|
|
130
166
|
end
|
|
131
167
|
|
|
132
168
|
def locate_many(gids, options = {})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
ids_by_model = Hash.new { |hash, key| hash[key] = [] }
|
|
170
|
+
|
|
171
|
+
gids.each do |gid|
|
|
172
|
+
next unless model_id_is_valid?(gid)
|
|
173
|
+
ids_by_model[gid.model_class] << gid.model_id
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
records_by_model_name_and_id = {}
|
|
177
|
+
|
|
178
|
+
ids_by_model.each do |model, ids|
|
|
179
|
+
records = find_records(model, ids, ignore_missing: options[:ignore_missing], includes: options[:includes])
|
|
138
180
|
|
|
139
|
-
|
|
181
|
+
records_by_id = records.index_by do |record|
|
|
182
|
+
record.id.is_a?(Array) ? record.id.map(&:to_s) : record.id.to_s
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
records_by_model_name_and_id[model.name] = records_by_id
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
gids.filter_map { |gid| records_by_model_name_and_id[gid.model_name][gid.model_id] }
|
|
140
189
|
end
|
|
141
190
|
|
|
142
191
|
private
|
|
143
192
|
def find_records(model_class, ids, options)
|
|
193
|
+
model_class = model_class.includes(options[:includes]) if options[:includes]
|
|
194
|
+
|
|
144
195
|
if options[:ignore_missing]
|
|
145
|
-
model_class.where(
|
|
196
|
+
model_class.where(primary_key(model_class) => ids)
|
|
146
197
|
else
|
|
147
198
|
model_class.find(ids)
|
|
148
199
|
end
|
|
149
200
|
end
|
|
201
|
+
|
|
202
|
+
def model_id_is_valid?(gid)
|
|
203
|
+
Array(gid.model_id).size == Array(primary_key(gid.model_class)).size
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def primary_key(model_class)
|
|
207
|
+
model_class.respond_to?(:primary_key) ? model_class.primary_key : :id
|
|
208
|
+
end
|
|
150
209
|
end
|
|
151
210
|
|
|
152
211
|
class UnscopedLocator < BaseLocator
|
|
153
|
-
def locate(gid)
|
|
212
|
+
def locate(gid, options = {})
|
|
154
213
|
unscoped(gid.model_class) { super }
|
|
155
214
|
end
|
|
156
215
|
|
|
@@ -167,19 +226,20 @@ class GlobalID
|
|
|
167
226
|
end
|
|
168
227
|
end
|
|
169
228
|
end
|
|
170
|
-
|
|
229
|
+
|
|
230
|
+
self.default_locator = UnscopedLocator.new
|
|
171
231
|
|
|
172
232
|
class BlockLocator
|
|
173
233
|
def initialize(block)
|
|
174
234
|
@locator = block
|
|
175
235
|
end
|
|
176
236
|
|
|
177
|
-
def locate(gid)
|
|
178
|
-
@locator.call(gid)
|
|
237
|
+
def locate(gid, options = {})
|
|
238
|
+
@locator.call(gid, options)
|
|
179
239
|
end
|
|
180
240
|
|
|
181
241
|
def locate_many(gids, options = {})
|
|
182
|
-
gids.map { |gid| locate(gid) }
|
|
242
|
+
gids.map { |gid| locate(gid, options) }
|
|
183
243
|
end
|
|
184
244
|
end
|
|
185
245
|
end
|
data/lib/global_id/railtie.rb
CHANGED
|
@@ -5,7 +5,7 @@ class SignedGlobalID < GlobalID
|
|
|
5
5
|
class ExpiredMessage < StandardError; end
|
|
6
6
|
|
|
7
7
|
class << self
|
|
8
|
-
attr_accessor :verifier
|
|
8
|
+
attr_accessor :verifier, :expires_in
|
|
9
9
|
|
|
10
10
|
def parse(sgid, options = {})
|
|
11
11
|
super verify(sgid.to_s, options), options
|
|
@@ -19,8 +19,6 @@ class SignedGlobalID < GlobalID
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
attr_accessor :expires_in
|
|
23
|
-
|
|
24
22
|
DEFAULT_PURPOSE = "default"
|
|
25
23
|
|
|
26
24
|
def pick_purpose(options)
|
|
@@ -29,11 +27,22 @@ class SignedGlobalID < GlobalID
|
|
|
29
27
|
|
|
30
28
|
private
|
|
31
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)
|
|
32
41
|
metadata = pick_verifier(options).verify(sgid)
|
|
33
42
|
|
|
34
43
|
raise_if_expired(metadata['expires_at'])
|
|
35
44
|
|
|
36
|
-
metadata['gid'] if pick_purpose(options) == metadata['purpose']
|
|
45
|
+
metadata['gid'] if pick_purpose(options)&.to_s == metadata['purpose']&.to_s
|
|
37
46
|
rescue ActiveSupport::MessageVerifier::InvalidSignature, ExpiredMessage
|
|
38
47
|
nil
|
|
39
48
|
end
|
|
@@ -55,25 +64,19 @@ class SignedGlobalID < GlobalID
|
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
def to_s
|
|
58
|
-
@sgid ||= @verifier.generate(
|
|
67
|
+
@sgid ||= @verifier.generate(@uri.to_s, purpose: purpose, expires_at: expires_at)
|
|
59
68
|
end
|
|
60
69
|
alias to_param to_s
|
|
61
70
|
|
|
62
|
-
def to_h
|
|
63
|
-
# Some serializers decodes symbol keys to symbols, others to strings.
|
|
64
|
-
# Using string keys remedies that.
|
|
65
|
-
{ 'gid' => @uri.to_s, 'purpose' => purpose, 'expires_at' => encoded_expiration }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
71
|
def ==(other)
|
|
69
72
|
super && @purpose == other.purpose
|
|
70
73
|
end
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
75
|
+
def inspect # :nodoc:
|
|
76
|
+
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
|
|
77
|
+
end
|
|
76
78
|
|
|
79
|
+
private
|
|
77
80
|
def pick_expiration(options)
|
|
78
81
|
return options[:expires_at] if options.key?(:expires_at)
|
|
79
82
|
|
data/lib/global_id/uri/gid.rb
CHANGED
|
@@ -30,6 +30,13 @@ 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 = "/"
|
|
38
|
+
|
|
39
|
+
URI_PARSER = URI::RFC2396_Parser.new # :nodoc:
|
|
33
40
|
|
|
34
41
|
class << self
|
|
35
42
|
# Validates +app+'s as URI hostnames containing only alphanumeric characters
|
|
@@ -57,7 +64,7 @@ module URI
|
|
|
57
64
|
# URI.parse('gid://bcx') # => URI::GID instance
|
|
58
65
|
# URI::GID.parse('gid://bcx/') # => raises URI::InvalidComponentError
|
|
59
66
|
def parse(uri)
|
|
60
|
-
generic_components = URI.split(uri) <<
|
|
67
|
+
generic_components = URI.split(uri) << URI_PARSER << true # RFC2396 parser, true arg_check
|
|
61
68
|
new(*generic_components)
|
|
62
69
|
end
|
|
63
70
|
|
|
@@ -83,7 +90,8 @@ module URI
|
|
|
83
90
|
def build(args)
|
|
84
91
|
parts = Util.make_components_hash(self, args)
|
|
85
92
|
parts[:host] = parts[:app]
|
|
86
|
-
|
|
93
|
+
model_id_segment = Array(parts[:model_id]).map { |p| CGI.escape(p.to_s) }.join(COMPOSITE_MODEL_ID_DELIMITER)
|
|
94
|
+
parts[:path] = "/#{parts[:model_name]}/#{model_id_segment}"
|
|
87
95
|
|
|
88
96
|
if parts[:params] && !parts[:params].empty?
|
|
89
97
|
parts[:query] = URI.encode_www_form(parts[:params])
|
|
@@ -147,12 +155,22 @@ module URI
|
|
|
147
155
|
|
|
148
156
|
def set_model_components(path, validate = false)
|
|
149
157
|
_, model_name, model_id = path.split('/', 3)
|
|
150
|
-
validate_component(model_name) && validate_model_id(model_id, model_name) if validate
|
|
151
|
-
|
|
152
|
-
model_id = CGI.unescape(model_id) if model_id
|
|
153
158
|
|
|
159
|
+
validate_component(model_name) && validate_model_id_section(model_id, model_name) if validate
|
|
154
160
|
@model_name = model_name
|
|
155
|
-
|
|
161
|
+
|
|
162
|
+
if model_id
|
|
163
|
+
model_id_parts = model_id
|
|
164
|
+
.split(COMPOSITE_MODEL_ID_DELIMITER, COMPOSITE_MODEL_ID_MAX_SIZE)
|
|
165
|
+
.reject(&:blank?)
|
|
166
|
+
|
|
167
|
+
model_id_parts.map! do |id|
|
|
168
|
+
validate_model_id(id)
|
|
169
|
+
CGI.unescape(id)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
@model_id = model_id_parts.length == 1 ? model_id_parts.first : model_id_parts
|
|
173
|
+
end
|
|
156
174
|
end
|
|
157
175
|
|
|
158
176
|
def validate_component(component)
|
|
@@ -162,13 +180,20 @@ module URI
|
|
|
162
180
|
"Expected a URI like gid://app/Person/1234: #{inspect}"
|
|
163
181
|
end
|
|
164
182
|
|
|
165
|
-
def
|
|
166
|
-
return model_id unless model_id.blank?
|
|
183
|
+
def validate_model_id_section(model_id, model_name)
|
|
184
|
+
return model_id unless model_id.blank?
|
|
167
185
|
|
|
168
186
|
raise MissingModelIdError, "Unable to create a Global ID for " \
|
|
169
187
|
"#{model_name} without a model id."
|
|
170
188
|
end
|
|
171
189
|
|
|
190
|
+
def validate_model_id(model_id_part)
|
|
191
|
+
return unless model_id_part.include?('/')
|
|
192
|
+
|
|
193
|
+
raise InvalidModelIdError, "Unable to create a Global ID for " \
|
|
194
|
+
"#{model_name} with a malformed model id."
|
|
195
|
+
end
|
|
196
|
+
|
|
172
197
|
def parse_query_params(query)
|
|
173
198
|
Hash[URI.decode_www_form(query)].with_indifferent_access if query
|
|
174
199
|
end
|
data/lib/global_id/verifier.rb
CHANGED
|
@@ -3,11 +3,11 @@ require 'active_support/message_verifier'
|
|
|
3
3
|
class GlobalID
|
|
4
4
|
class Verifier < ActiveSupport::MessageVerifier
|
|
5
5
|
private
|
|
6
|
-
def encode(data)
|
|
6
|
+
def encode(data, **)
|
|
7
7
|
::Base64.urlsafe_encode64(data)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def decode(data)
|
|
10
|
+
def decode(data, **)
|
|
11
11
|
::Base64.urlsafe_decode64(data)
|
|
12
12
|
end
|
|
13
13
|
end
|
data/lib/global_id.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: globalid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Heinemeier Hansson
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activesupport
|
|
@@ -16,14 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '6.1'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '6.1'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: rake
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -60,8 +59,11 @@ homepage: http://www.rubyonrails.org
|
|
|
60
59
|
licenses:
|
|
61
60
|
- MIT
|
|
62
61
|
metadata:
|
|
62
|
+
bug_tracker_uri: https://github.com/rails/globalid/issues
|
|
63
|
+
changelog_uri: https://github.com/rails/globalid/releases/tag/v1.3.0
|
|
64
|
+
mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
|
|
65
|
+
source_code_uri: https://github.com/rails/globalid/tree/v1.3.0
|
|
63
66
|
rubygems_mfa_required: 'true'
|
|
64
|
-
post_install_message:
|
|
65
67
|
rdoc_options: []
|
|
66
68
|
require_paths:
|
|
67
69
|
- lib
|
|
@@ -69,15 +71,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
69
71
|
requirements:
|
|
70
72
|
- - ">="
|
|
71
73
|
- !ruby/object:Gem::Version
|
|
72
|
-
version: 2.
|
|
74
|
+
version: 2.7.0
|
|
73
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
76
|
requirements:
|
|
75
77
|
- - ">="
|
|
76
78
|
- !ruby/object:Gem::Version
|
|
77
79
|
version: '0'
|
|
78
80
|
requirements: []
|
|
79
|
-
rubygems_version: 3.
|
|
80
|
-
signing_key:
|
|
81
|
+
rubygems_version: 3.6.7
|
|
81
82
|
specification_version: 4
|
|
82
83
|
summary: 'Refer to any model with a URI: gid://app/class/id'
|
|
83
84
|
test_files: []
|