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...