Pundit: auhorize Index in nested resources - ruby-on-rails-4

Using Rails 4.2.4 with Devise (3.5.2) and Pundit (1.0.1). Decent_exposure (2.3.2).
I have a simple nested associaton for User and Idea:
class User < ActiveRecord::Base
has_many :ideas
...
class Idea < ActiveRecord::Base
belongs_to :user
...
In routes.rb
devise_for :users
resources :users do
resources :ideas
end
Then I am simply trying to disallow access to users/1/ideas if current_user is not the owner of the Ideas (in this example, if current_user.id != 1).
I can not figure out how to do it. I am able to show just the current_user Ideas in the Index view with:
[Ideas controller]
def show
authorize idea
end
[Idea policy]
def show?
#current_user == #idea.user
end
But how can I prevent a user to simply navigate to other user's Idea index page?
I guess that in Ideas controller I should use something like:
def index
authorize user
end
But then what? How can I send to the User Policy the info regarding the Idea collection?
Or should I authorize via the Idea Policy itself?

Duplicating my response on GitHub here because this gets more traffic.
One way is to create a stub Idea owned by the user to authorize against.
def index
#user = User::find(params[:user_id])
idea = Idea.new(user_id: #user.id)
authorize idea
# ...
end
and an index? method in your IdeaPolicy
def index?
record.user_id = user.id
end
Another way is to change what you're authorizing against. Instead of authorizing against Idea, authorize against the User.
def index
#user = User::find(params[:user_id])
authorize #user, :show_ideas?
# ...
end
and create a new show_ideas? method on your UserPolicy
def show_ideas?
user.id == record.id
end

Related

Devise get remote_ip in model validator

I'm trying to validate user sign ups against a Spam service that requires the IP and eMail. I'm using Rails 4.2 with Devise. Devise has the current_sign_in_ip attribute, but it is nil during sign up.
Is there a way to pass the value of request.remote_ip from the Devise registration/create controller action to the User model on sign up?
I tried the following, but the key is not added:
class Users::RegistrationsController < Devise::RegistrationsController
def create
sign_up_params[:current_sign_in_ip] = request.remote_ip
super
end
end
Finally figured it out. The before_filter needed to be uncommented and the param added to the permit list.
class Users::RegistrationsController < Devise::RegistrationsController
before_filter :configure_sign_up_params, only: [:create]
def create
params[:user][:current_sign_in_ip] = request.remote_ip
super
end
protected
def configure_sign_up_params
devise_parameter_sanitizer.for(:sign_up) << :current_sign_in_ip
end
end

How to set up authorization policies for two controllers using Pundit?

I'm learning Pundit using the RailsApps Pundit Tutorial and this statement from the tutorial totally confused me:
Given that the policy object is named UserPolicy, and we will use it
for authorization from the Users controller, you might wrongly assume
that the name of the policy object will always match the name of the
controller. That is not the case.
How can I create a policy (o set of policies) that allow users with the "role_a" to use the users_controller.index action and users with the "role_b" to use the orders_controller.index action?
1.1 Does this require two different policies (UserPolicy and OrderPolicy) or should I name the index action for every controller differently to differentiate it on the UserPolicy?
Yes it requires two different policies(UserPolicy and OrderPolicy)
#user_policy.rb
class UserPolicy
attr_reader :current_user
def initialize(current_user)
#current_user = current_user
end
def index?
#current_user.role_a?
end
end
And in your index method of users_controller
def index
#user = User.find(params[:id])
authorize #user
end
Same for OrderPolicy
#order_policy.rb
class OrderPolicy
attr_reader :current_user
def initialize(current_user)
#current_user = current_user
end
def index?
#current_user.role_b?
end
end
And in your index method of orders_controller
def index
#user = User.find(params[:id])
authorize #user
end

Rails 4: where to put functions that are needed across all controllers

I am trying to make sure that the following method
def current_user
current_user = current_member
end
Is available to all actions in all my controllers
I have tried putting it in the ApplicationsController with no luck.
I tried using solutions in the following
Where to put Ruby helper methods for Rails controllers?
With no effect.
What is the Rails-way solution to this?
I have the same method in my ApplicationsHelper and I can access it in my views no problem.
EDIT:
To give more detail.
I had an application with an authentication system I built from scratch and this used a function in a SessionHelper file called "current_user"
I have been implementing Devise into my app, and maintained my user model to hold the user details, but created a member model to hold the devise authentication information (i.e. keep the user profile info separate from the table devise is using as suggested by the doc).
This gives me a devise helper method called current_member (based on my naming of the model).
I have "current_user" all over my app, both in controller actions and in views.
I want to create an app-wide helper that will alias current_member to current_user. Strictly speaking in my question my function is wrong - this will assign current_user to an instance of the member class. Since there is a one to one relationship between member and user, with the foreign key being member.id the correct function is....
def current_user
if member_signed_in?
current_user = User.find_by_member_id(current_member.id)
end
end
My ApplicationHelper:
module ApplicationHelper
def current_user
if member_signed_in?
current_user = User.find_by_member_id(current_member.id)
end
end
end
This takes care of current_user in all the views,
But I can't get it to work in the controllers...see for example this code in my "show" action of the UserController
def show
#associates = []
#colleagues = current_user.nearbys(1000).take(20)
#colleagues.each do |associate|
unless current_user.following?(associate) || current_user == associate
#associates.push(associate)
end
end
impressionist(#user)
end
Forget the logic- I am just using geocoder to find nearly users. Its that current_user is resolving to "nil".
Even if I put
before_action :current_user
def current_user
if member_signed_in?
current_user = User.find_by_member_id(current_member.id)
end
end
In the UserController, current_user isn't working within the action. I have current_user in actions of other controllers too and the app breaks at these points, but not when current_user is in a view.
Let me know if you need more info.
EDIT 2:
I added
before_action :authenticate_member!
To the UsersController, but this still had no effect.
EDIT 3:
I'm an idiot. The nil class error was occurring because I had no seed data in the database, thus the
#colleagues = current_user.nearbys(1000).take(20)
#colleagues was nil, and therefore calling "take" on nil was throwing an error.
Rookie mistake.
When you define application actions, if you want them to be available across all other actions you need to set a before filter. So in your application controller you would have something like:
before_action :current_user
def current_user
current_user = current_member
end
This would run this action before any other action in your application regardless of the controller
I think there are two parts of your question.
1 - where to put functions that are needed across all controllers ?
So the general answer is to put them in the ApplicationController, because normally all the other controllers are inheriting from ApplicationController
2 - About the error you are getting.
My guess is , you are not loading devise before you call the devise method. So try something like
class ApplicationController < ActionController::Base
before_action :authenticate_member!
before_action :current_user
def current_user
#your method
end
end
and as a suggestion, since you are using the same method in the helpers, to keep things DRY, you can make the controller method a helper method. So it will be available across the views
class ApplicationController < ActionController::Base
helper_method :current_user
before_action :authenticate_member!
before_action :current_user
def current_user
#your method
end
end
so in your view you can use current_user
If all of that fails, as #abbott567 said post your error log.
HTH

Extra Attributes while login with facebook using OAuth and Devise Rails 4 app

i've been searching for a way to do the following and I haven't succeded yet.
I have already implemented Facebook Login with my app, but i am looking for a way to ask the user for some extra attributes that are not provided by Facebook (Like home address for example).
I tried redirecting the user to the edit_user_registration page once he is logged in for the first time, but I can't add the new attributes because the user won't know his password due to the fact that is provided by Facebook and its encripted.
Thanks in advance for any answers!
First override Devise::OmniauthCallbacksController controller. Add a method for Facebook connection in which you have to do two things. First if the user is already on your system, this method should log the user in directly.
Second if the user is new, you should build a user object, extract the parameters you need from Facebook and update this user object with these parameters. After that you should render a page where the user can input their own password and add the extra information you need.
omniauth_callbacks_controller.rb
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
user = User.find_by_fb_token(token)
if user.present?
sign_in user, event: :authentication
else
#user = User.new(**Facebook parameters**)
render 'devise/registrations/after_social_connection'
end
end
end
In the after_social_connection view, add a form for #user where you will have all attributes from Facebook prepopulated. Do not forget to add :password and :password_confirmation fields for the user to be able to have a password on your own application. In this view you can add whatever attributes you like the user to input.
Since you are using Rails 4, you also need to override devise parameters sanitizer (strong parameters) to be able to input extra fields in a form. This can be done in the application controller (the lazy way)
application_controller.rb
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:password, :password_confirmation, :email. :name, :biography, :profile_picture) }
end
end
For further reading
https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview

