Coder Social home page Coder Social logo

globalid's Introduction

Global ID - Reference models by URI

A Global ID is an app wide URI that uniquely identifies a model instance:

gid://YourApp/Some::Model/id

This is helpful when you need a single identifier to reference different classes of objects.

One example is job scheduling. We need to reference a model object rather than serialize the object itself. We can pass a Global ID that can be used to locate the model when it's time to perform the job. The job scheduler doesn't need to know the details of model naming and IDs, just that it has a global identifier that references a model.

Another example is a drop-down list of options, consisting of both Users and Groups. Normally we'd need to come up with our own ad hoc scheme to reference them. With Global IDs, we have a universal identifier that works for objects of both classes.

Usage

Mix GlobalID::Identification into any model with a #find(id) class method. Support is automatically included in Active Record.

person_gid = Person.find(1).to_global_id
# => #<GlobalID ...

person_gid.uri
# => #<URI ...

person_gid.to_s
# => "gid://app/Person/1"

GlobalID::Locator.locate person_gid
# => #<Person:0x007fae94bf6298 @id="1">

Signed Global IDs

For added security GlobalIDs can also be signed to ensure that the data hasn't been tampered with.

person_sgid = Person.find(1).to_signed_global_id
# => #<SignedGlobalID:0x007fea1944b410>

person_sgid = Person.find(1).to_sgid
# => #<SignedGlobalID:0x007fea1944b410>

person_sgid.to_s
# => "BAhJIh5naWQ6Ly9pZGluYWlkaS9Vc2VyLzM5NTk5BjoGRVQ=--81d7358dd5ee2ca33189bb404592df5e8d11420e"

GlobalID::Locator.locate_signed person_sgid
# => #<Person:0x007fae94bf6298 @id="1">

Expiration

Signed Global IDs can expire some time in the future. This is useful if there's a resource people shouldn't have indefinite access to, like a share link.

expiring_sgid = Document.find(5).to_sgid(expires_in: 2.hours, for: 'sharing')
# => #<SignedGlobalID:0x008fde45df8937 ...>

# Within 2 hours...
GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
# => #<Document:0x007fae94bf6298 @id="5">

# More than 2 hours later...
GlobalID::Locator.locate_signed(expiring_sgid.to_s, for: 'sharing')
# => nil

In Rails, an auto-expiry of 1 month is set by default. You can alter that deal in an initializer with:

# config/initializers/global_id.rb
Rails.application.config.global_id.expires_in = 3.months

You can assign a default SGID lifetime like so:

SignedGlobalID.expires_in = 1.month

This way any generated SGID will use that relative expiry.

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

Document.find(5).to_sgid.to_s == Document.find(5).to_sgid.to_s
# => false

You need to explicitly pass expires_in: nil to generate a permanent SGID that will not expire,

# Passing a false value to either expiry option turns off expiration entirely.
never_expiring_sgid = Document.find(5).to_sgid(expires_in: nil)
# => #<SignedGlobalID:0x008fde45df8937 ...>

# Any time later...
GlobalID::Locator.locate_signed never_expiring_sgid
# => #<Document:0x007fae94bf6298 @id="5">

It's also possible to pass a specific expiry time

explicit_expiring_sgid = SecretAgentMessage.find(5).to_sgid(expires_at: Time.now.advance(hours: 1))
# => #<SignedGlobalID:0x008fde45df8937 ...>

# 1 hour later...
GlobalID::Locator.locate_signed explicit_expiring_sgid.to_s
# => nil

Note that an explicit :expires_at takes precedence over a relative :expires_in.

Purpose

You can even bump the security up some more by explaining what purpose a Signed Global ID is for. In this way evildoers can't reuse a sign-up form's SGID on the login page. For example.

signup_person_sgid = Person.find(1).to_sgid(for: 'signup_form')
# => #<SignedGlobalID:0x007fea1984b520

GlobalID::Locator.locate_signed(signup_person_sgid.to_s, for: 'signup_form')
# => #<Person:0x007fae94bf6298 @id="1">

Locating many Global IDs

When needing to locate many Global IDs use GlobalID::Locator.locate_many or GlobalID::Locator.locate_many_signed for Signed Global IDs to allow loading Global IDs more efficiently.

For instance, the default locator passes every model_id per model_name thus using model_name.where(id: model_ids) versus GlobalID::Locator.locate's model_name.find(id).

In the case of looking up Global IDs from a database, it's only necessary to query once per model_name as shown here:

gids = users.concat(people).sort_by(&:id).map(&:to_global_id)
# => [#<GlobalID:0x00007ffd6a8411a0 @uri=#<URI::GID gid://app/User/1>>,
#<GlobalID:0x00007ffd675d32b8 @uri=#<URI::GID gid://app/Student/1>>,
#<GlobalID:0x00007ffd6a840b10 @uri=#<URI::GID gid://app/User/2>>,
#<GlobalID:0x00007ffd675d2c28 @uri=#<URI::GID gid://app/Student/2>>,
#<GlobalID:0x00007ffd6a840480 @uri=#<URI::GID gid://app/User/3>>,
#<GlobalID:0x00007ffd675d2598 @uri=#<URI::GID gid://app/Student/3>>]

