I'm using devise with my rails 4 app to handle the authentication, and no problems there.
However, I want to make sure a logged in user can only view / edit (via the show and update actions) the items that his user owns (that are linked to his user_id).
I think I could hack something to make this all work by checking the current_user.id, but many users in Stackoverflow and other places say to use cancan-- however it appears cancan is dead and gone, and there's a replacement called cancancan, which may be ok, but I don't know.
Is there a standard way to do this in Rails 4, or is the best route to still use a third party gem like cancancan? Is there a better gem?
I've been using Pundit instead of Cancan for the last few projects I've done. It is lightweight, flexible and easy to use. Here's the link: https://github.com/elabs/pundit
In regards to your question, you will create policies for each model. For each action you define a method. It's super simple and explained on the link I've attached. Here as an example you have update in your model (models/post.rb):
def update
#post = Post.find(params[:id])
authorize #post
if #post.update(post_params)
redirect_to #post
else
render :edit
end
end
Call authorize to define permissions.
In your policies/post.rb:
class PostPolicy < Struct.new(:user, :post)
def update?
user.admin? or not post.published?
end
end
That returns true or false. In your case if you want to check if the user is a owner you can place the following if statement:
if user.admin? || user.owner_of?(post)
You get the idea. You can also define scopes, etc.
I don't think there's a standard, per se, but rather it's based on what you need. For Rails 4, cancancan brings a lot to the table and is built off of a gem that has been used regularly by the Rails community.
The only other alternatives I'm familiar with are protector and pundit - maybe check those out.
However, if cancancan and protector don't fit your needs, you could always roll your own authorization solution, but to me, why reinvent the wheel if cancancan will satisfy your needs.
I'd recommend Action Access, it's much simpler and straightforward. It boils down to this:
class ArticlesController < ApplicationController
let :user, :all
let :guest, [:show, :index]
# ...
def edit
not_authorized! unless #article.user == current_user
# ...
end
# ...
end
First of this will automatically lock the controller, allowing only users to access every action, guests can only show or index articles. Then not_authorized! will reject and redirect with an alert any user other than the owner of the article.
What's good about this is that it makes controllers to be self contained, everything related to the controller is within the controller. This also makes it very modular and avoids leaving forgotten trash anywhere else when you refactor.
It's completely independent of the authentication system (so no problem with Devise) but it bundles a set of handy model additions that allow to do things like:
<% if current_user.can? :edit, :article %>
<%= link_to 'Edit article', edit_article_path(#article) %>
<% end %>
Here :article refers to ArticlesController, so the link will only be displayed if the current user is authorized to access the edit action in ArticlesController. It supports namespaces too.
You can lock controllers by default, customize the redirection path and the alert message, etc. Checkout the documentation for more.
Related
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.
i created a Devise with CanCan integration like told on:
http://starqle.com/articles/rails-3-authentication-and-authorization-with-devise-and-cancan-part-1/
http://starqle.com/articles/rails-3-authentication-and-authorization-with-devise-and-cancan-part-2/
now i have two resources for my User class. Devise and a RESTful resources :users.
as mentioned in the tutorial, i included in the RESTful edit_user_path a form for editing the rights for the user.
now i don't understand how i can restrict normal users to access that edit function and use devise edit function for that.
Is it possible to just restrict a user to
can :manage, User
but he still can manage devise controller?
Solved
Just can add an in ability.rb
can :assign_roles, User
and then in _form for RESTful edit
<% if can? :assign_roles, current_user %>
and then let Users edit either over RESTful _form or Devise form, doesn't matter then
Edit
_form.html.erb (or haml)
<% if can? :assign_roles, #user %>
may work too. depends on your controller. should work better since i have made a bit workaround to fit it to current_user
If you want to be sure that your abilities get checked also on controller level, you should add load_and_authorize_resource to it.
class ProductsController < ActionController::Base
load_and_authorize_resource
end
https://github.com/ryanb/cancan/wiki/authorizing-controller-actions
if you just show / hide the links in the view depending on the <% if can? %> method, a user might still type the direct link to e.g. edit action in the adress bar
I am fairly new to Ruby On Rails and right now I am doing a simple app. In this app a user can create many items and I use devise for authentication. Ofcourse I want to make sure that you are the owner in order to delete items (Teams, Players etc) and the way I do it now is:
def destroy
#team = Team.find(params[:id])
if current_user.id == #team.user_id
#team.destroy
redirect_to(teams_url, :notice => 'The team was deleted.')
else
redirect_to root_path
end
end
Is this the best way? I was thinking about putting a method in the model but I am not sure I can access current_user from there. I was also thinking about a before_filer, something like:
before_filter :check_ownership, :only => [:destroy, :update]
I that case and if I want to code only one method for all objects (all objects this relates to have a "user_id"-field)
In my application controller I put:
before_filter :authorize
def authorize
false # Or use code here to check if user is admin or not
end
Then I override the authorize method in my actual controller to allow access to various actions.
You're looking for an authorization solution on top of your authentication (devise)
You can't access current user in the model no. I've had a fair amount of success using Makandra's Aegis for authorization. It allows you to create roles specify permissions attributed to each role. The docs are pretty good and I know it works fine with Devise, it's pretty agnostic that way, I've also used it with Clearance. It also passes an implicit "current_user" to your permissions so you can specify and check that your current user can take appropriate actions.