You may have heard of Pundit. https://github.com/elabs/pundit Basically, it's an authorization gem.
What I want to know is, how does it access the variable current_user inside its classes?
I don't know how, but #user and user are both equal somehow to the current_user
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
#user = user
#post = post
end
def update?
user.admin? or not post.published?
end
end
We also have the post variable inside this class. We can access this by running
def publish
#post = Post.find(params[:id])
authorize #post
end
in an action.
To install Pundit you need to include the module to the application controller:
class ApplicationController < ActionController::Base
include Pundit
end
However, I still can't see how the class "queries" the controller for the current_user and how authorize gives the variable (post) to the class. Please answer these two questions :)
The PostPolicy class doesn't query anything.
The authorize method is a controller instance method, so it can just call current_user. You've passed it #post, which it uses to determine which policy class to use. Then it creates a new instance of that class, passing current_user and #post through.
Related
I have a controller that looks something like this:
module Guardians
class StudentsController < ApplicationController
def show
#student = Student.find params[:id]
authorize #student, policy_class: StudentPolicy
end
end
end
Because the controller is within a module, the policy class which is used is Guardians::StudentPolicy, which is what I want.
However I now have another controller:
module Teachers
class StudentsController < ApplicationController
def show
#student = Student.find params[:id]
authorize #student, policy_class: StudentPolicy
end
end
end
Here the policy_class used is Teachers::StudentPolicy
But because the show method itself is identical, ideally I would like to dry this up with a concern. However if I cannot seem to do this, as
authorize #student, policy_class: StudentPolicy
will no longer automatically call the namespaced policy class when it is called from inside the concern.
What is the DRYest way to achieve this?
If you create a concern like this:
module StudentsConcern
extend ActiveSupport::Concern
included do
before_action :find_and_authorize_student, only:[:show]
end
private
def find_student_and_authorize
#student = Student.find(params[:id])
if self.class.name.deconstantize == 'Teachers'
authorize #student, policy_class: Teachers::StudentPolicy
else
authorize #student, policy_class: Guardians::StudentPolicy
end
end
end
And then include this in both of the controllers and clear the show actions.
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
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
how to make this code clean in rails?
profiles_controller.rb :
class ProfilesController < ApplicationController
before_action :find_profile, only: [:edit, :update]
def index
#profiles = Profile.all
end
def new
#profile = Profile.new
end
def create
profile, message = Profile.create_object(params["profile"], current_user)
flash[:notice] = message
redirect_to profile_url
end
def edit
end
def update
profile, message = #profile.update_object(params["profile"])
flash[:notice] = message
redirect_to profile_url
end
private
def find_profile
#profile = Profile.friendly.find(params["id"])
end
end
i look flash[:notice] and redirct_to profile_url is duplicate in my code, how to make the code to clean and dry?
How about moving the repetitive code to a separate method and call that method inside the actions.
def flash_redirect # you can come up with a better name
flash[:notice] = message
redirect_to profile_url
end
then in update action:
def update
profile, message = #profile.update_object(params["profile"])
flash_redirect
end
do the same thing for create action
UPDATE:
in case you are wondering about usingafter_action, you can't use it to redirect as the call-back is appended after the action runs out its course. see this answer
Take a look at Inherited Resources. It's based on the fact that many CRUD controllers in Rails have the exact same general structure. It does most of the work for you and is fully customisable in case things are done a little different in your controllers.
Using this gem, your code would look like this:
class ProfilesController < InheritedResources::Base
def create
redirect_to_profile(*Profile.create_object(params[:profile], current_user))
end
def update
redirect_to_profile(*#profile.update_object(params[:profile]))
end
private
def redirect_to_profile(profile, message)
redirect_to(profile_url, notice: message)
end
def resource
#profile ||= Profile.friendly.find(params[:id])
end
end
The create and update methods return multiple values, so I used the splat operator to DRY this up.
create_object and update_object don't follow the Rails default, so we need to implement those actions for Inherited Resources instead. Currently they don't seem to be handling validation errors. If you can, refactor them to use ActiveRecord's save and update, it would make everything even easier and DRYer.
I'm trying to view my new action in my blogs controller, but I keep getting the following error message:
NameError in BlogsController#new
undefined local variable or method `authenticate_admin'
In my blogs controller, I want to restrict the new action to admins only (admins and users are two different models). I was able to get this to work in another model. If I'm not mistaken, helpers are open to all classes. I also tried to add the code from my admins helper to the blogs helper, but that didn't work.
Why can't my blogs controller use my authenticate_admin method?
Thanks for lookign :)
Here are relevant files:
blogs_controller.rb
class BlogsController < ApplicationController
before_filter :authenticate_admin, :only => [:new]
def new
#blog = Blog.new
#title = "New Article"
end
end
admins_helper.rb
def authenticate_admin
deny_admin_access unless admin_signed_in?
end
def deny_admin_access
redirect_to admin_login_url, :notice => "Please sign in as admin to access this page."
end
def admin_signed_in?
!current_admin.nil?
end
def current_admin
#current_admin ||= Admin.find(session[:admin_id]) if session[:admin_id]
end
In this case Helpers are accessible in your Views not in Controllers.
Solution is to move your methods from admins_helper.rb to ApplicationController and set them as helper_methods. You will be able to access them in your Controllers and Views.
Example:
class ApplicationController < ActionController::Base
# Helpers
helper_method :authenticate_admin
def authenticate_admin
deny_admin_access unless admin_signed_in?
end
end
Read documentation about helper_method:
http://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html#method-i-helper_method