GlobalID::Locator.locate_many gids
# SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]
# SELECT "students".* FROM "students" WHERE "students"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]
# => [#<User id: 1>, #<Student id: 1>, #<User id: 2>, #<Student id: 2>, #<User id: 3>, #<Student id: 3>]

Note the order is maintained in the returned results.

Options

Either GlobalID::Locator.locate or GlobalID::Locator.locate_many supports a hash of options as second parameter. The supported options are:

  • :includes - A Symbol, Array, Hash or combination of them The same structure you would pass into a includes method of Active Record. See Active Record eager loading associations If present, locate or locate_many will eager load all the relationships specified here. Note: It only works if all the gids models have that relationships.
  • :only - A class, module or Array of classes and/or modules that are allowed to be located. Passing one or more classes limits instances of returned classes to those classes or their subclasses. Passing one or more modules in limits instances of returned classes to those including that module. If no classes or modules match, +nil+ is returned.
  • :ignore_missing (Only for locate_many) - By default, locate_many will call #find on the model to locate the ids extracted from the GIDs. In Active Record (and other data stores following the same pattern), #find will raise an exception if a named ID can't be found. When you set this option to true, we will use #where(id: ids) instead, which does not raise on missing records.

Custom App Locator

A custom locator can be set for an app by calling GlobalID::Locator.use and providing an app locator to use for that app. A custom app locator is useful when different apps collaborate and reference each others' Global IDs. When finding a Global ID's model, the locator to use is based on the app name provided in the Global ID url.

A custom locator can either be a block or a class.

Using a block:

GlobalID::Locator.use :foo do |gid, options|
  FooRemote.const_get(gid.model_name).find(gid.model_id)
end

Using a class:

GlobalID::Locator.use :bar, BarLocator.new
class BarLocator
  def locate(gid, options = {})
    @search_client.search name: gid.model_name, id: gid.model_id
  end
end

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. Other apps will still keep using the default locator.

Contributing to GlobalID

GlobalID is work of many contributors. You're encouraged to submit pull requests, propose features and discuss issues.

See CONTRIBUTING.

License

GlobalID is released under the MIT License.

globalid's People

Contributors

adrianna-chang-shopify avatar alexcwatt avatar amatsuda avatar andyatkinson avatar assain avatar bradleybuda avatar byroot avatar danolson avatar dependabot[bot] avatar dhh avatar eugeneius avatar georgeclaghorn avatar ghiculescu avatar guilleiguaran avatar ideasasylum avatar jeremy avatar junaruga avatar kaspth avatar larrylv avatar nvasilevski avatar p8 avatar rafacoello avatar rafaelfranca avatar seuros avatar spastorino avatar tenderlove avatar thomasfedb avatar tony612 avatar vipulnsward avatar y-yagi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

globalid's Issues

Test failed with rails 5.2

Thanks for maintaining globalid.

I am trying to making rails 5.2 available in Debian. During our tests we found this gem fail on tests with rails 5.2:

Failure:
RailtieTest#test_SignedGlobalID.verifier_defaults_to_nil_when_secret_token_is_not_present [/home/travis/build/alee-ccu/globalid/test/cases/railtie_test.rb:41]:
Expected #<GlobalID::Verifier:0x00007f7558521b40 @secret="\xE9\xCD\xAB\xCE\xFF\x93v\x14\xF1\a\xE7/\e\xB2<\xE0\xE3\xA5~\r\xC7\xB5\xEB\xC9`PFx\f\x13\x04vs\x95\xDB\xB1`U\x8FM^(\x7FQ\x7F\xED\xE7U;\xF8r\xB7\x87\xF6\xC8;o\xE7\xBB\xCC\x8E\xFB\xE6\x94", @digest="SHA1", @serializer=Marshal, @options={}, @rotations=[]> to be nil.

I also reproduced the failed tests on travis-ci.org with both version 0.4.1 and master branch, please see the full log at:
https://travis-ci.org/alee-ccu/globalid/jobs/464195985
https://travis-ci.org/alee-ccu/globalid/jobs/464202126

Stale data in ActiveJob

I have an ActiveJob created in the after_save callback. The job gets the instance as a parameter. Every now and then when the job executes the instance is stale - it contains the data from before the model update. I have observer in the logs that the instance is not being read from the database - it must be coming from cache. Debugging the updated_at column showed that the value in the Job is earlier than the the one saved in the database.

after_save do |instance|
    SaveAttachmentsJob.perform_later(instance)
end

class SaveAttachmentsJob < ActiveJob::Base
  queue_as :default

  def perform(instance)
    # This fixes the issue.
    # instance.reload 

    # Every now and then this will show stale object.
    Rails.logger.info(instance.inspect)

    # ... some more code.
  end
end

I have worked around the problem calling instance.reload in the Job.

globalid 0.3.5, active job 4.2.3, I am using Sidekiq to process the Jobs.

What are some potential ways to misuse signed Global IDs?

