Rails: Access to current_user from within a model in Ruby on Rails
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 whe开发者_StackOverflow中文版ther 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 likemay_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?
I'd say your instincts to keep current_user
out of the model are correct.
Like Daniel I'm all for skinny controllers and fat models, but there is also a clear division of responsibilities. The purpose of the controller is to manage the incoming request and session. The model should be able to answer the question "Can user x do y to this object?", but it's nonsensical for it to reference the current_user
. What if you are in the console? What if it's a cron job running?
In many cases with the right permissions API in the model, this can be handled with one-line before_filters
that apply to several actions. However if things are getting more complex you may want to implement a separate layer (possibly in lib/
) that encapsulates the more complex authorization logic to prevent your controller from becoming bloated, and prevent your model from becoming too tightly coupled to the web request/response cycle.
Although this question has been answered by many I just wanted to add my two cents in quickly.
Using the #current_user approach on the User model should be implemented with caution due to Thread Safety.
It is fine to use a class/singleton method on User if you remember to use Thread.current as a way or storing and retrieving your values. But it is not as easy as that because you also have to reset Thread.current so the next request does not inherit permissions it shouldn't.
The point I am trying to make is, if you store state in class or singleton variables, remember that you are throwing thread safety out the window.
The Controller should tell the model instance
Working with the database is the model's job. Handling web requests, including knowing the user for the current request, is the controller's job.
Therefore, if a model instance needs to know the current user, a controller should tell it.
def create
@item = Item.new
@item.current_user = current_user # or whatever your controller method is
...
end
This assumes that Item
has an attr_accessor
for current_user
.
(Note - I first posted this answer on another question, but I've just noticed that question is a duplicate of this one.)
I'm all in for skinny controller & fat models, and I think auth shouldn't break this principle.
I've been coding with Rails for an year now and I'm coming from PHP community. For me, It's trivial solution to set the current user as "request-long global". This is done by default in some frameworks, for example:
In Yii, you may access the current user by calling Yii::$app->user->identity. See http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html
In Lavavel, you may also do the same thing by calling Auth::user(). See http://laravel.com/docs/4.2/security
Why if I can just pass the current user from controller??
Let's assume that we are creating a simple blog application with multi-user support. We are creating both public site (anon users can read and comment on blog posts) and admin site (users are logged in and they have CRUD access to their content on the database.)
Here's "the standard ARs":
class Post < ActiveRecord::Base
has_many :comments
belongs_to :author, class_name: 'User', primary_key: author_id
end
class User < ActiveRecord::Base
has_many: :posts
end
class Comment < ActiveRecord::Base
belongs_to :post
end
Now, on the public site:
class PostsController < ActionController::Base
def index
# Nothing special here, show latest posts on index page.
@posts = Post.includes(:comments).latest(10)
end
end
That was clean & simple. On the admin site however, something more is needed. This is base implementation for all admin controllers:
class Admin::BaseController < ActionController::Base
before_action: :auth, :set_current_user
after_action: :unset_current_user
private
def auth
# The actual auth is missing for brievery
@user = login_or_redirect
end
def set_current_user
# User.current needs to use Thread.current!
User.current = @user
end
def unset_current_user
# User.current needs to use Thread.current!
User.current = nil
end
end
So login functionality was added and the current user gets saved to a global. Now User model looks like this:
# Let's extend the common User model to include current user method.
class Admin::User < User
def self.current=(user)
Thread.current[:current_user] = user
end
def self.current
Thread.current[:current_user]
end
end
User.current is now thread-safe
Let's extend other models to take advantage of this:
class Admin::Post < Post
before_save: :assign_author
def default_scope
where(author: User.current)
end
def assign_author
self.author = User.current
end
end
Post model was extended so that it feels like there's only currently logged in user's posts. How cool is that!
Admin post controller could look something like this:
class Admin::PostsController < Admin::BaseController
def index
# Shows all posts (for the current user, of course!)
@posts = Post.all
end
def new
# Finds the post by id (if it belongs to the current user, of course!)
@post = Post.find_by_id(params[:id])
# Updates & saves the new post (for the current user, of course!)
@post.attributes = params.require(:post).permit()
if @post.save
# ...
else
# ...
end
end
end
For Comment model, the admin version could look like this:
class Admin::Comment < Comment
validate: :check_posts_author
private
def check_posts_author
unless post.author == User.current
errors.add(:blog, 'Blog must be yours!')
end
end
end
IMHO: This is powerful & secure way to make sure that users can access / modify only their data, all in one go. Think about how much developer needs to write test code if every query needs to start with "current_user.posts.whatever_method(...)"? A lot.
Correct me if I'm wrong but I think:
It's all about separation of concerns. Even when it's clear that only controller should handle the auth checks, by no means the currently logged in user should stay in the controller layer.
Only thing to remember: DO NOT overuse it! Remember that there may be email workers that are not using User.current or you maybe accessing the application from a console etc...
Ancient thread, but worth noting that starting in Rails 5.2, there's a baked-in solution to this: the Current model singleton, covered here: https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond#current-everything
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
Well my guess here is that current_user
is finally a User instance, so, why don't u add these permissions to the User
model or to the data model u want to have the permissions to be applied or queried?
My guess is that u need to restructure your model somehow and pass the current user as a param, like doing:
class Node < ActiveRecord
belongs_to :user
def authorized?(user)
user && ( user.admin? or self.user_id == user.id )
end
end
# inside controllers or helpers
node.authorized? current_user
I'm always amazed at "just don't do that" responses by people who know nothing of the questioner's underlying business need. Yes, generally this should be avoided. But there are circumstances where it's both appropriate and highly useful. I just had one myself.
Here was my solution:
def find_current_user
(1..Kernel.caller.length).each do |n|
RubyVM::DebugInspector.open do |i|
current_user = eval "current_user rescue nil", i.frame_binding(n)
return current_user unless current_user.nil?
end
end
return nil
end
This walks the stack backwards looking for a frame that responds to current_user
. If none is found it returns nil. It could be made more robust by confirming the expected return type, and possibly by confirming owner of the frame is a type of controller, but generally works just dandy.
I'm using the Declarative Authorization plugin, and it does something similar to what you are mentioning with current_user
It uses a before_filter
to pull current_user
out and store it where the model layer can get to it. Looks like this:
# set_current_user sets the global current user for this request. This
# is used by model security that does not have access to the
# controller#current_user method. It is called as a before_filter.
def set_current_user
Authorization.current_user = current_user
end
I'm not using the model features of Declarative Authorization though. I'm all for the "Skinny Controller - Fat Model" approach, but my feeling is that authorization (as well as authentication) is something that belongs in the controller layer.
I have this in an application of mine. It simply looks for the current controllers session[:user] and sets it to a User.current_user class variable. This code works in production and is pretty simple. I wish I could say I came up with it, but I believe I borrowed it from an internet genius elsewhere.
class ApplicationController < ActionController::Base
before_filter do |c|
User.current_user = User.find(c.session[:user]) unless c.session[:user].nil?
end
end
class User < ActiveRecord::Base
attr_accessor :current_user
end
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
My feeling is the current user is part of the "context" of your MVC model, think of the current user like of the current time, the current logging stream, the current debugging level, the current transaction etc. You could pass all these "modalities" as arguments into your functions. Or you make it available by variables in a context outside the current function body. Thread local context is the better choice than global or otherwise scoped variables because of easiest thread safety. As Josh K said, the danger with thread locals is that they must be cleared after the task, something a dependency injection framework can do for you. MVC is a somewhat simplified picture of the application reality and not everything is covered by it.
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!
精彩评论