I just set up Devise with Cancan for user roles. I think I'm on the right track, but I just ran into a situation where I think I'm missing something small.
I want any user with role :admin to be able to edit the profiles/roles of every other user. I have the routes set up right, but when I click on the links for other users, I get redirected.
_user.html.erb
<% #users.each do |user| %>
<li>
<%= gravatar_for user, size: 52 %>
<%= link_to user.name, user %>
<% if can? :assign_roles, #user %>
| <%= link_to "delete", user, method: :delete, confirm: "Delete user?" %>
| <%= link_to "edit", edit_user_path(user) %>
<% end %>
</li>
<% end %>
users_controller.rb
...
def edit
#user = User.find(params[:id])
end
def update
authorize! :assign_roles, #user if params[:user][:assign_roles]
if #user.update_attributes(params[:user])
flash[:success] = "Profile updated"
sign_in #user
redirect_to #user
else
render 'edit'
end
end
ability.rb
def initialize(user)
can :assign_roles, User if user.admin?
can :manage, :all if user.is? :admin
end
I've been changing this code around all day, I might even be going in circles.
Any help would be greatly appreciated.
I figured it out. Even though I was able to limit the html/css with the logic shown about, I was not able to limit model/controller interaction. I have multiple controllers, and the one I was dealing with had no authentication check. So in other words, I added
before_filter :authenticate_user!
to my users_controller.rb file, and now it knows that I'm an admin, and what that means. I just added this on a whim, but everything I've learned about Devise/Cancan so far is from the wiki:
https://github.com/ryanb/cancan/#wiki-readme
Related
I'm a little new to it, but I'm building a new web app using rails. Most of what I've got so far is based on railstutorial.org. I've only got a few possible user "roles" (basic user, excom, and admin), so I'm just modeling it using a couple boolean fields in the user model.
I'd like my admin users to be able to make other users admin or excom, without having to resort to some full blown user role modeling system.
I don't want admins to be able to modify other user data (like name, email, etc.) or of course allow users to make themselves admin, so adding something like that to the users_controller update method seems cumbersome and error prone. But it also seems like a whole new controller and routes is overkill.
I just want a button for admins to click to "Make user admin" and have it work, but I'm not sure of the "right" way to implement that.
Edit:
The only exposure an admin has at this point, is checking whether a user is an admin in some before_action. I.e.
def admin_user
redirect_to(root_url) unless current_user.admin?
end
or
def correct_user_or_excom_or_admin
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user) || current_user.admin? || current_user.excom?
end
I think what I want is how to define a route such that I can write the following method in the users_controller and include it in the admin_user before_action.
def make_admin
#user = User.find(params[:id])
#user.admin = true
#user.save
flash[:success] = "#{#user.name} is now an Admin"
end
And then be able to include the following in the appropriate view
<%= link_to "Make Admin", user_admin_path(user), method: :post,
data: { confirm: "You sure?" } %>
I think #widjajayd answer is on the right track. Does creating custom routes that way include the user id in the params?
you can create custom route with custom method for admin
inside routes.rb, create 2 routes for new and create just for admin
resources users do
collection {
get :new_admin
put :create_admin
}
end
inside user_controllers.rb, create 2 methods
def new_admin
#user = User.new
# this depending with what system you use devise/bcryt/others
end
def create_admin
#user = User.new(user_params)
#user.role = "Admin"
# this depending with what system you use devise/bcryt/others
end
create view file inside app/users/new_admin.html.erb
<%= form_for #user, url: create_admin_users_path, do |f| %>
# your fields name, password, etc
<% end %>
button availability just for admin user
<% if user.role == admin %>
<%= link_to 'Make User Admin', new_admin_users_path, :class => 'form-control btn btn-info' %>
<% end %>
Edit with additional code if you want to make some user to be an admin
below usually you list user in index.html.erb
<% if #users.any? %>
<table id="table-user" class="table table-striped">
<thead>
<tr>
<th>email</th>
<th>name</th>
<th>Role</th>
<th class="edit"></th>
<th class="destroy"></th>
</tr>
</thead>
<tbody>
<tr>
<% #user.each do |user| %>
<td><%= user.email %></td>
<td><%= user.username %></td>
<td><%= user.role %></td>
<td><%= link_to "Make Admin", create_admin_users_path(user_id: user.id), method: :post,
data: { confirm: "You sure?" } %> </td>
<% end %>
</tbody>
</table>
<% end %>
from form you pass params with hash user_id (it can be any name you want) then inside create controller you get the params with sample below
def create_admin
#user = User.find(params[:user_id])
#user.admin = true
#user.save
flash[:success] = "#{#user.name} is now an Admin"
end
Here's the solution I came up with, with inspiration taken from #widjalayd.
Create the following custom routes.
post '/users/:id/make_admin', to: 'users#make_admin', as: :make_admin
delete '/users/:id/remove_admin', to: 'users#remove_admin', as: :remove_admin
post '/users/:id/make_excom', to: 'users#make_excom', as: :make_excom
delete '/users/:id/remove_excom', to: 'users#remove_excom', as: :remove_excom
Create the corresponding methods in the users_controller being sure they are in the admin_user before_action
def make_admin
#user = User.find(params[:id])
#user.admin = true
#user.save
flash[:success] = "#{#user.name} is now an Admin"
redirect_to users_url
end
def remove_admin
#user = User.find(params[:id])
#user.admin = false
#user.save
flash[:success] = "#{#user.name} is no longer an Admin"
redirect_to users_url
end
def make_excom
#user = User.find(params[:id])
#user.excom = true
#user.save
flash[:success] = "#{#user.name} is now an Executive Committee Member"
redirect_to users_url
end
def remove_excom
#user = User.find(params[:id])
#user.excom = false
#user.save
flash[:success] = "#{#user.name} is no longer an Executive Committee Member"
redirect_to users_url
end
And the partial for displaying a user on the index page is then
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
<% if current_user.admin? && !current_user?(user) %>
|
<%= link_to "Delete", user, method: :delete,
data: { confirm: "You sure?" } %>
|
<% if user.admin? %>
<%= link_to "Remove Admin", remove_admin_path(user), method: :delete,
data: { confirm: "You sure?" } %>
<% else %>
<%= link_to "Make Admin", make_admin_path(user), method: :post,
data: { confirm: "You sure?" } %>
<% end %>
|
<% if user.excom? %>
<%= link_to "Remove Excom", remove_excom_path(user), method: :delete,
data: { confirm: "You sure?" } %>
<% else %>
<%= link_to "Make Excom", make_excom_path(user), method: :post,
data: { confirm: "You sure?" } %>
<% end %>
<% end %>
</li>
And then write some tests to be sure.
test "admins should be able to make and remove new admins" do
log_in_as(#user)
post make_admin_path(#other_user)
assert #other_user.reload.admin?
delete remove_admin_path(#other_user)
assert_not #other_user.reload.admin?
end
test "non admins can't make or remove admins" do
log_in_as(#other_user)
delete remove_admin_path(#user)
assert #user.reload.admin?
post make_admin_path(#another_user)
assert_not #another_user.reload.admin?
end
test "admins should be able to make and remove executive committee" do
log_in_as(#user)
post make_excom_path(#another_user)
assert #another_user.reload.excom?
delete remove_excom_path(#another_user)
assert_not #another_user.reload.excom?
end
test "non admins can't make or remove executive committee" do
log_in_as(#another_user)
post make_excom_path(#user)
assert_not #user.reload.excom?
delete remove_excom_path(#other_user)
assert #other_user.reload.excom?
end
Edit:
This probably is pushing the limit of "good/maintainable" code and the "rails-way", which is why I asked the question. But since this works, and took a lot less time than learning and setting up a full blown roles system like devise I'll stick with it for now. If I need to make any significant changes then I will probably switch to devise.
In my rails app , when I logout , in the destroy method I am setting session[:user_id]=nil. But when I press back button on the browser the session[:user_id] gets back its previous value and it is automatically showing the logged in page. Why is this happening? How do I make the session[:user_id]=nil persistent till I change it?
session_controller.rb
class SessionsController < ApplicationController
def index
end
def show
end
def new
end
def create
#user = User.find_by_email(params[:email])
if #user && #user.authenticate(params[:password])
session[:user_id] = #user.id
redirect_to user_posts_path
else
render 'new'
end
end
def destroy
session[:user_id] = nil
end
end
application.html.erb
<% if !(session[:user_id].nil?)%>
Logged in as <%= current_user.email %>
<%= link_to 'Log Out', session_path(current_user), :method => :delete %>
<% else %>
<% if current_page?(new_user_path) %>
<%= link_to "Log in", login_path %>
<% elsif current_page?(login_path) %>
<%= link_to "sign up",new_user_path%>
<% else %>
<%= link_to "Log in", login_path %>
<%= link_to "sign up",new_user_path%>
<% end %>
<% end %>
<%= yield %>
there is no error in the rails s console.
last message on the console.
Started DELETE "/sessions/2" for 127.0.0.1 at 2015-10-08 00:23:11 +0530
Processing by SessionsController#destroy as HTML
Parameters: {"authenticity_token"=>"B0QLdVrsV9ZgwjS/Y8qVb3ID0q9gsC2peFQAZ/0J638kUTpXcAYcg1I+ulX1UaLujr4C7NPgIann74UETMOz6w==", "id"=>"2"}
Rendered sessions/destroy.html.erb within layouts/application (0.1ms)
Completed 200 OK in 144ms (Views: 143.4ms | ActiveRecord: 0.0ms)
Use reset_session in your logout action instead. This will issue a new session identifier and declare the old one invalid and prevents other session fixation based attacks.
http://guides.rubyonrails.org/security.html#session-fixation-countermeasures
This is a run through of how to setup your SessionsController properly:
Sessions are not really like a standard crud resource where you have the full range of CRUD verbs and fetch records from the database.
From a user standpoint there are only three actions:
new - displays the login form
create - verifies the credentials and signs the user in.
destroy - logs user out by resetting the session.
Change your routes definition to treat Sessions as a singular resource:
resource :sessions, only: [:new, :create, :destroy]
Then we are going to create a helper:
module SessionsHelper
def current_user
#user ||= User.find!(session[:user_id]) if session[:user_id]
end
def user_signed_in?
!current_user.nil?
end
def can_sign_in?
user_signed_in? || current_page?(new_user_path) || current_page?(new_session_path)
end
end
This way the actual implementation of how the user is stored in the session is only in one place in your application and not spread all over your controllers and views.
Lets make sure we can call it from our controllers:
class ApplicationController < ActionController::Base
include SessionsHelper
end
Then lets remedy the controller:
class SessionsController < ApplicationController
# GET /session
def new
end
# POST /session
def create
reset_session # prevents sessions fixation!
#user = User.find_by(email: params[:email])
if #user && #user.authenticate(params[:password])
session[:user_id] = #user.id
redirect_to user_posts_path
else
render 'new', flash: "Invalid username or password."
end
end
# DELETE /session
def destroy
reset_session
if user_signed_in?
flash[:notice] = 'You have been signed out successfully.'
else
flash[:error] = 'You are not signed in!'
end
redirect_to root_path
end
end
application.html.erb
<%= render partial: 'sessions/actions' %>
<%= yield %>
We use a partial since the application layout tends to turn into a monster.
sessions/_actions.html.erb.
<% if user_signed_in? %>
Logged in as <%= current_user.email %>
<%= link_to 'Log Out', session_path, method: :delete %>
<% else %>
<%= link_to 'Log in', new_session_path if can_sign_in? %>
<% end %>
First of all I am new to rails. I used devise gem for user authentication. Now I want to provide admin a way to delete other users. id of the user is not passing to my destroy action. Here is my code
user_controller.rb
class UsersController < ApplicationController
def destroy
User.find(params[:id]).destroy
redirect_to dashboard_path
end
end
dashboard.html.erb
<% if current_user.admin == true %>
<% #users = User.all %>
<% #users.each do |user| %>
<%= user.email %>
| <%= link_to "delete", destroy_path, method: :delete, data: { confirm: "Are you sure"} %><br>
<% end %>
<% end %>
First of all, You shouldn't assign instance variables directly in your views. This is a Controller responsibility. So, in the above example, the right thing to do is something like this:
# users_controller.rb
class UsersController < ApplicationController
def dashboard
#users = User.all
end
def destroy
User.find(params[:id]).destroy
redirect_to dashboard_path
end
end
And your view should look something like this:
# dashboard.html.erb
<% if current_user.admin == true %>
<% #users.each do |user| %>
<%= user.email %>
| <%= link_to "delete", user, method: :delete, data: { confirm: "Are you sure"} %><br>
<% end %>
<% end %>
And last but not least =P make sure your routes.rb have something like this:
# routes.rb
delete "/users/:id" => "users#destroy", as: :user
Of course it's just an example based on your question, but it should work like a charm =P
I have a user model and a question model.
In the user model:
has_many :questions
The question model:
belongs_to
in my questions/show.html.erb
<% if #question.user == current_user %>
<%= link_to 'Edit', edit_question_path(#question) %> | <%= link_to 'Destroy', #question, method: :delete, data: { confirm: 'Are you sure you want to delete this job?' } %>
<%= link_to 'Back', questions_path %>
<% else %>
<%= link_to 'Back', questions_path %>
<% end %>
How can only the user that authored the question edit and delete it?
Take a look at CanCan, the authorization gem by Ryan Bates of Railscasts. It's great for Rails authorization needs.
First, you'll create an Ability class that defines all of the abilities in the application.
class Ability
include CanCan::Ability
def initialize(user)
can :manage, Question, user_id: user.id
end
end
Then, you'll be able to easily integrate authorization into your controllers.
class QuestionsController < ApplicationController
def update
authorize! :manage, #question
...
end
def destroy
authorize! :manage, #question
...
end
end
And also customize your views.
<% if can? :manage, #question %>
<%= link_to 'Edit', edit_question_path(#question) %> | <%= link_to 'Destroy', #question, method: :delete, data: { confirm: 'Are you sure you want to delete this job?' } %>
<% end %>
All you need in your controller is:
def destroy
#question = current_user.questions.find(params[:id])
#question.destroy
render ... #anything you want to render
end
The previous code will ensure that an user can only delete his own questions. If the id of the question doesn't belongs to the user no question will be deleted and it would throw and ActiveRecord::RecordNotFound - Internal Server error. You can add a begin - rsecue block to catch this exception an handle it as you want.
def destroy
begin
#question = current_user.questions.find(params[:id])
#question.destroy
render or redirect_to ....
rescue Exception ActiveRecord::RecordNotFound
flash[:notice] = 'not allow to delete this question'
redirect_to ....
end
end
Other simple way is to add a before filter in your controller
before_filter :require_authorization, only: [:delete]
...
def destroy
#question = current_user.questions.find(params[:id])
#question.destroy
render or redirect_to ....
#With the before filter this code is only going to be executed if the question belongs to the user
end
...
private
def require_authorization
redirect_to :root unless current_user.questions.find_by_question_id(params[:id])
#Use the find_by to avoid the ActiveRecord::RecordNotFound and get a nil instead in case the question id doesn't belong to a question of the user
end
you can try changing your if to the following:
<% if current_user.questions.include?(#question) %>
Also you can take a look at :inverse_of
Then in your Edit and Delete actions in the controller you can again check for the right user before showing the edit form or deleting the question.
I have added a before filter and def check priv to the users controller. It is suppose to be setup so that only admin can view/edit all profiles, and that only the created User can view their own profile. As before anyone could view/edit profiles. I have tried a few methods, none work. When I go to view profile as admin or even regular user I get the "not authorized" message.
Any help would be appreciated.
users_controller:
before_filter :check_privileges, :only => [:edit, :update]
def check_privileges
unless current_user.admin? || current_user.id == params[:id]
flash[:warning] = 'not authorized!'
redirect_to root_path
end
end
index.html:
<%= link_to user.name, user %>
<% if (current_user.admin? || current_user) == #user %>
<%= link_to "Edit #{user} profile", user %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?"} %>
<% end %>
I have a similar method in my app, try something like this:
def check_privileges
return if current_user.admin? # this user is an admin, if is not the case do:
flash[:warning] = 'not authorized!'
redirect_to root_path
end
UPDATE 1
Again, try to change the if condition as the follow
if (condition || condition)
or
if ((condition) || (condition))
The problem is that Ruby parsers stop at the first condition if not explicited declared.
UPDATE 2
I think that there are an error in the parentheses on your index.html.erb, try the following:
<%= link_to user.name, user %>
<% if (current_user.admin? || (current_user == #user)) %>
<%= link_to "Edit #{user} profile", user %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?"} %>
<% end %>
Food for thought maybe you could do something like this:
def correct_user
#user = User.find(params[:id])
if current_user.role? :administrator
#Allow admin to access everyone account
else
access_denied unless current_user?(#user)
end
end
Then inside your view your view do the if statement. Or alternatively my best suggestion is to go with something like CanCan. Something like this will allow you to set up role authentication really easily. If you have a look at it you can set a certain amount of rule in your ability.rb which you can then enforce on the view.
If you WERE to go with the method of CanCan you could do something like this in your ability.rb
def initialize(user)
user ||= User.new # guest user
# raise user.role?(:administrator).inspect
if user.role? :administrator
can :manage, :all
can :manage, User
elsif user.role? :employee
can :read, :all
end
The above is an example.... So that in your views you can enforce this type of rule by doing something like
<%= link_to user.name, user %>
<% if can? :manage, #user %>
<%= link_to "Edit #{user} profile", user %>
| <%= link_to "delete", user, method: :delete,
data: { confirm: "You sure?"} %>
<% end %>
Something like this should work. Your options are there hope this helps :)