Some of you might be familiar with the "No Way, JOSE! Javascript Object Signing and Encryption is a Bad Standard That Everyone Should Avoid" post.

Recently at work we began sending a callback URL to a 3rd party. To verify that it's indeed the 3rd party that's calling back the endpoint, someone suggested putting a JSON web token in the payload. I suggested using signed Global IDs, since they're accessible OOTB in Rails and also, in my impression, are somewhat more secure than JWT.

However, in the context of that post, I was thinking if there are ways in which signed Global IDs might be misused. I'm not a security expert, so it's hard for me to grasp if this library can be used in unsecure ways.

I suppose it wouldn't be that great for storing sessions for the same reasons as JWT. If there's a need to invalidate certain signed IDs, globalid doesn't handle this out of the box, so that also might not be that great of a pick.

Anything else that comes to your mind?

Bug or breaking change? Model class must now implement `.primary_key`

I noticed a recent change now requires any class that has include GlobalID::Identification must now also respond to .primary_key. I think it came from #163

I'm not sure if GlobalId is intended to be used on only Active Record / Active Model, but that's what I've been doing. The readme says:

Mix GlobalID::Identification into any model with a #find(id) class method

If model in this sense means generically "a domain model" than I think this is a bug or breaking change. And if not, the readme needs to be updated to say both .find and .primary_key.

Expose Railtie configuration to apps

Let apps set the gid app portion of the URL:

config.global_id.app = 'myapp'

This defaults to Rails.application.railtie_name.remove('_application') currently, so a MyBlog::Application app gets gids like gid://MyBlog/Post/1234

Let apps provide a signed message verifier:

config.global_id.verifier = custom_verifier

Manually specifying GID classes

Sometimes might want to explicitly declare which class to use for a given class, instead of letting GID infer it.

For example:

class Post
  def self.gid_class
    Post.name
  end
end

class Post::Foo < Post
end

class Post::Bar < Post
end

and 

module URI
  class GID < Generic

    class << self

      def create(app, model, params = nil)
        model_name = model.try(:gid_class) || model.class.name
        build app: app, model_name: model_name, model_id: model.id, params: params
      end

    end
  end
end

The main usage case is GID + ActiveJob. For example, if update a type of record, all old Jobs will be failed, because they will not be able to find a record by old URL.

So, any ideas how to resolve it, or better to implement something like this?
PS: Solution by https://github.com/elabs/pundit/tree/v1.0.1#manually-specifying-policy-classes

NoMethodError: undefined method `unscoped' for Class

locate_many does not work properly when different objects are passed.

GlobalID::Locator.locate_many ['gid://app/User/3', 'gid://app/Custom::Item/74592']

The issue User is active_record model and responds to :unscoped, Custom::Item is custom model and does not support :unscoped method.

As a result the above code returns
NoMethodError: undefined method 'unscoped' for Custom::Item:Class

but works if you swap the order of objects.

Related
https://github.com/rails/globalid/pull/73/files

Support for Sequel?

Sequel doesn’t follow the same protocol as ActiveRecord, even when ActiveModel compliant (or so it seems). I’ve come up with what looks like the right level of monkey-patching for this, and can turn this into a real patch (with tests) for GlobalID if there is interest. The changes are only necessary in GlobalID::Locator::BaseLocator for #locate and #find_records.

The only other change is including GlobalID::Identification into Sequel::Model, but in my basic testing, these changes create the same result as AR-backed GlobalID, mod the appropriate exceptions.

Also opened as TalentBox/sequel-rails#111.

module SequelBaseLocator
  def locate(gid)
    if defined?(::Sequel::Model) && gid.model_class < Sequel::Model
      gid.model_class.with_pk!(gid.model_id)
    else
      super
    end
  end

  private

  def find_records(model_class, ids, options)
    if defined?(::Sequel::Model) && model_class < Sequel::Model
      model_class.where(model_class.primary_key => ids).tap do |result|
        if !options[:ignore_missing] && result.count < ids.size
          fail Sequel::NoMatchingRow
        end
      end.all
    else
      super
    end
  end
end

GlobalID::Locator::BaseLocator.prepend SequelBaseLocator
Sequel::Model.send(:include, ::GlobalID::Identification)

Signing... with an expiration date

Once we can sign with purpose, we'll also want to be explicit about how long the signed Global ID is valid. It needs an expiration date!

See the work in progress on expiration @ rails/rails#16462 - they can use some help on this as well ❤️

We'll want to be able to pass :expires_in or :expires_at when we create signed Global IDs. When we parse a sgid, we'll rely on the MessageVerifier to raise when it's past the expiration date. We'll have to rescue that error and return nil.

Furthermore, we'll want expiration by default, so we'll never inadvertently send out forever-valid signed Global IDs. So, SignedGlobalID.expires_in = 1.month for example, and expose config.global_id.expires_in = ... to the Railtie. Allow passing expires_in: nil to override and use no expiry.

Consider adding a request-specific expires_at

This way signed global IDs can be compared even if generated in different places (we get this issue with html select elements, and with ids generated across the app).

Example code to add to ApplicationController:

  before_action :set_sgid_expiry

  # Set a default expiry for SGIDs which means that any ids generated within this request will have the same value
  # and can thus be matched on the frontend
  def set_sgid_expiry
    SignedGlobalID.expires_at 1.week.from_now
  end

Would this be something viable?

readme: mention to_param / GlobalID::Locator.locate as a feature

It'd be nice for the readme to say a bit about to_param and GlobalID::Locator.locate working together, as it enables this:

view: link_to x, somepath(content_gid: content.to_global_id)
controller: content = GlobalID::Locator.locate params.fetch(:content_gid)

It looks like some thought has gone into this, but it's not currently advertised as a feature.
Would a readme PR be welcome for this or is it an internals thing we shouldn't be relying on?

Improve Error When Converting Records without an Id

Model.new.to_global_id currently raises a URI::InvalidComponentError with the message URI::InvalidComponentError: Expected a URI like gid://app/Person/1234: #<URI::GID gid://app>. An error particular to GlobalID would be useful:

GlobalID::UnidentifiableRecord Unable to identify #{record.class} without an id. (Maybe you forgot to call save?)

This is already being discussed in rails/rails#19861 and rails/rails#19877.

Typo in README.md

"With Global IDs, we have a universal identifier that works for objects both classes."

Unsure the implied meaning (objects of both classes?), so haven't updated it myself.

Test failures

RailtieTest#test_GlobalID.app_can_be_set_with_config.global_id.app_= = 0.43 s = E


Error:
RailtieTest#test_GlobalID.app_can_be_set_with_config.global_id.app_=:
Errno::ENOENT: No such file or directory @ rb_sysopen - /<<PKGBUILDDIR>>/tmp/development_secret.txt
    test/cases/railtie_test.rb:27:in `block in <class:RailtieTest>'

