Search code examples
ruby-on-railsherokudoorkeeper

Reset scopes set in config/initializers/doorkeeper.rb on Heroku


I'm trying to create a feature in my OAuth2 service where developers can create, read, and destroy scopes for use by their applications that use my service.

In order to do this, I've created a basic Scope model and I want Doorkeeper to update its @optional_scopes / @scopes with whatever scopes a user comes and creates/destroys. (Note: scopes can only be destroyed if they aren't in use.)

Note (TL;DR): This all works perfectly in development, but it isn't working in production on Heroku -- so the crux of the question is really surrounding how to update the instance variables inside of Doorkeeper that are normally set upon the app's initialization.... And if it's possible at all!

I've set the initializer to grab all the scopes in the DB and set them to optional_scopes.

In config/initializers/doorkeeper.rb:

Doorkeeper.configure do
  ...
  default_scopes :public
  optional_scopes( *Scope.where.not(name: 'public').map{ |s| s.name } )
  ...
end

I have a basic controller for my "CRD" of scopes which has a filter to reset the scopes list after one has been created or destroyed:

class ScopesController < ApplicationController
  after_action :set_optional_scopes, only: [ :create, :destroy ]
  ...

  def set_optional_scopes
    Doorkeeper.configuration.instance_variable_set(
      '@optional_scopes',
      Scope.where.not(name: 'public').map{ |s| s.name }
    )
  end
end

In the view for my linked applications, I have a loop of the scopes which offers the user checkboxes for the scopes. views/doorkeeper/applications/_form.html.erb:

<% Doorkeeper.configuration.optional_scopes.each do |scope| %>
  <%= check_box_tag(
    scope,
    'true',
    application_has_scope?( application, scope.to_s )
  ) %>
  <%= label_tag(
    scope,
    scope.to_s,
    class: 'no-style display-inline-block'
  ) %>
  <br>
<% end %>

Note how I'm calling Doorkeeper.configuration.optional_scopes to populate the checkboxes.

Concerned with this code updating appropriately across Heroku instances, I also overwrote Doorkeeper's self.configuration method from:

module Doorkeeper
  ...
  def self.configuration
    @config || (fail MissingConfiguration)
  end
  ...
end

to:

module Doorkeeper
  def self.configuration
    if @config
      # Reset the scopes every time the config is called
      @config.instance_variable_set(
        '@scopes',
        Scope.all.map{ |s| s.name }
      )
      @config
    else
      (fail MissingConfiguration)
    end
  end
end

So, as I said above, this is working well in development. However, in production it fails to update the list of checkboxes, which means that Doorkeeper.configuration.optional_scopes doesn't get appropriately reset after the create action.

Thanks so much for your time and any help!


Solution

  • Okay, well, in the process of writing this, I slowed down and figured out the solution, which was right in front of my nose...

    In the override of Doorkeeper's self.configuration method, all I needed to do was reset optional_scopes instead of scopes as scopes gets defined as default_scopes + optional_scopes anyway.

    So it looks like this:

    def self.configuration
      if @config
        # Reset the scopes every time the config is called
        @config.instance_variable_set(
          '@optional_scopes',
          Scope.where.not(name: 'public').map{ |s| s.name }
        )
        @config
      else
        (fail MissingConfiguration)
      end
    end
    

    This caused all my tests to fail due to a NoMethodError for the super class of Doorkeeper::OAuth::Scopes then I realized I needed to rewrite that method to include an elsif for Array. So, here's that method:

    module OAuth
      class Scopes
        def +(other)
          if other.is_a? Scopes
            self.class.from_array(all + other.all)
          elsif other.is_a? Array
            self.class.from_array(all + other)
          else
            super(other)
          end
        end
      end
    end
    

    You can see the original here.

    I hope all of this helps someone someday!