While I'm reading Rails 4 in Action, I'm trying to implement my own application, so it doesn't look same as in the book.
The book's corresponding commit is Section 7.2.3: Only admins can create or delete projects
In my case, admins can only delete the item (item corresponds to the project in the book.).
My repo https://github.com/tenzan/shop and deployed http://ichiba-demo.herokuapp.com/
The rule I want to apply is:
A regular user (you can login with staff#example.com/password) can do everything except destroy action.
Admin (admin#example.com/password) only can destroy.
To realise that I have:
In admin/items_controller.rb:
class Admin::ItemsController < Admin::ApplicationController
def destroy
#item = Item.find(params[:id])
#item.destroy
flash[:notice] = 'Item has been deleted.'
redirect_to items_path
end
private
def item_params
params.require(:item).permit(:name, :quantity)
end
end
In controllers/items_controller.rb:
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update]
def index
#items = Item.all
end
def new
#item = Item.new
end
def create
#item = Item.new(item_params)
if #item.save
flash[:notice] = 'Item has been created.'
redirect_to #item
else
flash.now[:alert] = 'Item has not been created.'
render 'new'
end
end
def show
end
def edit
end
def update
if #item.update(item_params)
flash[:notice] = 'Item has been updated.'
redirect_to #item
else
flash.now[:alert] = 'Item has not been updated.'
render 'edit'
end
end
private
def set_item
#item = Item.find(params[:id])
rescue ActiveRecord::RecordNotFound
flash[:alert] = 'The item could not be found.'
redirect_to items_path
end
def item_params
params.require(:item).permit(:name, :quantity)
end
end
In routes.rb:
Rails.application.routes.draw do
namespace :admin do
root 'application#index'
resources :items, only: :destroy
end
devise_for :users
root 'items#index'
resources :items, only: [:index, :show, :edit, :update, :new, :create] do
resources :comments
end
end
Questions:
Do I have to specify actions in the routes.rb, as I already have specified who can use what actions in their corresponding controllers? I didn't notice any change when I remove them from the routes.rb...
Am I violating DRY concept, when I specify actions in 2 places, i.e. in the routes.rb and controllers/items_controllers.rb ?
I'll be happy if you point out other places to improve to meet best practice.
PS: The subject maybe vague, please feel free to edit it.
Do I have to specify actions in the routes.rb, as I already have
specified who can use what actions in their corresponding controllers?
Yes. For instance, if you'd have only one action in items_controller.rb controller (let say show), and left
resources :items do # no specified actions
#...
end
in routes.rb it would generate all routes for items controller (for new, create, edit, destroy, update etc). But specifying actions in routes.rb you limit generated routes to only needed.
Am I violating DRY concept, when I specify actions in 2 places, i.e.
in the routes.rb and controllers/items_controllers.rb ?
No. Because you actually specify actions in controller, in routes.rb you only specify routes.
I'll be happy if you point out other places to improve to meet best
practice.
This line:
resources :items, only: [:index, :show, :edit, :update, :new, :create] # better to use %i() notation, eg only: %i(index show edit update new create)
could be written as:
resources :items, except: :destroy
Regarding your admin user - to allow only him to destroy, just check if current_user is admin. If you'll have more, than one action which is allowed to be performed by only admin, you can create before_action in controller:
before_action :check_admin?, only: %i(destroy another_action)
private
def check_admin?
# your logic to check, if user is admin
end
You can also be interested in going through Ruby style guide.
Even though you're not violating DRY directly, you're muddying up the REST architecture by moving a single entity's actions to different controllers. You don't need a specific controller or namespace for admins - you just need to assert that the user is an administrator before proceeding with the delete action.
Since you have already added the admin column to your devise model, you can move the delete action to ItemsController
def destroy
if current_user.try(:admin?)
#item = Item.find(params[:id])
#item.destroy
flash[:notice] = 'Item has been deleted.'
else
flash[:alert] = 'Only admins can delete items.'
end
redirect_to items_path
end
Your routes would be cleaner since your admin namespace would be used only for user moderation. The only route for items would be:
resources :items do
resources :comments
end
Related
I am using Rails 7, Devise and Pundit.
I've got Users and Projects.
Only Users classified as "admin" or "moderator" can perform actions (New,
Edit, Update, Destroy, ...).
Unlogged Users and Users classified as "user" can see Index and Show pages.
When I'm on a show page ('http://localhost:3000/projects/[id]') as an unlogged User and try to edit it (via 'http://localhost:3000/projects/[id]/edit') it sends me to a Devise login page which is normal. Once logged in correctly with an unauthorized profile (User classified as "user") Pundit authorization kicks in and rescues the request.
=> The problem is here :
First Firefox tells me that the page isn't redirected properly ... Probably because I'm sent back to 'http://localhost:3000/users/sign_in' while being signed in.
When I reload my page it tells me via an alert "You are already signed in." on my root_path page.
Application_controller :
class ApplicationController < ActionController::Base
before_action :store_user_location!, if: :storable_location?
before_action :authenticate_user!, except: [:index, :show]
before_action :configure_permitted_parameters, if: :devise_controller?
include Pundit
protect_from_forgery with: :exception
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
after_action :verify_authorized, except: :index, unless: :skip_pundit?
after_action :verify_policy_scoped, only: :index, unless: :skip_pundit?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
devise_parameter_sanitizer.permit(:sign_in, keys: [:username])
devise_parameter_sanitizer.permit(:account_update, keys: [:username])
end
private
def skip_pundit?
devise_controller? || params[:controller] =~ /(^(rails_)?admin)|(^pages$)/
end
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_back(fallback_location: root_path)
end
def storable_location?
request.get? && is_navigational_format? && !devise_controller? && !request.xhr?
end
def store_user_location!
# :user is the scope we are authenticating
store_location_for(:user, request.fullpath)
end
def after_sign_in_path_for(resource_or_scope)
stored_location_for(resource_or_scope) || super
end
end
Project_policy :
class ProjectPolicy < ApplicationPolicy
class Scope < Scope
# NOTE: Be explicit about which records you allow access to!
# def resolve
# scope.all
# end
def resolve
scope.all
end
private
attr_reader :user, :scope
end
def index?
true
end
def show?
true
end
def create?
user.admin? || user.moderator?
end
def edit?
user.admin? || user.moderator?
end
def update?
user.admin? || user.moderator?
end
def destroy?
user.admin? || user.moderator?
end
end
I don't think more is needed but if some code samples are missing don't hesitate to tell me ! I'd like to find a way to handle this properly. Thanks !
I know it is in the Pundit documentation but have you tried without the protect_from_forgery line? I can tell you from first hand experience Pundit works without it...
EDIT: Try to move the protect_from_forgery before the before_action :authenticate_user!
I found a solution but it's probably not a clean one and I don't know if it is safe or if it's durable.
I removed from my application_controller the following :
the method : storable_location?
the method : store_user_location!
before_action :store_user_location!, if: :storable_location?
This is what I added/modified under "private".
# Redirect after login via Devise
def after_sign_in_path_for(resource)
session["user_return_to"] || root_path
end
# Redirect if unauthorized by Pundit
def user_not_authorized
session["user_return_to"] = redirection_reroll
flash[:alert] = "You are not authorized to perform this action."
redirect_to(session["user_return_to"] || root_path)
end
# Reroll redirection path when unauthorized
def redirection_reroll
checker = ["new", "edit"]
path = session["user_return_to"].split("/")
if checker.include? path[-1]
path.pop()
end
session["user_return_to"] = path.join("/")
end
I'm working on a Rails 5 app using the omniauth-bnet gem, not devise, have a Single Sign On through that gem, and have a few User types, using Single Table Inheritance. For whatever reason, the admin type can login fine, but the average User cannot create a session. Here's some of the relevant code.
items_controller.rb:
before_action :check_authorization, except: [:show]
before_action :check_for_email, except: [:show]
...
private
def check_authorization
unless current_user
redirect_to root_path
end
end
def check_for_email
unless current_user.email
redirect_to signup_add_email_url
end
end
sessions_controller.rb:
class SessionsController < ApplicationController
def create
begin
#user = User.from_omniauth(request.env['omniauth.auth'])
session[:user_id] = #user.id
flash[:success] = "Well met, #{#user.name}!"
rescue
flash[:warning] = "There was an error while trying to create your
account..."
end
redirect_to items_path
end
...
admin_user.rb:
class AdminUser < User
end
normal_user.rb:
class NormalUser < User
end
user.rb:
class User < ApplicationRecord
...
class << self
def from_omniauth(auth_hash)
user = find_or_create_by(name: auth_hash['info']['battletag'], uid:
auth_hash['uid'], provider: auth_hash['provider'])
user.name = auth_hash['info']['battletag']
user.uid = auth_hash['uid']
user.token = auth_hash['credentials']['token']
user.save!
user
end
end
routes.rb:
...
# Auth
get '/auth/:provider/callback', to: 'sessions#create'
...
The logs show that my NormalUser type session never gets created. Yet the AdminUser type doesn't have any problem logging in...
Any ideas? I've tried everything I can google or think of.
Problem localhost:3000/users/ won't display
I enter humbly as I am trying to make it through the rails tutorial for the first time. I am in chapter 10 and I have been trouble shooting this for 5 hours. When I attempt to visit localhost:3000/users/ I get an error (I believe this has something to do with factory_girl) that explain that the #users variable is empty and that I forgot to pass a collection object for will_paginate.
I'm currently at chapter 10, section 10.23 and each time I run:
$ bundle exec rake db:reset $ bundle exec rake db:populate
$ bundle exec rake db:test:prepare
I get an error explaining that
rake aborted!
Factory already registered: micropost
This is my second time trying this chapter as I encountered problems the first time and started from chapter 9. Please help and be clear and detailed when providing directions. I am happy to post whatever files that will be helpful.
Here is my index.html.erb - I save these as HTML, should they be saved as ruby files instead?
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<%= render #users %>
</ul>
<%= will_paginate %>
Here is my users controller
class UsersController < ApplicationController
before_filter :signed_in_user, only: [:index, :edit, :update, :destroy]
before_filter :correct_user, only: [:edit, :update]
before_filter :admin_user, only: :destroy
def show
#user = User.find(params[:id])
#microposts = #user.microposts.paginate(page: params[:page])
end
end
def new
#user = User.new
end
def index
#title = "All users"
#users = User.paginate(:page => params[:page])
end
def create
#user = User.new(params[:user])
if #user.save
sign_in #user
flash[:success] = "Do more of the things you love!"
redirect_to #user
else
render 'new'
end
end
def edit
end
def update
if #user.update_attributes(params[:user])
flash[:success] = "Profile updated"
sign_in #user
redirect_to #user
else
render 'edit'
end
end
def destroy
User.find(params[:id]).destroy
flash[:success] = "User destroyed."
redirect_to users_url
end
private
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
def correct_user
#user = User.find(params[:id])
redirect_to(root_path) unless current_user?(#user)
end
def admin_user
redirect_to(root_path) unless current_user.admin?
end
In your Users controller, make sure you have #users and if you are using will_paginate, make sure you call .paginate(page: params[:page], per_page: 20] and in your view, have <%= will_paginate #users %>.
/users should point to UsersController#index. Make sure you are assigning the collection #users.
For instance it could look like this at the most basic level:
def index
#users = User.all # not paginated
#users = User.paginate(page: params[:page]) # paginated
end
As far as the test database error, I'm guessing that's because you define a :micropost factory more than once.
Instead of #user = User.find(params[:id]) this you should use
#users = User.paginate(page: params[:page])
In my application using CanCan I have permissions where users can view and create stores but I also want them to only be able to edit the ones they've created. Users can create as many stores as they like, which all should be editable by them. A store doesn't have users so how could I do this when theirs no user_id apart of the Store table?
CanCan:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.role == "default"
can :read, Store
can :create, Store
end
end
end
Since a user will be able to create as many stores as they like, a store will belong to a user.
You MUST create this relationship.
So, in the User model.
class User < ActiveRecord::Base
has_many :stores
end
And in the Store model.
class Store < ActiveRecord::Base
belongs_to :user
end
And in the ability.rb file, just put something like:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.role == 'default'
can :manage, Store , :user_id => user.id
end
end
end
I would add the following to the store model:
has_one :created_by, :class => User
Then add a migration to add a created_by_id to your Store class.
You should then be able to add a CanCan::Ability:
can :edit, Store, :created_by => user
I agree with a previous poster, that you must set up a relationship between User and Store. The relationship can be one-to-many (as Kleber S. showed), or many-to-many, if a store can have multiple users.
Then, the best way to handle access control is in the controller, by using the association. For the show, edit, update, destroy methods, you'll need to find the store, given a logged in user, so do something like this:
class StoresController < ApplicationController
before_filter :find_store, only: [:show, :edit, :update, :destroy]
def show
end
def edit
end
def update
if #store.update_attributes(params[:store])
# redirect to show
else
# re-render edit, now with errors
end
end
# ...
private
def find_store
#store = current_user.stores.find(params[:id])
end
end
This way, the association takes care of limiting the lookup to only those stores that are connected to the current_user by foreign key. This is the standard way for RESTful Rails resources to perform access control of associated resources.
I'm kind of new to Rails 3.1. and I'm facing an issue only in my production env with my Signup form (actually, it's more about the controller).
Here is the code in User
class UsersController < ApplicationController
[...]
def create
#user = User.new(params[:user])
logger.info "value of login in param : #{params[:user][:login]}" #-> log the actual login
logger.info "value of login : #{#user.login}" #-> log empty
#user.admin = false
if #user.save
flash[:notice] = t('flash.notice.user.create.valid')
redirect_back_or_default root_path
else
flash[:notice] = t('flash.notice.user.create.invalid')
render :action => :new
end
end
end
Also, the controller logs show that the params hash is good
Parameters: {"utf8"=>"✓",
"authenticity_token"=>"QwOqmp0CT/d4mmC1yiLT4uZjP9bNDhbUXHanCQy5ZrA=",
"user"=>{"login"=>"myLogin",
"email"=>"t.r#gmail.com",
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]"}}
My login form works as expected (already created users are able to sign in)
Again, this only happens in production.
EDIT: Here is my User Model
class User < ActiveRecord::Base
acts_as_authentic
#== Callbacks
before_create :set_defaults
attr_accessible :avatar ##### EDIT: TO FIX THE ISSUE, ADD THE OTHER FIELDS AS WELL
protected
def set_defaults
self.total_1 = self.total_2 = self.total_3 = 0
end
end
Just to memorialize the answer from the comments above:
Normally you can use mass assignment to set fields on a model, but when you use attr_accessible, you are then limited to only mass assigning those fields. So stuff like User.new(params[:user]) won't work; instead, you'd have to do:
#user = User.new
#user.login = params[:user][:login]
# ...etc.
#user.save
Simple add your fields to the attr_accessible list and you can go back to mass assignment.