bin/rails test test/cases/railtie_test.rb:25


RailtieTest#test_GlobalID.app_for_Blog::Application_defaults_to_blog = 0.40 s = E


Error:
RailtieTest#test_GlobalID.app_for_Blog::Application_defaults_to_blog:
Errno::ENOENT: No such file or directory @ rb_sysopen - /<<PKGBUILDDIR>>/tmp/development_secret.txt
    test/cases/railtie_test.rb:21:in `block in <class:RailtieTest>'

bin/rails test test/cases/railtie_test.rb:20

RailtieTest#test_config.global_id_can_be_used_to_set_configurations_after_the_railtie_has_been_loaded = 0.44 s = E


Error:
RailtieTest#test_config.global_id_can_be_used_to_set_configurations_after_the_railtie_has_been_loaded:
Errno::ENOENT: No such file or directory @ rb_sysopen - /<<PKGBUILDDIR>>/tmp/development_secret.txt
    test/cases/railtie_test.rb:38:in `block in <class:RailtieTest>'

bin/rails test test/cases/railtie_test.rb:31


Finished in 1.367685s, 97.2446 runs/s, 174.7479 assertions/s.
133 runs, 239 assertions, 0 failures, 3 errors, 0 skips

I think there's no directory, perhaps we could add the secret token?

suggestion - class method to create globalid from values

Would like to suggest to add class method like GlobalID.create but where you can pass model_class and model_id directly.

This is helpful when you need to create GlobalID without object, for example in polymorphic associations where you have two columns with class and id (like owner_type and owner_id)

for example

GlobalID.create('User', 4, options)

alternatively currently we have to do

User.new(id: 4).to_global_id

which results in unnecessary object creation

Require an app to be set before creating a GlobalID

The initial URI implementation expects the Railtie to set GlobalID.app = 'name'.

If that's nil, nothing complains currently. GlobalID.create should raise an optional :app keyword argument that defaults to GlobalID.app and raise ArgumentError if app is nil.

[Suggestion] Allow different primary_ids such as UUID

I have a similar concept that I use in some apps when syncing data. I call it a source_token
The format is almost identical, except that it uses UUID instead of ID. Would there be interest in allowing an optional parameter which would serialize the uuid instead?
I'm imagining something along these lines:

User.find_by(uuid: "abc-123-uuid").to_global_id(primary_key: :uuid).to_s
=> "gid://myapp/User/uuid/abc-123-uuid"

The presence of a value between the class and id would indicate a different method of lookup. In order to maintain backwards compatibility, it would assume id if no other value is specified.

I'm happy to take a stab at implementing but I would love any insight around the acceptance of such a feature.

Passing non gid:// URI will try a useless call to base64 decode

So If you give the locator "http://google.com": it will reach extract_uri_components which will raise URI::BadURIError, "Not a gid:// URI scheme: #{@uri.inspect}" unless @uri.scheme == 'gid' but #parse rescues that (because URI::BadURIError is a subclass of URI::Error) and tries to base64 decode.
We should rescue URI::BadURIError in #parse and return nil

Error while trying to deserialize arguments: uninitialized constant GlobalID::Locator

From @jwoertink on April 19, 2017 19:16

I don't get this error often, but sometimes I get

Error while trying to deserialize arguments: uninitialized constant GlobalID::Locator Did you mean? GlobalID::Locator

I'm not able to recreate this error locally, and out of 310 processed jobs, it's only shown up twice.

I'm using Sidekiq with ActiveJob on Rails 5.0.2 with ruby 2.4.1.

Here's the data from Sidekiq

Job
Queue 	default
Job 	{"job_class"=>"EncoderJob", "job_id"=>"2c6589f6-172e-4e83-9910-c4ffc29bb5d0", "queue_name"=>"default", "priority"=>nil, "arguments"=>["update", "poster", 54381, ["hq_gallery_zip_url", "promo_photo_pack_zip_url", "updated_at"]], "locale"=>"en"}
Arguments 	
JID 	6e64e144c2eea7c09d418b53
Enqueued 	2 days ago
Extras 	{"wrapped"=>"EncoderJob", "processor"=>"d14dcdc7631e:1"}
Error
Error Class 	ActiveJob::DeserializationError
Error Message 	Error while trying to deserialize arguments: uninitialized constant GlobalID::Locator Did you mean? GlobalID::Locator

Copied from original issue: rails/rails#28801

New release please?

Hello. There was a great feature added - hash equality. Can somebody release it, please?

URL-safe Base64 for use in routes and params

Global IDs can be URI-escaped and embedded in an app's routes and query params, but the percent escaping and needless detail makes a mess of the URL.

Let's provide a nice compact #to_param representation for these cases:

Person.find(1).gid.to_param
# => "Z2lkOi8vYXBwL1BlcnNvbi8x"

(That's just def to_param; Base64.strict_encode64 to_s end)

The locator can try to Base64-decode non-URI arguments:

GlobalID::Locator.locate "Z2lkOi8vYXBwL1BlcnNvbi8x"
# => Person id=1

SignedGlobalID: Consider re-adding the ability to use the old serialization format

12f7629 removed the ability to keep using the old format when serializing via SignedGlobalID. This causes the following issues:

  • Temporary errors during rolling deployments: During a rolling deployment, is possible to sign new data using the new format (new codebase) and then trigger an error when decoding the new data via the old codebase.
  • Rollbacks: When the data is encoded with the new format, it needs to be re-encoded to be read using the old format; this complicates rolling back to an old version.

I understand that "Rollbacks" can be considered a "no-issue" since the upgrade is one-way but I'm still afraid of the rolling-deployment issue. Additionally, Version "1.2.1" is the new version pointed by Rails "7.1", which makes rolling back a Rails upgrade more complicated because it forces you to upgrade GlobalID before upgrading Rails in order to not encounter the "unexpected" rollback issue.

#!/usr/bin/env ruby

require "bundler/inline"

globalid_version = ARGV[0]
$sgid = ARGV[1]

# true = install gems so this is fast on repeat invocations
gemfile(true, quiet: true) do
  source "https://rubygems.org"

  gem "globalid", globalid_version
  gem "activerecord", "~> 7.1"
  gem "sqlite3"
end

require "active_record"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")

ActiveRecord::Schema.define do
  create_table "people" do |t|
    t.string "name"
  end
end
class Person < ActiveRecord::Base; end

SignedGlobalID.verifier = ActiveSupport::MessageVerifier.new("secret")
person = Person.create!(name: "John Doe")

if $sgid
  require "minitest/autorun"
  class SignedGlobalIdTest < Minitest::Test
    def test_cant_locate_new_format_sgid_with_old_version
      assert GlobalID::Locator.locate_signed($sgid), "Can't locate by SGID"
    end
  end
else
  puts SignedGlobalID.create(person, app: "test")
end
jacopo-37s-mb 3.3.0-preview2 ~ ./signed_globalid_serializarion_issue.rb 1.2.0 | tail -n 1 | xargs ./signed_globalid_serializarion_issue.rb 1.2.0
-- create_table("people")
   -> 0.0081s
Run options: --seed 25563

# Running:

.

Finished in 0.014660s, 68.2128 runs/s, 68.2128 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
jacopo-37s-mb 3.3.0-preview2 ~ ./signed_globalid_serializarion_issue.rb 1.2.0 | tail -n 1 | xargs ./signed_globalid_serializarion_issue.rb 1.1.0
-- create_table("people")
   -> 0.0093s
Run options: --seed 3475

# Running:

F

Finished in 0.000923s, 1083.4238 runs/s, 1083.4238 assertions/s.

  1) Failure:
SignedGlobalIdTest#test_cant_locate_new_format_sgid_with_old_version [./signed_globalid_serializarion_issue.rb:36]:
Can't locate by SGID

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
jacopo-37s-mb 3.3.0-preview2 ~

Can't use purpose implementation(for:)

Hi,
I am trying to use the purpose as specified in the readme file.
Readme file has the folowing example given:

signup_person_sgid = Person.find(1).to_sgid(for: 'signup_form')
=> #<SignedGlobalID:0x007fea1984b520

I am trying this out in the rails 4.2.0 console. My command line is:

