UserPolicy for Index - ruby-on-rails

I am trying to create a policy so that only admins can acces a page. I've already managed to get pundit to work in another controller, but for some reason this policy wont work.
I've created a controller: users_controller.rb which is as follows:
def index
#user = current_user
authorize #user
end
end
I've created a Policy user_policy.rb which is:
def initialize(current_user, record)
#user = current_user
#record = record
end
def index?
#user.admin?
end
end
Any idea's what's going wrong?

I messed up, it routes to the users page, not an index.
Problem solved by changing
def index
to
def users
And do the same for the policy method

Related

How to restrict access to different pages in Rails?

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.

How to solve NoMethodError with Pundit

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.

Devise: restricting page access using user attributes

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

Rails- Ruby Instance Variable

So I am using omniauth-facebook to create a log in using Facebook.
Here is my sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to "/sessions/menu"
end
def destroy
session[:user_id] = nil
redirect_to root_url
end
def new
end
def menu
puts user.name
end
end
Unfortunately I don't know how to access the user variable in the menu action. How would you guys recommend I do this?
Update
class SessionsController < ApplicationController
def create
#user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = #user.id
redirect_to "/sessions/menu"
end
def destroy
session[:user_id] = nil
redirect_to root_url
end
def new
end
def menu
puts #user
end
end
Even when I update it like so, it doesn't work
Unfortunately I don't know how to access the user variable in the menu
action. How would you guys recommend I do this?
Every time a request is made in your app for actions in SessionsController, a new instance of the SessionsController is created. So, instance variable set during create action would not be available when request for menu action is called as now you have a new instance of SessionsController which is why #user set in create action would not be available in menu. Also, if you use user (local variable) in this case, then its always local to the method/action in which its defined. So even that would not be available in menu.
By using facebook-omniauth gem you would receive Auth Hash in env["omniauth.auth"] which in turn you would use to create(new user) or initialize(in case of existing user) a user hopefully in from_omniauth method.
NOTE: env is request specific so env["omniauth.auth"] value will be present in create action but not in menu.
To resolve this, i.e., to access the created or initialized facebook user in menu action, you should make use of the user_id that you stored in session as below:
def menu
if session[:user_id]
#user = User.find(session[:user_id])
end
end
Also, if you would like to access the user in other actions as well then I would recommend to reuse the code by using before_action callback:
class SessionsController < ApplicationController
## Pass specific actions to "only" option
before_action :set_user, only: [:menu, :action1, :action2]
#...
def menu
puts #user.name
end
private
def set_user
if session[:user_id]
#user = User.find(session[:user_id])
end
end
end
where you can add specific actions via :only option

redirecting from another page taking info

So after the user signs up, i redirect them to my additional info page where i collect some more information. However, something is wrong with my design/implementation as rails is saying im missing users/create template
this is my users controller
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def additional_info
#user = User.new(user_addinfo)
if #user.save
redirect_to show_path
end
end
def create
#user = User.new(user_params)
if #user.save
# UserMailer.welcome_email(#user).deliver
sign_in #user
redirect_to additional_info_path
flash[:success] = "Welcome to InYourShoes!"
#return #user
else
render'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
def user_addinfo
params.permit(:year)
end
end
def show is the user profile page i want to show after redirecting to the additional_info page
def additional_info is just take additional info from the private method def user_addinfo
def create is the sign up process.
After entering the basic user info, it gets redirected to additional which is fine. but after the additional, it says im missing the users/create template, but my code i attempted to redirect to show_path and #usersshow, still doesnt work
any suggestions? sorry if this seems intuitive but Im new to rails.
I think your problem is in the additional_info method, as i said in the comment. What you're doing is:
creating a user
creating a session for the user (sign_in #user) - storing somewhere the user_id in the session
redirecting to your additional_info page
And here comes the problem. As the user is already signed in you don't have any need to create a new user with additional params. You should have some helper to retrieve the current signed in user (like current_user) and in additional_info method, just update it.
So your additional_info method would become something like:
def additional_info
user = User.find session[:user_id]
user.update params[:user]
redirect_to user_path #show action
end

Resources