This might be a bit of a noob question, but I'm asking anyway.
So I'm building an app where people make posts. So it's a social network.
But I don't want people to be able to edit and delete other's posts.
I don't think a role-based system would work here, because people only administrate their own posts only.
I was thinking some sort of AR association, but I don't know if that would work.
What I want is something like this for my app/models/ability.rb:
class Ability
def initialize(user)
if current_user.username == #post.username
can :edit, Post
can :destroy, Post
end
end
end
How would I go about doing this (assuming the models are User and Post)?
So basically should I do a User has Posts, or User has and belongs to Posts?
Use this
class Ability
def initialize(user)
can [:edit, :destroy], Post do |post|
post.try(:user) == user
end
end
end
The problem is that class Ability won't have access to the #post instance variable. So I think in Ability.rb you would have to say that users can :manage, Post so they can create, edit and destroy Post objects. Then it's up to your controller and model layers to ensure that they can only edit and destroy their own Posts.
With CanCan you can call load_and_authorize_resource at the top of a controller to protect the entire thing, but in your case your PostsController will probably need to protect at the action level. Calling authorize! :manage, #post in the create, destroy, edit and update actions, for example, along with checking to make sure that the current_user.username == #post.username, can ensure that people can modify only their own posts.
Finally, instead of actually deleting posts, you may want users to instead "soft-delete" by simply marking a Post as deleted. In this case, you would explicitly authorize the creation and editing of Posts, but you would not authorize them to actually destroy Posts. In your index and show actions, you would ensure that Posts marked as deleted were not shown.
As to role-based systems, down the road you may want a group of moderators or to add on another administrator. That's when you'll want a role-based system. I almost always make mine role-based to start with, even if it's just to have admin and user roles.
Related
I have typical RESTful routes for a user:
/user/:id
/user/:id/edit
/user/:id/newsfeed
However the /user/:id/edit route can only be accessed when the id equals the current_user's id. As I only want the current_user to have access to edit its profile. I don't want other users able to edit profiles that don't belong to them.
What is typically the best practice to handle this situation?
Should I leave the route as is, and thrw an error if the current_user.id != param[:id], forcing the front end client calling the api to track the logged in user's id?
Should I make a special route /user/self/edit and in the controller check to see if param[:id] == 'self'?
I would've added special routes for current user profile actions, in this case you don't have to check anything. Just load and display the data of current user. For example:
/my-profile/edit
/my-profile/newsfeed
It's not that RESTful but you don't have to put extra checks keeping your code clean.
If you still have to have (or want to have) a strict RESTful routes then I would use a before_filter and check if the id = current_user.id. If not then return 401 or 403.
I only want the current_user to have access to edit its profile. I
don't want other users able to edit profiles that don't belong to
them.
What I suggest is to use some authorization gems like pundit
Sample code:
class UserPolicy
attr_reader :current_user, :model
def initialize(current_user, model)
#current_user = current_user
#user = model
end
def edit?
#current_user == #user
end
end
Also with an authentication gem like Devise, only the current_user(the users who logged in) can only access and edit their profiles
I would say that you are doing it correctly, just keep your current route as it is right now. And what you should do is to add a restriction in your controller instead. I would assume that you are using Rails, and working on users_controller.
class UsersController < ApplicationController::Base
def edit
if current_user.id == params[:id]
# do your work
else
render :404
end
end
end
Or you could clean up your controller by moving the restriction into a callback instead:
class UsersController < ApplicationController::Base
before_filter :restrict_user, only: [:edit]
def edit
# do your work
end
private
def restrict_user
render :404 unless current_user.id == params[:id]
end
end
You can add the gem "cancancan" and after the initialize....
class Ability
include CanCan::Ability
def initialize(user)
can :update, User do |user|
user.id == params[:id]
end
end
end
Then add this authorize! :edit, #user to your update action
You're going to need to add authorization code in all the user_controller methods as another comment suggested. Usually what I do in apps where a user is only supposed to edit their own profile I add a /profile route for a user to edit their own profile and then on the main /users/:id/* routes I add logic to prevent non-admin users from accessing those routes.
User is able to view his profile /users/1 or edit his profile /users/1/edit. From users perspective this URLs are absolutely fine.
There is no links which may lead user to edit the another user. You are trying to cover the different situation: when someone manually trying to craft the URL and get access to another account. I would not call them hackers, but technically they are – users who are trying to exploit your website to pass the restrictions.
You don't have to worry about "hackers" convenience. I'm always use current_user in edit action so nobody can edit wrong profile whatever his profile is.
def edit
#user = current_user
end
Also, I need to mention that you should also cover update action with such checks. With edit you may only get data (and probably only wide-public open data, unless you put billing information or plain-text-passwords inside your edit template). But with update you can actually change the data, which may be more destructive.
Because it seems that the only available user resource should be the authenticated user, I think the best way to solve this is
GET /user
PUT /user
GET /user/newsfeed
If you like to extend the api usage in future so that one user could have access to other user resources, than you need a solution that includes the user ids. Here it makes sense to introduce the routes for "self", too. But then you also have to implement an access check on server side.
GET /user/id/:id
PUT /user/id/:id
GET /user/id/:id/newsfeed
GET /user/self
PUT /user/self
GET /user/self/newsfeed
But I think you should keep it as simple as possible
For further investigations I would propose books like http://apigee.com/about/resources/ebooks/web-api-design which give a good introduction into API design
Since you only care to provide RESTful endpoints only for the currently authenticated user, which is available in your controllers as current_user, i say you don't need the id identifier parameter. I suggest using the following routes:
GET /user => users#show
PUT/PATCH /user => users#update
GET /user/edit => users#edit
You should keep the url as it is. Authentication and Authorization are separate concerns. 'current_user' refers to the user who is authenticated to access the apis. The id in the url identifies the resource on which 'current_user' is working, so does he have access to that resource or not is the concern of authorization. So you should add current_user.id != param[:id] (as you mentioned) in your api permissions and throw 403 status code in response.
You should use this route:
PUT /user/me
Note that there is no need for "edit": you should use the PUT method instead.
Also, you should explicitly define the route I've written above, instead of checking if id == 'self'.
I have a question about good practice. I am about to implement (just started) Pundit for my authorisation. However, in my controllers, say Projects and Stages I am associating all of my calls using current_user. e.g.
class ProjectsController < ApplicationController
before_action :authenticate_user!
before_action :set_project, only: [:show, :edit, :update, :destroy]
# GET /projects
# GET /projects.json
def index
#projects = current_user.Projects.all
authorize #projects
end
# GET /projects/1
# GET /projects/1.json
def show
end
# GET /projects/1/edit
def edit
end
private
# Use callbacks to share common setup or constraints between actions.
def set_project
#project = current_user.projects.find(params[:id])
authorize #project
end
# Never trust parameters from the scary internet, only allow the white list through.
def project_params
params[:title, :description]
end
end
I will have many more associations that are associated to a user (or many users). Is the above a good method to ensure that users can only view projects that they are allowed to? And is an authorisation 'gem' therefore not required?
Thanks in advance
Rudy
The short answer: yes, what you're doing with Devise's current_user is fine. As for an authorisation gem, you may or may not still need one.
The longer answer:
It's first worth briefly clarifying the difference between authentication and authorisation.
authentication is about saying "is this user who they say they are?". If I log in with the username 'bob', and also am able to correctly enter bob's password, then your app (via the help of devise) is able to say "OK, yep, I believe this user is who they say they are - I believe that it is in fact Bob on the end of the keyboard". That is to say, Bob is authenticated.
authorisation is about saying "is this user allowed to do what they are trying to do". For example, Bob may be able to create new projects, but he might not be allowed to delete projects. That is to say, Bob is authorised to create projects, and is not authorised to delete them.
OK. Now, authentication allows you, in and of itself, to perform some level of authorisation, because you can say "logged in users can view projects, but logged out users can't". Or you may want to say, as you are, "logged in users can view their own projects (but no one elses), but logged out users can't view any projects".
This last scenario is referred to as "scoping" by the logged in user, and the way you're doing it (using Devise's current_user) is just fine.
The last thing is the question of wether or not you need an authentication gem, in addition to the authorisation provided by Devise. If all you need to do is distinguish between logged in users and non-logged in users, then Devise should be all you need, and there's no need for an authentication gem.
If, on the other hand, you need more fine-grained authentication (for example, logged in users who are also admins can delete their own projects, but logged in users who are not admins can only view their own projects, and cannot delete them), then you'll likely want an authorisation gem, in addition to Devise.
Yes. It's ok if you don't need any advanced control features (like read-only).
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'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.
I know there's probably solutions to this elsewhere, but I'm looking for help that works specifically in my case because I'm having a lot of trouble translating other solutions into my situation.
I currently have a device set up and the database is seeded so an admin is already created. Everyone else that signs up after that is a user.
There are two tables right now, a user table generated by rails and a cadet table. The cadet table stores information such as company, room number, class year and such.
My question is, how do I allow a user to edit/destroy only the cadet record that they've created? I know it seems like a big question but I've been looking all over and still can't find a reasonable way to implement this. Thank you!
Devise is related to authentication (who you are), you need a solution for authorization (who can do what). My suggestion is to go for CanCan (https://github.com/ryanb/cancan), which is a gem very widely use together wide Devise.
For your example, and after install the gem via Gemfile+Bundler:
Initialize the gem for your User model
rails g cancan:ability
it will create a file in app/models/ability.rb to define your restrictions
Define your restrictions, for instance:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (this line it to manage users not logged in yet)
if user
can :manage, Cadet, user_id: user.id
end
end
end
That will allow a user just to read, create, edit and destroy Cadets which user_id matches the id for the User.
Take a look at CanCan github page is wery well documented and with lot of examples; it's very simple to set up and works great.
You can also use a before_filter, something like the following:
class CadetsController < ApplicationController
before_filter :cadet_belongs_to_user, only: [:show, :edit, :update, :destroy]
....
private
def cadet_belongs_to_user
# following will work only on routes with an ID param
# but there are a few ways you could check if the cadet
# belongs to the current user
unless current_user && current_user.cadets.where(id: params[:id]).any?
flash[:notice] = "You are not authorized to view this page."
redirect_to root_path
end
end
end