devise 'authorize User' results in undefined method

I'm starting with the Rails 4.1 Pundit / Devise app from RailsApps.org and continue to get undefined method errors when 'authorize User' is called in the User controller. The user can register, log in, and edit their account info. When the Users link is clicked, the following results:
NoMethodError in UsersController#index
undefined method `authorize' for #
Here is the UsersController...
class UsersController < ApplicationController
before_filter :authenticate_user!
after_action :verify_authorized
def index
#users = User.all
authorize User # <== This is the line the error occurs on
end
def show
#user = User.find(params[:id])
authorize #user
end
def update
#user = User.find(params[:id])
authorize #user
if #user.update_attributes(secure_params)
redirect_to users_path, :notice => "User updated."
else
redirect_to users_path, :alert => "Unable to update user."
end
end
def destroy
user = User.find(params[:id])
authorize user
user.destroy
redirect_to users_path, :notice => "User deleted."
end
private
def secure_params
params.require(:user).permit(:role)
end
end
and the ApplicationController:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Any thoughts on how to resolve this?
Thanks in advance!
Comment: This is from the RailsApp Pundit Quickstart guide to explain authorize
The keyword authorize is a helper method that provides a shortcut to a longer statement that implements the actual authorization. We never see the full statement because we use the helper method, but if we were to use it, it would look like this:
raise "not authorized" unless UserPolicy.new(current_user, User).index?
The authorize helper method finds a UserPolicy class and instantiates it, passing the current_user object and either the User class or an instance of the User model, and calling an index? method to return true or false. You may wonder why we can provide either the User class (as in the index action) or the #user instance variable (as in the show action).
Pundit looks for a policy object when authorize is called from a controller action. We already saw that Pundit will find a UserPolicy if given any of these arguments:
authorize User – the User class
authorize #user – an instance variable that is an instance of the User class
authorize user – a simple variable that is an instance of the User class
authorize #users – an array of User objects
To me, it seems as if the helper method is found sometimes like in show and update but not index.
It looks like this issue is being discussed here: https://github.com/RailsApps/rails-devise-pundit/issues/10
Basically, your solutions are to:
A) Restart the rails server and the problem should go away. You will have to do this whenever the problem shows up (editing the file, etc). (It shouldn't happen in production if it's any consolation)
B) Move the code in config/intiializers/pundit.rb in to ApplicationController (without the included do...end block)
User is a class name, not an instance. Also authorize used for create/update/edit actions. For index you should use Policy.
For example, UserPolicy:
def index
#users = UserPolicy::Scope.new(current_user, User).resolve
end