DRY concepts in rails controllers - ruby-on-rails

I'm working on my first rails api server.
I've got a controller for my User model that looks as such:
class UsersController < ApplicationController
def index
if current_user.admin?
#users = User.all
render json: #users
else
render json: { message: 'You do not have the appropriate permissions to access this resource' }, status: 401
end
end
def show
if User.exists?(#id)
#id = params[:id]
if current_user.id.to_s == #id || current_user.admin?
#user = User.find(#id)
render json: #user
else
render json: { message: 'You do not have the appropriate permissions to access this resource' }, status: 401
end
else
render json: { message: 'Requested resource not found' }, status: 404
end
end
end
What I want and currently have for these two controller methods is:
/users fetch all users only if the authenticated user making the request is of role admin
/users/:id fetch a user by id only if the authenticated user making the request has a matching id or is of role admin
The current implementation breaks the DRY philosophy. The reasoning is that the logic for handling whether or not the requesting user has the permissions to access the requested resource(s) is repeated across both controller methods. Furthermore, any model's controller method for show will repeat the logic for checking whether or not the requested resource exists. I also feel like this kind of implementation makes for fat controllers, where I'd rather them be skinny.
What I want to know from the community and from those that have solved this problem before; what is the best way to go about this in order to conform to the DRY philosophy and to keep controllers skinny.
Good to know: I'm using devise and devise-token-auth for authentication.

You need to use some kind of Authorization gem like cancancan. It is exactly what you need. Also it's else not elsif. elsif is followed by condition.

You can use github.com/varvet/pundit instead, for authorization.
It matches with the controller, instead of putting the authorization in the controller, you can use this to move out the authorization to another class.
I have used this across multiple Rails/Rails-API projects and didn't encounter a problem so far.
Instead of writing the code above. You can do this instead.
Also, prioritize early returns over nested ifs for readability.
In your controller.
class UsersController < ApplicationController
def index
authorize User # This will call the policy that matches this controller since this is UsersController it will call `UserPolicy`
#users = User.all
render :json => #users
end
def show
#user = User.find_by :id => params[:id] # Instead of using exists which query the data from db then finding it again, you can use find_by which will return nil if no records found.
if #user.blank?
return render :json => {:message => 'User not found.'}, :status => 404
end
authorize #user # This will call the policy that matches this controller since this is UsersController it will call `UserPolicy`
render :json => #user
end
end
In your Policy
class UserPolicy < ApplicationPolicy
def index?
#user.admin? # The policy is called in controller then this will check if the user is admin if not it will raise Pundit::NotAuthorizedError
end
def show?
#user.admin? || #record == #user # The policy is called in controller then this will check if the user is admin or the user is the same as the record he is accessing if not it will raise Pundit::NotAuthorizedError
end
end
In your ApplicationController
class ApplicationController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, :with => :show_forbidden
private
def show_forbidden exception
return render :json => {
:message => 'You are not authorized to perform this action.'
}, :status => 403
end
end

Related

Devise: restricting page access using user attributes

Relatively new to rails, I've got a simple web app using Devise for user authentication. One attribute is an :admin boolean, set nil for most users, and I will change to true manually in the console for the few users who will need to have administrative access.
My question is: How should I restrict access to a particular page to those who have admin access marked as true?
I've attempted some of that logic in my pages_controller, but it doesn't seem to redirect me as desired (referring to the user_list section):
class PagesController < ApplicationController
before_action :authenticate_user!, :except => [:welcome]
def welcome
#code removed for brevity's sake
end
def dashboard
#ditto
end
def user_list
unless
current_user.admin == true
redirect_to pages_dashboard_path
else
#users = Users.all
end
end
end
Any suggestions on my goal of redirecting or otherwise restricting access to my user_list page would be greatly appreciated.
in your controller you can do something like this
class PagesController < ApplicationController
...
def user_list
if current_user.admin == true
#users = Users.all
else
render :not_an_admin
end
end
end
You can not send them to the same page that they dont have access
You can choose to render a new view
In your user_list method, model name should be singular.
def user_list
unless
current_user.admin == true
redirect_to pages_dashboard_path
else
#users = User.all
end
end

One action in Rails controller sends no data back

So I have these two actions in my RelationshipController:
class RelationshipsController < ApplicationController
before_filter :authenticate_user!
respond_to :json
def create
user = User.find(params[:user_to_follow_id])
relationship = current_user.follow!(user)
respond_with user
end
def destroy
user = User.find(params[:id])
current_user.unfollow!(user)
logger.debug user.id
respond_with user
end
end
Now when I use the create action, I get a response back with the respond_with user method which is also nicely formatted with ActiveModel::Serializer.
When I use the destroy method, I get a response back which is completely blank. It does seems to work with render json: {user: user, status: 201} but I am wondering why am I not able to use respond_with for both of them? The user they send back has the same type of data in both cases.
Unfollow method:
def unfollow!(user)
$redis.multi do
$redis.srem(self.redis_key(:following), user.id)
$redis.srem(user.redis_key(:followers), self.id)
end
end
Looks like you're bumping into the default response for action_controller / responder.rb
# File actionpack/lib/action_controller/metal/responder.rb, line 203
def api_behavior(error)
raise error unless resourceful?
raise MissingRenderer.new(format) unless has_renderer?
if get?
display resource
elsif post?
display resource, :status => :created, :location => api_location
else
head :no_content
end
end
If you're responding to a get or post, then some content is expected back, otherwise just a head and no content. By default, you just deleted a relationship, so there's nothing to return. If you want to send back a user in response to a relationship deletion, then I think you'll have to craft your own response (your render son statement).

Rails 3.2 Fetching Objects in application controller before filter

I am trying to use a _before_filter_ in my ApplicationController to fetch a user object matching a user_id in a http parameter like:
before_filter :fetch_user
def fetch_user
if params[:user_id].present?
#user = User.find(params[:user_id])
end
rescue ActiveRecord::RecordNotFound
# user not found
end
This is working for me in all controllers which inherit from ApplicationController except the controller which is called UsersController in which #user seems to be set to nil in some way.
What could be the reason for this behavior? And is this the standard behavior? How to avoid/disable it?
UPDATE:
I always pass a parameter called user_id to the controller.
If I include the exact same before filter directly into the UsersController it is working!
there are no other before filters in the UsersController
UPDATE 2:
Route which is use to users controller
match ':user_id' => 'users#show'
User Controller code:
def show
if #user
render :text => "user not nil"
else
render :text => "user nil"
end
end
There is definitely a user with the id passed at the user_id parameter because
Thanks for your help!
point.1 in UsersController, you get params[:id] as user_id
point.2 use find_by_id to avoid rescue ActiveRecord::RecordNotFound, it safely returns nil
and so...
def fetch_user
user_id = controller_name=='users' ? params[:id] : params[:user_id]
#user ||= User.find_by_id user_id
end

How to properly validate a user before displaying page in Rails

In my application, I store the user's ID in session[]. At the beginning of every controller action, I'm calling a method defined in the ApplicationController called current_user:
def current_user
#current_user ||= session[:current_user_id] &&
User.find_by_id(session[:current_user_id])
end
At the beginning of my controllers' methods, I have the following:
#current_user = current_user
if #current_user == nil
redirect_to :home
return
end
This is obviously repetitive code and should be a method somewhere. I read the answer for this question, and tried putting my method into a parent class that my controller classes now descend from, however it seems like I can't redirect from that method now.
In my parent class, I have:
def verify_user
user = current_user
if user == nil
redirect_to "/"
return
end
return user
end
And now I've changed my controller methods to this:
#current_user = verify_user
This doesn't work, and I think I know why. For one, I can't simply call return in my verify_user method, as that obviously will just return to the controller. The redirect doesn't seem to have any affect, probably because format.html is being called after the redirect call, which was the reason for the return in the original code.
So, what am I doing wrong here, and what suggestion do you have to solve it? Is this the wrong approach? My main goal is to keep the entire "check if user is logged in otherwise redirect" to one line of code per controller method.
Take a look at the devise gem https://github.com/plataformatec/devise. It handles a lot of this basic user authentication logic for you. This specific problem can we solved by adding before_filter :authenticate_user! to the controllers or actions that need to be guarded.
Add the following logic to the ApplicationController class:
class ApplicationController < ActionController::Base
def current_user
...
end
def logged_in?
current_user.present?
end
def require_user
return true if logged_in?
render_error_message("You must be logged in to access this page",
new_user_session_url)
return false
end
def render_message message
respond_to do |format|
format.html {
if request.xhr?
render(:text => message, :status => :unprocessable_entity)
else
redirect_to(root_url, :notice => message)
end
}
format.json { render :json => message, :status => :unprocessable_entity }
format.xml { render :xml => message, :status => :unprocessable_entity }
end
end
end
Now add a before_filter to your controller:
class OrdersController < ApplicationController
before_filter :require_user
end

Passing parameters into model

Rails 3.0.3
ruby 1.9.2p0
The Problem:
I have a Users table which has many items, the item(s) in turn therefore belongs to the Users.
In my model item.rb i attempt to save the item along with the value for the user.id so i have:
self.User_ID = #user.id
this however give me the error
Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
this is causing some confusion that it can't find this as in the show.html.erb that 'wraps' this page <%= #user.id %> displays the correct ID on the page
Many thanks in advance
** EDIT **
The Shorten action is the action upon which i want to parameter to be passed
class ItemsController < ApplicationController
def redirect
#item = Item.find_by_shortened(params[:shortened])
if #item
#redirect_to #item.original
redirect_to #item.original
else
redirect_to :shorten
end
end
def shorten
#host = request.host_with_port
#user = current_user
You need to load the #user model in every action that will require access to it. Having it render properly in the show action will not guarantee it is loaded in the update action.
Usually you need to have something like this in your controller:
class UsersController < ApplicationController
before_filter :load_user, :except => [ :index, :new, :create ]
# ...
protected
def load_user
#user = User.find(params[:user_id] || params[:id])
rescue ActiveRecord::RecordNotFound
render(:text => 'Record not found')
end
end

Resources