siddharth@siddharth-Inspiron-5537:~/Development/ROR/ror_2/app1$ rails console
Loading development environment (Rails 4.2.0)
irb(main):001:0> user = User.find(1).to_sgid
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
=> #<SignedGlobalID:0x007f4ba64aa2b0 @uri=#<URI::GID gid://app1/User/1>, @Verifier=#<ActiveSupport::MessageVerifier:0x007f4ba5448110 @secret="\xFD7\v<\xCF\x17;Q\x0E=\x8FRzv\xD6\xE7\xAB\x98Gp\xDC\xE9\xD1\r\xC2\x87\xD1\x94 \xD5\xCDN\x04\xEB\xFDBIA\x1F\x19W\xAA\xBC\x85\xD3S>\xEF\xAA\x91\xEC\r\xF2\x0F\x1Dv\x86\xCF\xEB\xED\n\x0EJ\xC1", @digest="SHA1", @Serializer=Marshal>, @purpose="default", @expires_at=Thu, 09 Apr 2015 20:04:00 UTC +00:00>
irb(main):002:0> user = User.find(1).to_sgid(for: 'signup_form')
irb(main):003:1>

User.find(1).to_sgid works well. However, it seems using for: as a parameter is creating a loop.
Also expire and all other things are working fine. I only have problem with the purpose implementation here.
Maybe I am doing this incorrectly. Can someone guide me through this?

expires_in does not work in initializer properly

From documentation ...

In Rails, an auto-expiry of 1 month is set by default. You can alter that deal in an initializer with:

# config/initializers/global_id.rb
Rails.application.config.global_id.expires_in = 3.months

