I'm running Pundit in my Rails app for authorisation. I seem to be getting the hang of it all but want to know how to restrict the edit or update actions to a certain field.
For example, a user can edit their user.first_name, user.mobile or user.birthday etc but can't edit their user.role. Essentially my logic is, let the user edit anything that's cosmetic but not if it is functional.
These fields should only be able to be edited by a user who has a 'super_admin' role (which I have setup on the user.rb with methods such as the below).
def super_admin?
role == "super admin"
end
def account?
role == "account"
end
def segment?
role == "segment"
end
def sales?
role == "sale"
end
def regional?
role == "regional"
end
def national?
role == "national"
end
def global?
role == "global"
end
I pretty much have a clean slate user_policy.rb file where the update and edit actions are the default
def update?
false
end
def edit?
update?
end
Maybe I am thinking entirely wrong about this and should just wrap a user.super_admin? if statement around the role field on the user show page but this feels wrong if I am only using that tactic for security.
Use Pundit's permitted_attributes helper which is described on the gem's README page: https://github.com/elabs/pundit
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes
if user.admin? || user.owner_of?(post)
[:title, :body, :tag_list]
else
[:tag_list]
end
end
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
#post = Post.find(params[:id])
if #post.update_attributes(post_params)
redirect_to #post
else
render :edit
end
end
private
def post_params
params.require(:post).permit(policy(#post).permitted_attributes)
end
end
In your views, you can limit what users can see based on their role.
User View
- if current_user.super_admin?
= f.select(:role, User.roles.keys.map {|role| [role.titleize.role]})
- else
= user.role
And in the policy you can call the role of the user to make sure they are able to edit.
class UserPolicy
attr_reader :current_user, :model
def initialize(current_user, model)
#current_user = current_user
#user = model
end
def edit?
#current_user.super_admin || #current_user == #user
end
end
Related
I'm working on a rails app where I wrote a personalized route called "all_designs"; with the corresponding method on the controller and the view, before I add pundit to my project it was working fine.
Now I'm having this error:
Pundit::AuthorizationNotPerformedError in DesignsController#all_designs
I understand that I'm missing a policy for this action, but the way I'm trying is not working.
How can I add a policy for this method?
Controller:
class DesignsController < ApplicationController
before_action :set_design, only: [:show,:edit,:update,:destroy]
def index
#designs = policy_scope(Design.where(user: current_user, status: 'activo'))
#user = current_user
end
def all_designs
#designs = Design.where(user: current_user)
#user = current_user
end
...
end
Policy:
class DesignPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
def create?
true
end
def show?
true
end
def destroy?
user == record.user
end
def update?
# If the user is the owner of the design
user == record.user
end
def all_designs?
true
end
end
I would consider separate controller and policy for this as what you're doing is really just a nested route (designs belonging to a singleton resource).
scope 'user', module: :users do
resources :designs, only: :index
end
module Users
class DesignsPolicy
class Scope < Scope
def resolve
#user.designs # make sure user has a `has_many :designs` assocation.
end
end
end
def index?
true
end
end
# Represents designs belonging to the current user
module Users
class DesignsController < ApplicationController
# GET /user/designs
def index
#designs = policy_scope
end
end
end
This lets you separate the logic of displaying the the current users designs from /designs which would display everything in a clean way.
Every method on the controller which needs to be authorized, needs to contains an explicit declaration like this:
def all_designs
#designs = Design.where(user: current_user)
#user = current_user
authorize #designs
end
The reason it wasn't working was: I missed the authorize line
I have controller Users:
class UsersController < ApplicationController
......
def show
#user = User.find(params[:id])
end
def index
#users = User.paginate(page: params[:page], per_page: 25)
end
......
end
Now users profiles are at /users/1, /users/2, etc. and list of users is at /users/.
I want to give special access:
user can see only own profile
admin can see the list of users and any profile
How can I restrict access this way?
Assuming you have a current_user defined and your User class has an admin attribute you can do the following:
class UsersController < ApplicationController
......
def show
#user = User.find(params[:id])
if current_user.admin || #user == current_user
# render the show screen
else
# redirect to wherever
end
end
def index
if current_user.admin
#users = User.paginate(page: params[:page], per_page: 25)
# render the index screen
else
# redirect to wherever
end
end
......
end
Or you could just use one of the plenty of authorization gems out there, like cancancan or pundit.
You should use ACL libraries like cancancan or pundit or from ruby-toolbox.com
I would probably handle this by having two different endpoints, something like /profile and /admin/users/1. Then you have different controllers for them:
UserProfileController < ApplicationController
def show
#user = current_user
end
end
and:
class Admin::UsersController < AdminController
def show
#user = User.find(params[:id])
render 'user_profile/show' # or another template if you like
end
end
class AdminController < ApplicationController
before_action :ensure_admin
def ensure_admin
if !current_user.admin?
raise ActionController::RoutingError, 'Not Found'
end
end
end
Considering your url user/1/ you grab the param id and compare it to the current user ID in a hook :
before_action :auth_user
private
def auth_user
unless params[:id].to_s == current_user.id.to_s
redirect_to root_path
end
Regarding the admin you probably have a dedicated namespace, with even more thorough checks, where you can see user profiles.
I don't know if I'm doing something wrong here but it seems like.
I use Pundit for authorization and I have set up a few models with it now.
Ive got a Category model which can only be created by admins. Also I don't want users to see the show/edit/destroy views either. I just want it to be accessed by admins. So far so good.
Will add some code below:
category_policy.rb
class CategoryPolicy < ApplicationPolicy
def index?
user.admin?
end
def create?
user.admin?
end
def show?
user.admin?
end
def new?
user.admin?
end
def update?
return true if user.admin?
end
def destroy?
return true if user.admin?
end
end
categories_controller.rb
class CategoriesController < ApplicationController
layout 'scaffold'
before_action :set_category, only: %i[show edit update destroy]
# GET /categories
def index
#category = Category.all
authorize #category
end
# GET /categories/1
def show
#category = Category.find(params[:id])
authorize #category
end
# GET /categories/new
def new
#category = Category.new
authorize #category
end
# GET /categories/1/edit
def edit
authorize #category
end
# POST /categories
def create
#category = Category.new(category_params)
authorize #category
if #category.save
redirect_to #category, notice: 'Category was successfully created.'
else
render :new
end
end
# PATCH/PUT /categories/1
def update
authorize #category
if #category.update(category_params)
redirect_to #category, notice: 'Category was successfully updated.'
else
render :edit
end
end
# DELETE /categories/1
def destroy
authorize #category
#category.destroy
redirect_to categories_url, notice: 'Category was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_category
#category = Category.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def category_params
params.require(:category).permit(:name)
end
end
application_policy.rb
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user
#record = record
end
def index?
false
end
def create?
create?
end
def new?
create?
end
def update?
false
end
def edit?
update?
end
def destroy?
false
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
scope.all
end
end
end
Ive got Pundit included in my ApplicationController and rescue_from Pundit::NotAuthorizedError, with: :forbidden this set up there as well.
The authorization itself works, if I'm logged in with an admin account I have access to /categories/*. If I'm logged out I get the following: NoMethodError at /categories
undefined methodadmin?' for nil:NilClass`
While writing the question I think I found the problem- I guess Pundit looks for a User that is nil because I'm not logged in. What would the correct approach of solving this issue look like?
Best regards
The most common approach is to redirect users from pages that are not accessible by not logged in users. Just add a before action in your controller:
class CategoriesController < ApplicationController
before_action :redirect_if_not_logged_in
<...>
private
def redirect_if_not_logged_in
redirect_to :home unless current_user
end
end
(I assume here that you have current_user method which returns user instance or nil. Please change :home to wherever you want to redirect users)
There are multiple ways of achieving what you want.
The most obvious (but kind of dirty) and straightforward-looking way of doing that would be to add a check for user presence in every condition:
user && user.admin?
It won't fail with nil error as the second part of the condition won't get executed. But it doesn't look very nice, right? Especially if you have to copy this over to all methods you have in CategoryPolicy.
What you can do instead, is to make Pundit "think" that you passed a User, by creating a GuestUser class which responds with false to admin? method (https://en.wikipedia.org/wiki/Null_object_pattern):
In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral ("null") behavior. The null object design pattern describes the uses of such objects and their behavior (or lack thereof)
And use it when user is nil. In practice, it will look like this:
class ApplicationPolicy
attr_reader :user, :record
def initialize(user, record)
#user = user || GuestUser.new
#record = record
end
# ...
end
class GuestUser
def admin?
false
end
end
This way you won't have to alter any of your other code, as the model you passed responds to the method which is expected by policy (admin?). You may want to define this GuestUser somewhere else (not in the policy file), depending if you want other parts of the app to reuse that behavior.
You can also proceed with the redirect approach from P. Boro answer. It's less flexible in some sense but can totally work fine if you don't need anything besides redirecting all non-logged in users.
Relatively new to rails, I've got a simple web app using Devise for user authentication. One attribute is an :admin boolean, set nil for most users, and I will change to true manually in the console for the few users who will need to have administrative access.
My question is: How should I restrict access to a particular page to those who have admin access marked as true?
I've attempted some of that logic in my pages_controller, but it doesn't seem to redirect me as desired (referring to the user_list section):
class PagesController < ApplicationController
before_action :authenticate_user!, :except => [:welcome]
def welcome
#code removed for brevity's sake
end
def dashboard
#ditto
end
def user_list
unless
current_user.admin == true
redirect_to pages_dashboard_path
else
#users = Users.all
end
end
end
Any suggestions on my goal of redirecting or otherwise restricting access to my user_list page would be greatly appreciated.
in your controller you can do something like this
class PagesController < ApplicationController
...
def user_list
if current_user.admin == true
#users = Users.all
else
render :not_an_admin
end
end
end
You can not send them to the same page that they dont have access
You can choose to render a new view
In your user_list method, model name should be singular.
def user_list
unless
current_user.admin == true
redirect_to pages_dashboard_path
else
#users = User.all
end
end
I'm learning Rails by building a shop application and I'm having a bit of trouble with redirects. I have 3 roles in the application:
Buyer
Seller
Administrator
Depending on which type they are logged in as then I would like to redirect to a different page/action but still show the same URL for each (http://.../my-account).
I don't like having to render partials in the same view, it just seems messy, is there another way to achieve this?
The only way I can think of is to have multiple actions (e.g. buyer, seller, administrator) in the accounts controller but that means the paths will look like http://.../my-account/buyer or http://.../my-account/seller etc.
Many thanks,
Roger
I've put my code below:
models/user.rb
class User < ActiveRecord::Base
def buyer?
return type == 'buyer'
end
def seller?
return type == 'seller'
end
def administrator?
return type == 'administrator'
end
...
end
controllers/accounts_controller.rb
class AccountsController < ApplicationController
def show
end
end
controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
def new
#user_session = UserSession.new
end
def create
#user_session = UserSession.new(params[:user_session])
if #user_session.save
if session[:return_to].nil?
# I'm not sure how to handle this part if I want the URL to be the same for each.
redirect_to(account_path)
else
redirect_to(session[:return_to])
end
else
#user_session.errors.clear # Give as little feedback as possible to improve security.
flash[:notice] = 'We didn\'t recognise the email address or password you entered, please try again.'
render(:action => :new)
end
end
def destroy
current_user_session.destroy
current_basket.destroy
redirect_to(root_url, :notice => 'Sign out successful!')
end
end
config/routes.rb
match 'my-account' => 'accounts#show'
Many thanks,
Roger
In UserSessionsController#create (i.e.: the login method) you could continue to redirect to the account path (assuming that goes to AccountsController#show) and then render different views according to the role. I.e.: something like this:
class AccountsController < ApplicationController
def show
if current_user.buyer?
render 'accounts/buyer'
elsif current_user.seller?
render 'accounts/seller'
elsif current_user.administrator?
render 'accounts/administrator
end
end
end
Better yet, you could do this by convention...
class AccountsController < ApplicationController
def show
render "accounts/#{current_user.type}"
end
end
If I understand you question correctly, then the solution is simple.
You can just call the method you want inside your controller. I do this in my project:
def create
create_or_update
end
def update
create_or_update
end
def create_or_update
...
end
In your case it should be:
def action
if administrator? then
admin_action
elsif buyer? then
buyer_action
elseif seller? then
seller_action
else
some_error_action
end
end
You should probably explicitly call "render" with an action name in each of those actions, though.