Rails: Access to current_user from within a model in Ruby on Rails

Viewed 81293

I need to implement fine-grained access control in a Ruby on Rails app. The permissions for individual users are saved in a database table and I thought that it would be best to let the respective resource (i.e. the instance of a model) decide whether a certain user is allowed to read from or write to it. Making this decision in the controller each time certainly wouldn’t be very DRY.
The problem is that in order to do this, the model needs access to the current user, to call something like may_read?(current_user, attribute_name). Models in general do not have access to session data, though.

There are quite some suggestions to save a reference to the current user in the current thread, e.g. in this blog post. This would certainly solve the problem.

Neighboring Google results advised me to save a reference to the current user in the User class though, which I guess was thought up by someone whose application does not have to accommodate a lot of users at once. ;)

Long story short, I get the feeling that my wish to access the current user (i.e. session data) from within a model comes from me doing it wrong.

Can you tell me how I’m wrong?

13 Answers

To shed more light on armchairdj's answer

I faced this challenge when working on a Rails 6 application.

Here's how I solved it:

From Rails 5.2 you now we can add a magic Current singleton which acts like a global store accessible from anywhere inside your app.

First, define it in your models:

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

Next, set the user somewhere in your controller to make it accessible in models, jobs, mailers, or wherever you want:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
  end
end

Now you can call the Current.user in your models:

# app/models/post.rb
class Post < ApplicationRecord
  # You don't have to specify the user when creating a post,
  # the current one would be used by default
  belongs_to :user, default: -> { Current.user }
end

OR you can call the Current.user in your forms:

# app/forms/application_registration.rb
class ApplicationRegistration
  include ActiveModel::Model

  attr_accessor :email, :user_id, :first_name, :last_name, :phone,
                    
  def save
    ActiveRecord::Base.transaction do
      return false unless valid?

      # User.create!(email: email)
      PersonalInfo.create!(user_id: Current.user.id, first_name: first_name,
                          last_name: last_name, phone: phone)

      true
    end
  end
end

OR you can call the Current.user in your views:

# app/views/application_registrations/_form.html.erb
<%= form_for @application_registration do |form| %>
  <div class="field">
    <%= form.label :email %>
    <%= form.text_field :email, value: Current.user.email %>
  </div>

  <div class="field">
    <%= form.label :first_name %>
    <%= form.text_field :first_name, value: Current.user.personal_info.first_name %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

Note: You may say: “This feature breaks the Separation of Concerns principle!” Yes, it does. Don’t use it if it feels wrong.

You can read more about this answer here: Current everything

That's all.

I hope this helps

This is 2021 calling. Since rails 5.2 there's a new global API that can be used, but use it with caution as stated in the API docs:

https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html

Abstract super class that provides a thread-isolated attributes singleton, which resets automatically before and after each request. This allows you to keep all the per-request attributes easily available to the whole system.

A word of caution: It's easy to overdo a global singleton like Current and tangle your model as a result. Current should only be used for a few, top-level globals, like account, user, and request details. The attributes stuck in Current should be used by more or less all actions on all requests. If you start sticking controller-specific attributes in there, you're going to create a mess.

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_current_user

  private

  def set_current_user
    Current.user = current_user
  end
end

# and now in your model
# app/models/post.rb
class Post < ApplicationRecord
  # You don't have to specify the user when creating a post,
  # the current one would be used by default
  belongs_to :user, default: -> { Current.user }
end

I am so very late to this party, but if you need fine-grained access control or have complex permissions I would definitely recommend the Cancancan Gem: https://github.com/CanCanCommunity/cancancan

It allows you to define permissions at every action in your Controller to whatever you want, and since you define the current ability on any controller you can send any parameters you need, like the current_user. You can define a generic current_ability method in ApplicationController and set up things automagically:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
  def current_ability
    klass = Object.const_defined?('MODEL_CLASS_NAME') ? MODEL_CLASS_NAME : controller_name.classify
    @current_ability ||= "#{klass.to_s}Abilities".constantize.new(current_user, request)
  end
end

This way, you can have a UserAbilities Class linked to your UserController, PostAbilities to your PostController and so on. And then define the complex rules in there:

class UserAbilities
  include CanCan::Ability

  def initialize(user, request)
    if user
      if user.admin?
        can :manage, User
      else
        # Allow operations for logged in users.
        can :show, User, id: user.id
        can :index, User if user
      end
    end
  end
end

It's a great Gem! hope it helps!

Related