I want to design an API in Rails that requires actions like Create, Update and Delete to be readonly for certain controllers, and open to the public for others (eg, comments on an article should be open but editing that article should require API authentication)
I know how to do the authentication part, what I don't know how to do is the "read only" part or the "you have permission to create a comment but not delete it" part.
Does any one have any resources, tips, tricks or github repositories that do this or something similar to this?
You are needing to do authorization. Look at Pundit for a scalable solution https://github.com/elabs/pundit
I had an app for a while that only needed a little bit of control as there were only a few methods on 2 controllers that were limited. For those i just created a before_filter and method to control the authorization.
The code below would allow everyone to do index and only allow users with a role attribute that has a value of "admin" to do any other action in the controller. You can also opt to raise an unauthorized error or raise an error message instead of redirecting. There are articles (probably books) written on the security side of the house for whether you should give users notice if they are not authorized to do something (which means they can infer that there is something there that someone can do at the uri)
SomeController < ApplicationController
before_filter check_authorized, except [:index]
def index
....stuff that everyone can do
end
def delete
....stuff only admin can do
end
private
def check_authorized
redirect_to root_path unless current_user.admin?
end
end
Of course you will need devise or a current_user method and a method on user that checks admin
class User < ActiveRecord::Base
def admin?
if self.role == "admin"
true
else
false
end
end
end
Related
I'm building a rails app, and so far I've set up a pretty basic user registration/login system, mainly by following this railcast I found thanks to stack overflow. I've left everything the same as the railcast, only used strong parameters instead of attr_accessible and added some additional fields (username, bio, img url) to the table.
Now I want my app to redirect users onto my login page if they're not logged in, no matter what page they try to access, and if they are, then redirect to the normal path. My loging page is currently my root_path. Do I need to do this in all the controllers separately or can I just write this into my appController? How would I go about writing the controller? I was thinking something like this:
if session[:user_id] == nil
redirect_to login_path
else
redirect_to current_controller_path
end
Now how do I check if user is logged in, and how do I redirect to current controller path (for instance articles_index_path?
I am new to ruby on rails, and still trying to wrap my head around models, views and controllers, so please assume I know nothing when writing up explanations :) Thanks for the help
Oh I'm using Rails 4 with ruby 2.2.1
You need to add a before_filter in your ApplicationController to check user's authentication.
class ApplicationController < ActionController::Base
...
before_filter :authenticate_user!
...
private
def authenticate_user!
redirect_to login_path unless session[:user_id]
end
end
Now it will make sure that user should be logged in for accessing any action of any controller, including signup, signin and other actions which should be accessible to non-logged in users too.
You need to make sure that you skip above before_filter where you don't want user to be logged in such as signup, signin, about us, contact us etc actions like this.
For Example:
class SessionsController < ApplicationController
skip_before_filter :authenticate_user!, :except => :destroy
...
def new
...
end
def create
...
end
...
end
You can read more about skip_before_filter on APIDock
My rails app has a few cab operators and they have a few cabs associated with them, and they are related as follows:
class Operator < ActiveRecord::Base
has_many :cabs
end
I have used Devise as my authentication gem. It authenticates users, admins and super admins in my app. I have created separate models for users, admins and super admins (and have not assigned roles to users per se).
I now wish to add the authorization feature to the app, so that an admin (who essentially would be the cab operator in my case) can CRUD only its own cabs. For e.g., an admins belonging to operator# 2 can access only the link: http://localhost:3000/operators/2/cabs and not the link: http://localhost:3000/operators/3/cabs.
My admin model already has an operator_id that associates it to an operator when an admin signs_up. I tried to add the authorization feature through CanCan, but I am unable to configure CanCan to provide restriction such as the one exemplified above.
I also tried to extend my authentication feature in the cabs_controller, as follows:
class CabsController < ApplicationController
before_action :authenticate_admin!
def index
if current_admin.operator_id != params[:operator_id]
redirect_to new_admin_session_path, notice: "Unauthorized access!"
else
#operator = Operator.find(params[:operator_id])
#cabs = Operator.find(params[:operator_id]).cabs
end
end
But this redirects me to the root_path even if the operator_id of the current_admin is equal to the params[:operator_id]. How should I proceed?
EDIT:
Following is my routes.rb file:
Rails.application.routes.draw do
devise_for :super_admins
devise_for :users
resources :operators do
resources :cabs
end
scope "operators/:operator_id" do
devise_for :admins
end
end
I have three tables: users, admins and super_admins. I created these coz I wanted my admins to hold operator_ids so that the admins corresponding to an operator can be identified. Also, I wanted the admin sign_in paths to be of the type /operators/:operator_id/admins/sign_in, hence the tweak in the routes file.
Unfortunately, initially I didn't understand that you actually have 3 different tables for users and (super)admins... Not sure that Pundit can help you in this case, but I'll keep the old answer for future visitors.
Coming back to your problem, let's try to fix just the unexpected redirect.
Routes seems fine, so the problem can be one of this:
You're getting redirected because you're currently not logged in as an admin, so you don't pass the :authenticate_admin! before_action.
You say "even if the operator_id of the current_admin is equal to the params[:operator_id]", but this condition is probably not true. Can you debug or print somewhere the value of both current_admin.operator_id and params[:operator_id] to see if they're actually equals?
Another interesting thing, is that you have a redirect for new_admin_session_path in your code, but then you say "this redirects me to the root_path". Can you please double check this?
OLD ANSWER
If you want to setup a good authorization-logic layer, I advice you to use pundit.
You've probably heard about cancan, but it's not supported anymore...
Leave Devise managing only the authentication part and give it a try ;)
PUNDIT EXAMPLE
First of all, follow pundit installation steps to create the app/policies folder and the base ApplicationPolicy class.
Then, in your case, you'll need to create a CabPolicy class in that folder:
class CabPolicy < ApplicationPolicy
def update?
user.is_super_admin? or user.cabs.include?(record)
end
end
This is an example for the update action. The update? function have to return true if the user has the authorisation to update the cab (You'll see later WHICH cab), false otherwise. So, what I'm saying here is "if the user is a super_admin (is_super_admin? is a placeholder function, use your own) is enough to return true, otherwise check if the record (which is the cab your checking) is included in the cabs association of your user".
You could also use record.operator_id == record.id, but I'm not sure the association for cab is belongs_to :operator. Keep in mind that in CabPolicy, record is a Cab object, and user is the devise current_user, so implement the check that you prefer.
Next, in your controller, you just need to add a line in your update function:
def update
#cab = Cab.find(params[:id]) # this will change based on your implementation
authorize #cab # this will call CabPolicy#update? passing current_user and #cab as user and record
#cab.update(cab_params)
end
If you want to make things even better, I recommend you to use a before_action
class CabsController < ApplicationController
before_action :set_cab, only: [:show, :update, :delete]
def update
#cab.update(cab_params)
end
#def delete and show...
private
def set_cab
#cab = Cab.find(params[:id])
authorize #cab
end
And of course, remember to define also show? and delete? methods in your CabPolicy.
I am still fairly new to rails.
In my jobs/index.html.erb file I currently have a conditional as follows:
<% if current_user.admin? %>
<td><%= link_to 'Edit', edit_job_path(job) %></td>
<%end%>
Now although the editing is forbidden on the employee side, because it clearly states "if current_user_admin" however if an employee were to login and type localhost:3000/jobs/1/edit they are somehow granted access to change the file.
How can I block the user who is NOT an admin from having the ability to do this?
Any suggestions would be greatly appreciated.
You're preventing them from seeing the link, you need to prevent them from accessing the feature. Add code to your controller that checks their status and prevents unauthorized users from doing things they shouldn't.
class JobsController
def edit
if !current_user.admin?
redirect_to '/'
return
end
// Old code here
end
end
If you are going to have a complex set of permissions, consider using a gem like Devise or Cancan. I don't have experience with those, but they seem to be the standards for authorization in rails.
If you want to forbid access to some parts of your application the right place to do it is in controller, not in views (hiding links doesn't work as you see). The common solution is to define before_filter (http://apidock.com/rails/ActionController/Filters/ClassMethods/before_filter) in your controller.
In your particular case this should work
class JobsController do
before_filter :authorize!, only: [:edit,:update]
#CRUD below
def authorize!
redirect_to(:back) unless current_user.admin?
end
end
You could also add message to flash before redirecting to let user know what's going on.
def authorize!
redirect_to(:back, alert: "YOu are not allowed to do it") unless current_user.admin?
end
also adding status: :403 would be nice in case building api (403 is knows as forbidden response)
I would suggest you to use cancancan gem to define abilities for different types of users.
Another option is to make a redirecton in edit view for unauthorized users
Don't do this in the view - it's just UI. You can put a test directly in the controller:
class JobsController < ApplicationController
def edit
raise ActionController::RoutingError.new('Not Found') unless current_user.admin?
end
def update
raise ActionController::RoutingError.new('Not Found') unless current_user.admin?
end
end
I use a 404 (not found) but you may prefer a 403 (not authorized), depending if you want the user to know there is something there that he has no access to.
You have protected the display of the link, which is good, so the benign user won't be surprised when it doesn't work. But as you point out, for the amlignant user, this is trivial to bypass. You need to add a guard in the destination controller as well.
But you really should check out some gems that provide exactly this type of security. Cancan seems to be the most widely used.
I have a scaffold Finances and I just realized that it can be edited by any logged in user by going to /finances/1/edit
I have installed activ_admin gem but I don't think it is what I need. How to make sure other than admin (or may be some users) no one can edit finances resource type- I
EDIT - I found https://github.com/EppO/rolify, is this best option or I still can do something better as it may be overkill ?
EDIT 1 - I went through this https://github.com/EppO/rolify/wiki/Tutorial and have assigned role "admin" to user = User.find(1), everything went well upto "ability.can? :manage, :all" in console, which shows TRUE for user 1 and false for other users. Now I am not able to figure out what to do ? I can still see all users being able to edit the page even though I have added "resourcify" in the finance.rb model. Any help ?
Well, I personally use rolify for my project and love it.. but to be honest this is super easy to achieve by simply adding a column "admin" to your User model and having it default to false. When you want a user to be an admin update the attribute to true and then require the User.admin==true to access the finances edit action... You can do this by redirecting the non-admin user from the controller (within the finances edit action)
By the way if you're using devise for auth check out Devise before_filter authenticate_admin?
I'm not sure how your models are set up, but lets say your User model has an admin column, you can do the following:
FinancesController < ApplicationController
before_filter :must_be_admin, only: :edit
def edit
...
end
private
def must_be_admin
unless current_user && current_user.admin?
redirect_to root_path, notice: "Some message"
end
end
end
You can add any actions needed to the before filter, e.g. before_filter :must_be_admin, only: [:edit, :destroy]
If you're looking to add sensible user authorization without rolling your own solution, definitely check out CanCan. Also helpful is this screencast by its author, Ryan Bates.
A have a bunch of controllers with the Admin namespace. I want to restrict access to these unless the user is an admin. Is there a way to do this using CanCan without having to call unauthorized! in every method of every controller?
Add an application controller to your namespace and a before filter to it.
class ApplicationController < ActionController::Base
end
class Admin::ApplicationController < ApplicationController
# these goes in your namespace admin folder
before_filter :check_authorized
def check_authorized
redirect_to root_path unless can? :admin, :all
end
end
class SomeadminController < Admin::ApplicationController
def some_action
# do_stuff
end
end
The Admin Namespaces wiki page for CanCan lists out several solutions to this problem.
As #mark suggested, have a base controller for admins which checks authorization for every action.
You may not need to use CanCan at all for this if all you require is to check that users have an admin
flag.
For handling admins differently from each other (as opposed to differently from regular users only),
consider a separate AdminAbility class (this is a little off-topic, but could prove relevant).
now rails_admin has full support with Cancan, you can find it in its official website, there is a wiki page for this topic:
Rails Admin's authorization with CanCan: