devise_cas_authenticatable with dynamic cas_base_url - ruby-on-rails

Using Rails, Devise and devise_cas_authenticatable, I want to use a different CAS server depending on some internal configuration.
Since cas_base_url is set up in an initializer, the value is re-written like this in a before filter:
Devise.setup do |config|
config.cas_base_url="my_custom_variable_cas_server"
end
As soon as the before_filter is called, and then :authenticate_user!, in the first run the custom cas server is set, but after that it's cached or something and never modified.
In the environments/development.rb, config.cache_classes = false.
Is there any way to extend Devise or override the function that specifies which is the login url for devise_cas_authenticatable?

The solution was to redefine the 'new' action in the cas gem in a lib file:
Create a new file in /lib called
devise_cas_authenticatable_extender.rb, or any other name you like.
touch lib/devise_cas_authenticatable_extender.rb
Copy and paste the following
text inside
# This library is necessary for CAS authentication to work dynamically
# Note: if called from application controller it raises a circular dependency ...
# ... error when using thin (not with rails s)
# It has to be required in every controller that uses CAS authentication
Devise::CasSessionsController.class_eval do
# We redefine the behaviour of the new action to be dynamic
def new
domain=request.protocol + ((request.port.blank? || request.port==80) ? request.host : "#{request.host}:#{request.port}") # Production and development
servicio=domain+"/users/service"
servicio=servicio.gsub(':', '%3A').gsub('/', '%2F') # This can probably be done with URI
url="https://your_cas_server_domain/#{session[:dynamic_url_path]}/login?service=#{servicio}"
redirect_to url
end
end
Insert the following in the controller(s) that use
authentication (note: if you use application_controller.rb, you may
get a circular_dependency error, I didn't get into solving that
since I don't use application_controller.rb for session stuff).
# This library is necessary for CAS authentication to work dynamically
# Note: if called from application controller it raises a circular dependency ...
# ... error when using thin (not with rails s)
# It has to be required in every controller that uses CAS authentication
require 'devise_cas_authenticatable_extender'
Somewhere in your code, set the session[:dynamic_url_path] variable to something that makes sense in your project, like:
session[:dynamic_url_path] = request.path_parameters[:your_routes_path_variable_here]
That's it :)

I've used this solution and I was getting the Circular dependency error with the application running in production environment (because of the eager_load configuration).
Finally I got it working inserting the require 'devise_cas_authenticatable_extender' in the config/environments.rb file.

Related

Accessing Rails engine's URL helpers in initializer

I'm trying to access the url helpers in my engine to set up rack-cors. Right now, I've hard-coded the strings for one of the urls in the rack-cors middleware configuration. I have read the order in which Rails initializers are run, and at this point in the load order I should have the engine routes available to me. I thought I would have them available at the event add_routing_paths, but I couldn't find the routes after digging around using pry. Another statement that leads me to think I'm doing this incorrectly is that the docs say: "Some parts of your application, notably routing, are not yet set up at the point where the after_initialize block is called." According to this list:
require "config/boot.rb" to setup load paths
require railties and engines
Define Rails.application as "class MyApp::Application < Rails::Application"
Run config.before_configuration callbacks
Load config/environments/ENV.rb
Run config.before_initialize callbacks
Run Railtie#initializer defined by railties, engines and application.
One by one, each engine sets up its load paths, routes and runs its config/initializers/* files.
Custom Railtie#initializers added by railties, engines and applications are executed
Build the middleware stack and run to_prepare callbacks
Run config.before_eager_load and eager_load! if eager_load is true
Run config.after_initialize callbacks
I'm trying to hook into (7), but perhaps routes aren't available until (11)?
module Zillow
class Engine < ::Rails::Engine
isolate_namespace Zillow
# Rails.application.routes.url_helpers
initializer "zillow.cors", after: :set_routes_reloader do |app|
require 'pry'; binding.pry
app.config.app_middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:3000'
resource '/zillow/search_results', methods: :get
end
end
end
end
end
Here is the output of my routes
zillow /zillow Zillow::Engine
Routes for Zillow::Engine:
api_deep_comps GET /api/deep_comps(.:format) zillow/api#deep_comps
api_zestimate GET /api/zestimate(.:format) zillow/api#zestimate
api_search_results GET /api/search_results(.:format) zillow/api#search_results
api_updated_property_details GET /api/updated_property_details(.:format) zillow/api#updated_property_details
You can fire your own event when routes are loaded and then subscribe to that event during initialization to get routes data. For that:
Add this to the end of config/routes.rb file (outside of routes.draw block)
ActiveSupport::Notifications.instrument 'routes_loaded.application'
Subscribe to this event in initialization code and use URL helpers!
ActiveSupport::Notifications.subscribe 'routes_loaded.application' do
Rails.logger.info Rails.application.routes.url_helpers.home_path
end
For more information see:
Subscribing to an event
Creating custom events
Load it just after initialize.
I needed to use root_url in my initializer but it wasn't properly initialized. I tried using the Pub / Sub method that Anton Styagun suggests but even when that's called the URL Helpers were not properly initialized.
Instead, I opted to load my initializer file manually just after the Rails app itself has initialized. This takes a few steps:
Move your initializer from config/initializers/ to lib/.
Require your file in environment.rb just after your application initializes. Ours looks something like this:
CNTRAL::Application.initialize!
require "my_initializer" # Omit the `lib/` because that's the load path it looks in.
Now, inside of my_initializer.rb I was able to call root_url by doing this:
Rails.application.routes.url_helpers.root_url
NOTE: It's important not to just use include Rails.application.routes.url_helpers. It raised an exception on me.
I also added a comment in both the initializer and the environment.rb of where this was loaded/required so it's more obvious to myself in the future and other engineers.
Alternative Method
You can also use the following method and it works well:
In your application.rb file, add:
config.after_initialize do
Rails.application.reload_routes! # Necessary to load the Routes.
require "my_initializer"
end
After looking into this further and reading the rack-cors example done in Rails3, it may not be possible to retrieve the route helpers inside the initializer at any point in time.

Pull some configuration variables from the DB to set dynamically in Rails environment?

Is it possible to set some configuration variables in production.rb dynamically using values from the database?
I'm building a multi-tenant app and each tenant has some environment specific information that needs to be set dynamically.
For example:
// production.rb
config.action_mailer.default_url_options = { :host => current_tenant.domain }
The current_tenant is a helper method is defined in ApplicationHelper so can't be accessed in production.rb
Here is current_tenant:
// application_helper.rb
def current_tenant
#current_tenant ||= Tenant.find_by(domain: Apartment::Tenant.current)
end
If not, is it possible to dynamically create secrets.yml using DB data? Since then I would be able to use ENV in production.rb
Maybe you can try this instead:
Create a helper method for your emails:
def build_tenant_url(route_name, *args)
opts = args.extract_options!
opts.merge!(host: #tenant ? #tenant.domain : your_domain)
#Here you can merge other options if you need to pass a token etc
Rails.application.routes_url_helpers.send(:"#{route_name}_url", *args, opts)
end
In your mailer, define the #tenant
Then in your email view use it
= link_to 'Edit', build_tenant_url(:edit_tenant, #tenant)
This setup runs when the app starts up, so if you want an option to be driven by some variable, that variable will need to be available to the Ruby code that runs at startup. There's various ways this could be done
the script (or whatever) which starts your rails server passes through a parameter
a value is written into a text file by some other process and read out again by the startup script
same again but with an environment variable
For example, if you were giving each tenant (group of users) their own subdomain on your app, then the subdomain name could be used as a key for the startup code to read the database and pull out some details for that tenant. This subdomain would then be passed through to the app when you spin it up.
EDIT - having said all this, i'm not sure if it's necessary for you to change the startup config at all. Can't you just look up the appropriate domain name whenever you send an email, rather than changing the default set in the config?

Where is a good place to initialize an API?

I wanted to use this api: https://github.com/coinbase/coinbase-ruby and the first step is to initialize the API, like this:
coinbase = Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
I was wondering what the best place to put this code is, and how would I access it if I put it "there"? I want this variable (coinbase) to be accessible ANYWHERE in the application.
Thanks!
The answer to this question really depends on your use case and your approach. My geral recommendation, however, is to create a Service Object (in the DDD sense) (see the section named "Domain Objects Should Not Know Anything About Infrastructure Underneath" in that link), that handles all communication with the Coinbase API. And then, within this service object, you can simply initialize the Coinbase::Client object once for however many times you call into it. Here's an example:
# app/services/coinbase_service.rb
class CoinbaseService
cattr_reader :coinbase_client, instance_accessor: false do
Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
end
def self.do_something
coinbase_client.do_something_in_their_api
end
def self.do_something_else
coinbase_client.do_something_else_in_their_api
end
end
So then you might do, e.g.:
# From MyController#action_1
if CoinbaseService.do_something
# ...
else
# ...
end
Or:
# From MyModel
def do_something
CoinbaseService.do_something_else
end
To get the service object working, you may need to add app/services to your autoload paths in application.rb file. I normally just add this:
# config/application.rb
config.autoload_paths += %W(#{config.root}/app)
I find this Service Object approach to be very beneficial organizationally, more efficient (only 1 invocation of the new Coinbase client needed), easier to test (easy to mock-out calls to Coinbase::Client), and simply joyful :).
One way to go about having a global variable can be done as similar as initializing redis in a Rails application by creating an initializer in config/initializers/coinbase.rb with:
$coinbase = Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
Now, you can access $coinbase anywhere in the application!
In the file config/initializers/coinbase.rb
Rails.application.config.after_initialize do
CoinbaseClient = Coinbase::Client.new(
Rails.application.credentials.coinbase[:api_key],
Rails.application.credentials.coinbase[:api_secret])
end
In place of the encrypted credentials, you could also use environment variables: ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET']
The above will make the constant CoinbaseClient available everywhere in your app. It will also ensure all your gems are loaded before the client is initialized.
Note: I am using Rails 6.1.4.4, and Ruby 2.7.5

How to extend an 'unloadable' Rails plugin?

I'm trying to write a plugin that will extend InheritedResources.
Specifically I want to rewrite some default helpers.
And I'd like it to "just work" once installed, w/o any changes to application code.
The functionality is provided in a module which needs to be included in a right place. The question is where? :)
The first attempt was to do it in my plugin's init.rb:
InheritedResources::Base.send :include, MyModule
It works in production, but fails miserably in development since InheritedResource::Base declared as unloadable and so its code is reloaded on each request. So my module is there for the first request,
and then its gone.
InheritedResource::Base is 'pulled' in again by any controller that uses it:
Class SomeController < InheritedResource::Base
But no code is 'pulling in' my extension module since it is not referenced anywhere except init.rb which is not re-loaded on each request
So right now I'm just including the module manually in every controller that needs it which sucks.
I can't even include it once in ApplicationController because InheritedResources inherites from it and so it will override any changes back.
update
I'm not looking for advice on how to 'monkey patch'. The extension is working in production just great. my problem is how to catch moment exactly after InheritedResources loaded to stick my extension into it :)
update2
another attempt at clarification:
the sequence of events is
a) rails loads plugins. my plugin loads after inherited_resources and patches it.
b) a development mode request is served and works
c) rails unloads all the 'unloadable' code which includes all application code and also
inherited_resources
d) another request comes in
e) rails loads controller, which inherites from inherited resources
f) rails loads inherited resources which inherit from application_controller
g) rails loads application_contrller (or may be its already loaded at this stage, not sure)
g) request fails as no-one loaded my plugin to patch inherited_resources. plugin init.rb files are not reloaded
I need to catch the point in time between g and h
The Rails::Configuration, config in the environment files, allows registering a callback on the dispatcher that runs before each request in development mode, or once when in production mode.
config.to_prepare do
# do something here
end
The problem is, I don't think your plugin has access to config when the init.rb file is run. Here is a way to register your callback directly in the dispatcher. Just put this in the init.rb file.
require 'dispatcher'
::Dispatcher.to_prepare do
puts "hi there from a plugin"
end
Warning: I don't know what side effects this may have. If possible, try to get access to config and register the callback tha right way.
What you are attempting to do is usually called "MonkeyPatch" - changing the way one module or class is working by "overriding" methods.
It is a common practice in Rails, but it doesn't mean it is the best way to do things - when possible, it is better to use common inheritance (it is more explicit about the changes you make).
Regarding your questions about "where to put the files": it is usually the lib/ directory. This can mean the lib of the rails app, or a lib directory inside a gem or plugin, if you are into that sort of thing.
For example, if the file you want to change is lib/generators/rails/templates/controller.rb of inherited resources, the first thing you have to do is replicate that directory structure inside your lib/ folder ('lib/generators/rails/templates/controller.rb')
Inside that new file of yours, (empty at the beginning) you can override methods. However, you must also the modules/classes hierarchy. So if the original gem had this:
module foo
module bar
def f1
...
end
def f2
...
end
end
def f3
...
end
end
And you wanted to modify f1, you would have to respect the foo-bar modules.
module foo
module bar
def f1
... # your code here
end
end
end
Now the last thing you need is to make sure this code is executed at the right time. If you are using the application's lib/ folder, you will need to create an entry on the initializers/ folder and require your new file. If you are developing a gem/plugin, you will have a init.rb file on the "root" folder of that plugin. Put the 'require' there.
I'm not very familiar with this unloadable stuff; maybe I'm asking something obvious but- have you tried making your extension module unloadable, too? (You shouldn't need this if you monkeypatched the module instead of creating a new one)

Constants set in environment.rb disappear in development mode

Someone who understands how rails caching works can really help me out here. Here's the code, nested inside of the Rails::Initializer.run block:
config.after_initialize do
SomeClass.const_set 'SOME_CONST', 'SOME_VAL'
end
Now if I run script/server and make a request, everything is dandy. However, on the second request to my Rails app, all goes to hell with an unitialized constant error. In production mode, I can make the second request successfully, meaning the constant is still there.
I've fixed the problem by changing the above to:
config.after_initialize do
require 'some_class' # in RAILS_ROOT/lib/some_class.rb
SomeClass.const_set 'SOME_CONST', 'SOME_VAL'
end
But now that means whenever I make a change to some_class.rb, I have to restart the server. Is there any way to set constants in an environment file and have them work correctly in development mode? Why does the constant exist on the first request, but not the following request?
UPDATE: Since environment.rb is only read when the Rails app is booted and I want both my lib files and models to be reloaded on each request, I was forced to move the constants into the some_class.rb file as follows:
if Rails.env.development?
const_set 'SOME_CONST', 'SOME_DEVELOPMENT_VAL'
end
And in environments/production.rb, I have the old const_set code.
UPDATE: An even better method using config.to_prepare is detailed below.
It only works on the first request in development mode because the classes are reloaded on each request. So on the first request the constant is set in the initializer, and all is good. Then on the next request, it reloads the class WITHOUT rerunning the bit from your initializer, so the constant isn't set from there on out.
It works in production mode because the classes aren't reloaded for each request, so you don't lose that bit of class state each time.
So you might want to set the constant either in the model, or in a config.to_prepare instead config.after_initialize. to_prepare is called before each request.
In the model:
class SomeClass < ActiveRecord::Base
MY_CONST = "whatever"
# You can access MY_CONST directly, but I tend to wrap them in a class
# method because literal constants often get refactored into the database.
def self.my_const
MY_CONST
end
end
In the config:
# This will run before every single request. You probably only want this in
# the development config.
config.to_prepare do
SomeClass.const_set 'SOME_CONST', 'SOME_VAL'
end
Production mode preloads all of the classes, whereas in development mode classes are loaded as needed, after the config files are read. Manually requiringing them in your configs forces the classes to be read before/during the config stage.

Resources