I'm just getting started in rails and I'm stuck with a little problem.
Imagine you have to Types of Users: Client and Admin. Both are stored in the same User Model with an boolean (admin=true/false). That's how I differentiate between those. So now there are Projects. These are just posts with multiple attributes like Name, Description, Category...
I want my admins to see all the Projects and he should have the ability to edit those project but at the same time I want to show the Client User only the Projects he is assigned to and he shouldn't be able to edit those.
So concluding: Admin -> See all posts and be able to edit and assign Clients to them
Client -> See Projects he is assigned to but not able to edit those. He should only see the ones he is assigned to.
How do I do it?
Make a Project belong to a User. That entails:
generate a migration to add the DB column (run this command):
$ rails g migration AddUserToProjects user:references
add the association to your Project model (app/models/project.rb):
belongs_to :user
add the association to your User model (app/models/user.rb):
has_many :projects
In your Projects controller, you want to have actions for the following:
a list of projects for a user
an admin's list of all projects
Those could look like this (app/controllers/projects_controller.rb):
class ProjectsController < ApplicationController
before_action :authenticate_user!
before_action :verify_admin_status, :only => [:admin_list]
def user_list
#projects = current_user.projects
end
def admin_list
#projects = Project.all
end
private
def verify_admin_status
unless current_user.admin
raise ActionController::RoutingError.new('Not Found') # or whatever other error you want, i.e. 403
end
end
end
Add the routes to your config/routes.rb file:
get 'projects/mine', :to => 'projects#user_list'
get 'projects/admin', :to => 'projects#admin_list'
Take a similar approach for defining the edit actions: check that the user who's trying to access it is an admin by using the verify_admin_status callback, then select (or update) the Project record.
check in your views and controller actions
if current_user.admin
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 using devise for authentication and have an association between users (has_many :products) and products model (belongs_to :user).
My routes file is
resources :users do
resources :products
end
Now what happens is, user with id 3 at /users/3/products can also see whats at /users/4/products. I want to restrict that. I dont want /users/3/products to able to see whats at /users/4/products and so on (not specific to these two users but for all). How do I do it ? Should I have a Users Controller? I dont have it right now. If i have the controller, how do I do it? I was thinking maybe redirect it?
thanks
You could add a before_filter in your products controller:
class ProductsController < ApplicationController
before_filter :user_is_current_user
...
private
def user_is_current_user
unless current_user.id == params[:user_id]
flash[:notice] = "You may only view your own products."
redirect_to root_path
end
end
end
Also, in the products controller you could retrieve only products belonging to the current_user:
def index
#products = current_user.products # will fetch all products with a user_id matching the current user's
end
If you used the above you wouldn't really need a user's ID in the URL, you could use a path like /users/products or just /products.
I have a User model that has_many projects. The Project model belongs_to the User model. I am currently using the projects_controller.rb index action to display all of the projects that have been created across all users (Project.all).
On a separate page, I would also like a way to display all of the projects that belong to a specific user (i.e. go to page and be able to see all of the projects that belong to a given user).
I am having difficulty figuring out which controller/action/view to use and how to set up the routes because I am already used the index action for the projects_controller for the purpose of displaying all of the projects. Does anybody have any suggestions?
You could do /users/{:id}/projects, which would map to the users controller projects action. The route would have to be custom member action
resources :users do
member do
get 'projects'
end
end
rather than having different pages of listing. use same index page based on different criterias i.e. filters.
URL
match ":filter/projects", :to => "projects#index"
inside controller something like
case params[:filter]
when "all"
#projects = Project.all
when "user"
#projects = current_user.projects
when "batch"
# ..
else
# ..
end
How about you display the projects that belong to a particular user on the User#show page?
Something like this perhaps:
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
end
# rest of class omitted...
Then you can access the projects that belong to the user in the view for the show page by calling #user.projects.
You should nest projects under users to get working code/paths like: /users/1/projects without any additional coding, so you need to change your resource lines in routes.rb to:
resources :users, :shallow => true do
resources :projects
end
and then under Projects#show action instead of Project.find(params[:id]) you need to get Project.find(params[:user_id])
That's seems to be correct
I have three models: users, organization, and projects.
Organization has_many users (with different roles) and users belong_to organization.
Users has_many projects, and projects belong_to both users and organizations (by putting #project.organization_id = current_user.organization_id in the def create method of the controller).
I put this bit in my projects_controller.rb file to make sure that users who are logged in can only view the projects that are associated with their organization.
def index
#projects = Project.where(organization_id:current_user.organization_id)
end
However, all this does is hide the other projects from the current_user. If he types in http://localhost:3000/projects/3 (a project that belongs to another user from another organization) he's still able to access it.
What should I put in the def show part of projects_controller to stop this from happening? I've hacked around a couple of things but I can't get it right without any errors. Also I'd like to not use CanCan if possible.
I tried putting a
before_filter :require_project_belong_to_organization, :only => [:show]
def require_project_belong_to_organization
#project = current_user.projects.find(params[:id])
end
but this is only returning a result if it was the user that created it. I need other users to be able to view it too, as long as they're in the same organization.
Try cancan.
Use a before_filter in you controller and check if the project's user is the same as the current_user
I would first make a function authorized? in the project model. Make sure that current_user is available to the model.
authorized?
self.organization_id == current_user.organization_id
end
Then in the controller:
def show
#project = Project.find(params[:id])
if #project
unless #project.authorized?
flash[:danger] = 'not authorized'
redirect_to....
end
else
flash.now[:danger] = 'Project was not found'
end
end
In my "routes.rb" file I have the following line:
resource :users
which gives me a bunch of named routes for accessing my User model in a RESTful manner.
Now, I've made some additions to the User model including creating a special class of user. These are still stored in the User model but there is a "special" flag in the database that identifies them as special.
So, is it possible to create special_users resource? For example, I'd like to have a "special_users_path" as a named route to "/special_users" which will return an index of only the special users when you perform a GET on the URL.
Is there a way to do this?
In Rails routing, a 'resource' refers to the standard 7 routes that are created for RESTful resources: index, show, new, create, edit, update and destroy. Normally that is enough, but sometimes you might want to create another action.
In the model, you want to create a scope that only returns special users:
class User < ActiveRecord::Base
scope :special, where(:special => true)
end
On the controller side, there are two ways to go about this. What you are suggesting is the creation of an additional action:
match "/users/special" => "users#special"
resource :users
In the controller, your special action would return the scope you just created:
class UsersController < ApplicationController
def special
#users = User.special
end
end
That will do what you ask, but I would suggest NOT doing it this way. What if you add other flags later that you want to search by? What if you want to search by multiple flags? This solution isn't flexible enough for that. Instead, keep the routes the way they are:
resource :users
and just add an additional line to your controller:
class UsersController < ApplicationController
def index
#users = User.all
#users = #users.special if params[:special]
end
end
and now, when you want to display special users, simply direct the user to /users?special=true
This approach is much more future-proof, IMO.
(This answer is assuming Rails-3. If you're still using 2.3 let me know)
You could set the special_users as a resource:
resource :special_users
If you need to point it to a special controller, you could specify it with:
resource :special_users, :controller => :users
But I would really suggest you to not creating another controller for retrieving a kind of user, but using a param to get them:
class UsersController < ApplicationController
def index
users = case params[:type].to_s
when "special"
User.special_users # Using named scopes
else
User.all
end
end
end
When you use the users_path to call the special users:
users_path(:type => :special)