Question

The scenario: I need to give models access to API tokens stored in the session.

Background: I have an API-driven rails 3 application utilizing DataMapper(DM) and a DM adapter to interface with the API. Each DM model has a corresponding REST-ish API endpoint much like you get with rails scaffolding. The API requires various headers for requests, including API tokens, keys, ids etc. The headers have nothing to do with the requested data, they exist for authorization and tracking purposes only. A number of these tokens are stored in the session. I want a clean way to make these API headers available to any model during a request.

Possible solutions:

1. Passing session variables from the controller to the models

The obvious answer is passing the tokens in a hash or other object from the controller to the models. A controller action might have the following: @user = User.find(params[:id], api_headers).

The problem is needing to override any model method to accept the additional api_headers object. Not counting methods defined by Rails and DataMapper, there are hundreds of methods already defined in the application models that would need to be rewritten. So I'm ruling out a rewrite, and this also doesn't seem like a good solution since it would require overriding a ridiculous number of DM methods like the User#find example above.

2. Some metaprogramming hack

I could catch any ArgumentError's on DM's base class and check if the last argument is the api_headers object, then set the values as instance variables and invoke the requested method. This thought exercise already has me cringing at dealing with optional arguments etc. If given long enough I could probably create a functional Frankenstein that should get me fired but probably wouldn't.

3. Use a singleton (current preferred solution)

In the application controller set a before_filter to dump the session-stored API headers into a singleton ApiHeaders object. Then any model making an API request can get that singleton with the required API headers.

An additional after_filter* on the application controller would set all attributes to nil on the ApiHeaders singleton at the end of the request to prevent leaking headers between requests.

This is currently my preferred solution but I don't like that the API header values could potentially carry over into other requests if the after_filter doesn't get invoked. I don't know in which scenarios this might happen (in an application error perhaps?) which raises concerns. All I know is the values don't necessarily die with the request.

4. Custom code

Drop support of DataMapper and the custom API adapter and manually make all API calls, passing through all required API headers. Besides the fact I don't have time for this level of rewrite, why use a framework at all if you have to throw a huge chunk out to support a custom auth scheme?

Summary

What's the cleanest way to get these pesky API tokens from the session into the bowels of the application where they can be sent with each API request? I'm hoping for a better solution than those listed above.

* An alias for after_action

Was it helpful?

Solution

I set the current user and the request information on my User model using the request_store gem which is just a tiny shim over thread local storage with a bit of clean-up.

This makes the information available from any of my models via the User class. I have User.current, User.request and User.location available wherever I need it.

Your controller just has to set User.current and User.request once it has authenticated the user.

Example User model:

# models/user.rb
require 'request_store'

class User

  def self.current
    RequestStore.store[:current_user]
  end

  def self.current=(user)
    RequestStore.store[:current_user] = user
  end

  def self.request
    RequestStore.store[:current_request]
  end

  def self.request=(request)
    # stash the request so things like IP address and GEO-IP based location is available to other models
    RequestStore.store[:current_request] = request
  end

  def self.location
    # resolve the location just once per request
    RequestStore.store[:current_location] ||= self.request.try(:location)
  end
end

OTHER TIPS

Use Thread.current, which is passed in from request to model (note, this breaks if, inside your request, you use sub-threads). You can store the attribute you want to share in a cattr_accessor or in rails cache:

in a cattr_accessor

class YourClass
    cattr_accessor :my_var_hash

...
# and in your controller

# set the var
YourClass.my_var_hash = {} if YourClass.my_var_hash.nil?
YourClass.my_var_hash[Thread.current.object_id] = {}
YourClass.my_var_hash[Thread.current.object_id][your_var] = 100

... and in your model

lvalue = YourClass.my_var_hash[Thread.current.object_id][your_var]

Note, if you use this method, you will also want to make one of the hash values a timestamp, and do some housekeeping on getting, by deleting old keys, b/c you'll eventually use up all your system memory if you don't do the housekeeping

with cache:

# in your controller
@var = Rails.cache.fetch("#{Thread.current.object_id}_var_name") do 
    return 100 # do your work here to create the var value and return it
end

# in your model
lvalue = Rails.cache.fetch(("#{Thread.current.object_id}_var_name")

You can then set the cache expiration to 5 minutes, or you can wildcard clear your cache at the end of your request.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top