Unfortunately in my case it loads after railtie initializer (https://github.com/rails/globalid/blob/master/lib/global_id/railtie.rb#L16)
As result, it does not work, because SignedGlobalID.expires_in is set with default 1.month

My application details:

Rails 5.1.2

*** LOCAL GEMS ***

globalid (0.4.0)

As a workaround, I put config.global_id.expires_in = 3.months to config/application.rb

Should be fixed or the doc should be updated

Preferred way to check if a Signed GlobalID is expired?

The documentation for Signed Global IDs shows that they can have an expiry date. I know that if the SGID is expired, then trying to use it to locate a record returns nil. But returning nil could also mean that the record indicated by the SGID no longer exists.

Is there a way to specifically tell that the SGID is expired? I see in the code, an ExpiredMessage exception is actually raised, but it is immediately caught and turned into nil.

def verify(sgid, options)
metadata = pick_verifier(options).verify(sgid)
raise_if_expired(metadata['expires_at'])
metadata['gid'] if pick_purpose(options) == metadata['purpose']
rescue ActiveSupport::MessageVerifier::InvalidSignature, ExpiredMessage
nil
end

I could use the SignedGlobalID#verifier to decode the message and then get the expired_at attribute out of the Hash. That seems hacky to me, so I was wondering if there was another way to check for expiration?

Would a PR to add an expired? method to SignedGlobalID be welcome? I'd be happy to work on that.

Thanks!

Spaces in model ids not supported.

I have an external database which has text identifiers, some of which end in a trailing space.

When using global ids with these records, the global ids generated fail to be parsed, and therefore lookup also fails.

Perhaps model ids should be urlencoded?

alias gid group_id

Upgrading an internal app to Rails 4.2 beta, and this alias hit me pretty hard. The app deals with file system permissions and as gid is the numeric group id, it is used heavily in modeling file system users.. Since now all my AR models can't use gid as an field anymore. so lots of renaming columns. Tried aliasing it, but that didn't work (as expected) Not sure if I should post this here or Active Record? Figure since this is where it is defined. I am probably not the only person to be bit by this. No real work around, that I can see..

Single app with multiple databases?

My app switches to a different database based on subdomain using:

  Mongoid.override_database("timeline_#{subdomain}_production")

So when the globalid is generated, it looks something like:

gid://timeline/User/55230a4278616e6fbc010000

This does not help because this ID is meaningless without knowing what the subdomain or the database name is. I am looking for something like:

gid://timeline/database_name/User/55230a4278616e6fbc010000

So that ActiveJob or whatever is responsible for finding the record knows where to actually look for it.

Any ideas/suggestions?

P.S. this is related to this issue: rails/activejob#112

Idea: Locating embedded resources

The code I am providing here is to illustrate an idea, a seed for discussion, not a proposal to adopt it as-is.

Nevertheless, I have put together some handy code that lets me locate resources that are embedded within other resources, for example, embedded within a serialized column, like a postgres jsonb structure. I could make another gem out of this, but some form of this idea seems pretty generally useful. I wonder if the community here has any thoughts on whether this idea is worth iterating on, or importing into the globalid project.

The concept is a regular URI::GID where a "host model" can be located in the normal way, but adds additional params to the URI::GID in order to locate an object that is only locatable within its host. This implementation imposes no requirements on the name or format of those params, only that whatever params exist can be used by the "host model" to locate the required sub-resource. The host model need only implement an instance method, gid_sublocate in this case, and given the gid property, it can do whatever work is needed to find the specified subresource.

In a nutshell, this means resources that are not normally globally addressable can become globally addressable by leaning on the addressability of some host model.

# Allows objects to be located within other objects and referenced through the
# GlobalID gem. In order to participate, the sublocatable's class must
# implement an instance method `to_global_id` and the host class must implement
# `gid_sublocate`
#
# # Returns a sublocated object if one can be found, for example:
# def gid_sublocator(gid)
#   key = gid.params[:some_key]
#   serialized_column[key]
# end
#
# # Returns a GlobalID object which will be passed to gid_sublocator to
# # locate this object later, for example:
# def to_global_id(options = {})
#   GlobalID.new(
#     URI::GID.build(
#       app: GlobalID.app,
#       model_name: 'Host::Model',
#       model_id: 123,
#       params: {
#         some_key: 'abc'
#       }
#     )
#   )
# end
class GidSublocator < GlobalID::Locator::UnscopedLocator
  def locate(gid, options = {})
    sublocate(super, gid)
  end

  def locate_many(gids, options = {})
    super.zip(gids).map do |(located, gid)|
      sublocate(located, gid)
    end
  end

  private def sublocate(located, gid)
    located.try(:gid_sublocate, gid) || located
  end
end

Raise error on unknown options

We just caught the following during code review:

model.to_sgid(purpose: :api, expires_at: 1.day.from_now.end_of_day)

The correct option to pass is for instead of purpose. This went unnoticed because everything still works, but the default purpose is used.

Maybe it would make sense to raise an error in case an unknown option is passed to to_sgid (and friends)?

bug - global id is incorrect with multiple calls

The issue as it seems global id cached on the model and if it is called more than once with different params it returns wrong result.

Check this out

> User.find(1).to_sgid.to_s == User.find(1).to_sgid(for:'asd').to_s
  User Load (1.5ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
  User Load (0.7ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
=> false

And compare to this:

> u = User.find(1)
  User Load (0.7ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
=> #<User:0x007febc9f74f38 ...>
> u.to_sgid.to_s == u.to_sgid(for:'asd').to_s
=> true

[Suggestion] Add support for eager loading optionally

Hello. I would like to ask you if there is an specific reason why not to optionally allow eager load of relationships on locate/locate_many.
I mean something like this:

def locate_many(gids, options = {})
  models_and_ids = gids.collect { |gid| [ gid.model_class, gid.model_id ] }
  ids_by_model = models_and_ids.group_by(&:first)
  loaded_by_model = Hash[ids_by_model.map do |model, ids|
    [ model, find_records(model, ids.map(&:last), includes: options[:includes], ignore_missing: options[:ignore_missing]).index_by { |record| record.id.to_s } ]
  end]

  models_and_ids.collect { |(model, id)| loaded_by_model[model][id] }.compact
end

private
 def find_records(model_class, ids, options)
    if options[:ignore_missing]
      model_class.includes(options[:includes]).where(id: ids)
    else
      model_class.includes(options[:includes]).find(ids)
    end
  end
def 

It would be not exactly like that code, but well, it's an idea.

Thanks in advance

Support for classes and methods

In my current project I use a lot of DI in the form of:

def do_something(first: SomeClass, second: SomeClass.method(:some_method))
  # ...
end

Passing my dependencies to an ActiveJob fails with an ActiveJob::SerializationError: Unsupported argument type: Class (or respectively Unsupported argument type: Method). However, I think this could be easily done by serialising methods and classes into:

SomeClass.method(:some_method).to_global_id # => gid://App/SomeClass/some_method
SomeClass.to_global_id # => gid://App/SomeClass/class

Anyone interested in this feature? Suggestions?

Allow apps to hook in their own locator

When multiple apps collaborate and reference each others' Global IDs, they'll use different means to locate the URI references.

For example, locate an Active Resource class:

GlobalID::Locator.use :foo do |gid|
  FooRemote.const_get(gid.model_name).find(gid.model_id)
end

GlobalID::Locator.locate 'gid://foo/Account/1234'
# => FooRemote::Account id=1234

Or locate the referenced model in a search index:

GlobalID::Locator.use :bar, BarLocator.new

class BarLocator
  def locate(gid)
    @search_client.search name: gid.model_name, id: gid.model_id
  end
end

GlobalID::Locator.locate 'gid://bar/Account/1234'
# => BarResult name=Account id=1234

App name cannot contain an underscore

Just trying Global ID with an app called PracticeManager -- this becomes practice_manager when creating a global ID URI, which causes URI::InvalidURIError, I believe due to the underscore:

URI::InvalidURIError: the scheme gid does not accept registry part: practice_manager (or bad hostname?)
from /Users/styrmis/.rvm/rubies/ruby-2.0.0-p0/lib/ruby/2.0.0/uri/generic.rb:214:in `initialize'

Setting config.global_id.app to a name which doesn't contain an underscore causes this error to go away.

{Fixnum,Bignum} is deprecated on Ruby 2.4

We may want to suppress the warnings for deprecated Fixnum/Bignum on Ruby 2.4.0.

Situation

$ ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]

$ bundle -v
Bundler version 1.14.3

$ rm Gemfile.lock (<= remove the `Gemfile.lock` file because tests on Ruby 2.4.0 are failed for activesupport-4 right now)

$ bundle install --path vendor/bundle

$ bundle list
Gems included by the bundle:
  * actionpack (5.0.1)
  * actionview (5.0.1)
  * activemodel (5.0.1)
  * activesupport (5.0.1)
  * builder (3.2.3)
  * bundler (1.14.3)
  * concurrent-ruby (1.0.4)
  * erubis (2.7.0)
  * globalid (0.3.7)
  * i18n (0.8.0)
  * loofah (2.0.3)
  * method_source (0.8.2)
  * mini_portile2 (2.1.0)
  * minitest (5.10.1)
  * nokogiri (1.7.0.1)
  * rack (2.0.1)
  * rack-test (0.6.3)
  * rails-dom-testing (2.0.2)
  * rails-html-sanitizer (1.0.3)
  * railties (5.0.1)
  * rake (12.0.0)
  * thor (0.19.4)
  * thread_safe (0.3.5)
  * tzinfo (1.2.2)

$ bundle exec rake test 2> test_err.log
...
127 runs, 229 assertions, 0 failures, 0 errors, 0 skips

$ grep deprecated test_err.log | grep -v 'vendor/bundle' | sort | uniq
/home/jaruga/git/globalid/test/cases/global_id_test.rb:105: warning: constant ::Fixnum is deprecated
/home/jaruga/git/globalid/test/cases/global_id_test.rb:106: warning: constant ::Fixnum is deprecated
/home/jaruga/git/globalid/test/cases/global_id_test.rb:115: warning: constant ::Bignum is deprecated
/home/jaruga/git/globalid/test/cases/global_id_test.rb:96: warning: constant ::Fixnum is deprecated
/home/jaruga/git/globalid/test/cases/global_id_test.rb:97: warning: constant ::Fixnum is deprecated

Possible Solutions

  • Change Fixnum to Integer, Bignum to Integer.
  • Change Fixnum to 0.class like rails case, rails/rails@cb0452e
  • Others

GlobalID assumes model name exists as constant in the top-level namespace

Currently GlobalID assumes that the GID model name exists as a constant defined in the top-level namespace. This is fine as a default, but I should be able to lookup models however I want if I'm using a customer locator.

Real world use case:

I have a monolithic application that I plan to break up into smaller services in the future. We are experimenting with GIDs to facilitate that transition.

Most GIDs look like 'gid://my-app/Person/1'.

For resources in namespaced modules that will eventually be refactored out, GIDs look like 'gid://my-module/Customer/1'. I have defined a custom locator for each module to handle this:

GlobalID::Locator.use :'my-module', MyModuleLocator.new

class MyModuleLocator
  def locate(gid)
    MyModule.const_get(gid.model_name).find(gid.model_id)
  end
end

However, this does not work—GlobalID.find('gid://my-module/Customer/1') raises a LoadError.

This is because GlobalID::Locator::find invokes GlobalID#model_class (which tries to Object.cont_get() the model name) before my custom locator is ever invoked [1]. Of course this raises a LoadError, because 'Customer' lives in the MyModule namespace.

[1] https://github.com/rails/globalid/blob/v0.3.6/lib/global_id/locator.rb#L17

Support for custom params

We use a multi-database based multi-tenant model in our app. This is not supported by the current gid format. A job scheduler has no way of finding out if "gid://MultiTenantApp/User/1" references the user in the database "tenant_1" or "tenant_2".

Some way to include the database name or tenant id would be very useful. Alternatively some way to easily add custom fields to gid would certainly be helpful.

expires_in does not work

tok = User.first.to_sgid(expired_in: Time.current)

...some time later...

GlobalID::Locator.locate_signed tok.to_s =>
 SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<User:0x00007f83d626bae0
 id: 1,
...

Is there a way to limit how many times a sgid can be used?

Is there a way for doing something similar to what is described bellow?

user = User.create(params)
uid = user.to_sgid(access_count: 1).to_s
GlobalID::Locator.locate_signed(uid) # => returns user
GlobalID::Locator.locate_signed(uid) # => returns nil

Allow limiting lookups to specific classes

So locating a gid doesn't mean looking up arbitrary objects.

With a class:

GlobalID::Locator.locate gid, only: [ User, Group ]

With a module:

GlobalID::Locator.locate gid, only: Subscribable

If the gid references a class that doesn't have only.any? { |mod| klass < mod }, return nil.

Signing.. with purpose

Signing a message prevents it from being tampered with, but it doesn't prevent it from being reused in another context.

For example, an app that accepts a User#sgid as a remember-me token and uses User#sgid for a list of forum members.

Someone can take the signed, tamperproof Global ID from the forum member list and stick it in their remember-me token and, voila, they're signed in as that user.

To prevent this class of reuse and replay, signed messages need to indicate their purpose and include it in the signed content. The purpose of the signed Global ID also needs to be tamperproof.

login_sgid = Person.find(1).signed_global_id(for: 'login')
GlobalID::Locator.locate_signed(login_sgid, for: 'like_button')
# => BOOM!

GlobalID::Locator.locate_signed(login_sgid, for: 'login')
# => Person id=1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.