Rails Pundit how the policies are worknig? - ruby-on-rails

I have a web application in ruby on rails with devise as the authentication and pundit as the authorization.
I have a model user with an integer role attribute with values 0, 1, 2, for visitor, vip, and admin respectively. I also have a scaffold, say Page that I want just vip and admin to have access to and not visitor users.
In page_policy.rb I have
def index?
current_user.vip? or current_user.admin?
end
and in pages_controller.rb I have a line authorize current_user.
Although I have given access to vip but it is available just for admin user. Where have I been wrong with the code?
Thank you

I assume you have properly set up your predicate methods vip? and admin? on the User model, and these work as expected? Can you check the result of calling each of these methods on your current user?
Pundit actually passes the result of current_user to your policy initializer, and you can access that via the user method inside your policy methods. Also I would be careful of using the or operator in this context (see http://www.virtuouscode.com/2010/08/02/using-and-and-or-in-ruby/).
So I would try:
def index?
user.vip? || user.admin?
end
Also, pundit expects you to pass the resource you are checking to the authorize method, not the user object. If you don't have an instance to pass, you can pass the class:
authorize Page

Related

Devise - bypass_sign_in without active_for_authentication? callback

I have functionality of inactive account in my application for handling this i override active_for_authentication? method as below
def active_for_authentication?
super && activated?
end
But In my application super admin can also directly login in to other user account, whether it is active or not active
bypass_sign_in(User.find(resource.id))
I used above method for by pass sign in, it allows me to directly sign in only for activated user, when i login for non activated user it goes in infinite loop .
Any solutions to over come this issue or don't run active_for_authentication? callback when bypass_sign_in?
When admin logs in to another user account you can store some additional data in session, that makes it clear that this is the super admin mode.
def login_as(another_user)
return unless current_user.super_admin?
session[:super_admin_mode] = true
bypass_sign_in(another_user)
end
Unfortunately, you can't access session in Rails models, but you can store needed session information in some per-request global variable that is available in models. The solution might be like this:
module SessionInfo
def self.super_user_mode?
!!Thread.current[:super_user_mode]
end
def self.super_user_mode=(value)
Thread.current[:super_user_mode] = value
end
end
In the ApplicationController:
class ApplicationController < ActionController::Base
before_filter :store_session_info
private
def store_session_info
SessionInfo.super_user_mode = session[:super_admin_mode]
end
end
In the model:
def active_for_authentication?
super && (activated? || SessionInfo.super_user_mode?)
end
Also, you should make sure that the :super_admin_mode flag is removed from session when the super user logs out. Maybe it happens automatically, I am not sure. Maybe you will need to do it manually overriding Devise::SessionsController#destroy method (see the example below)
def destroy
session[:super_admin_mode] = nil
super
end
Also read this for better understanding of how devise handles session Stop Devise from clearing session
I recently came across a similar issue where I needed to allow an Admin to sign in as regular Users who were not active in Devise. I came up with the following solution that doesn't involve using Thread.current (which after looking into further online it seems like using Thread.current could be a precarious solution to this problem).
You can create a subclass of User called ProxyUser that has the active_for_authentication? return true. Something like this:
class ProxyUser < User
# If you have a type column on User then uncomment this line below
# as you dont want to expect ProxyUser to have type 'ProxyUser'
#
# self.inheritance_column = :_type_disabled
devise :database_authenticatable
def active_for_authentication?
true
end
end
Then in the controller you want something like this:
proxy_user = ProxyUser.find(params[:user_id])
sign_in :proxy_user, proxy_user
Also in your routes you will need devise to expect ProxyUser so include:
devise_for :proxy_users
And finally when you sign this user out (assuming you can sign the user out in your controller code) make sure to tell devise the scope of the sign out, so you would do
sign_out :proxy_user
And then finally note that in your app you may be expecting current_user in different places (such as if you use CanCanCan for authorization) and now when you sign in as a proxy_user your app will return current_user as nil. Your app will instead have an object called current_proxy_user that will be your signed-in ProxyUser object. There are many ways to handle the issues resulting from your current_user returning nil in this case (including overwriting current_user in your application controller).

How do I pass extra context to Pundit scopes?

I have a system where a User can be associated with many Portals, however a user's permissions may differ between portals.
For example, a user might be able to see unpublished posts in one portal, but not in another portal.
For methods like show?, I can grab the portal off the record.
def show?
portal = record.portal
# logic to check whether, for this particular portal,
# this user has permission to view this record
end
However that solution doesn't work for policy scopes.
Is there any way I can, say, pass in the portal to policy_scope method in the controller?
One solution I've seen around the place is to set a (temporary) attribute against the user, so that policy methods can use it. e.g.
# model
class User < ActiveRecord::Base
attr_accessor :current_portal
...
end
# controller
posts = portal.posts
current_user.current_portal = current_portal
policy_scope posts
# policy Scope
def resolve
portal = user.current_portal
# logic to scope these records by user's portal permissions
end
However this seems like a workaround, and I can definitely think of other scenarios where I'd like to be able to give authorisation logic more context as well, and I don't want this workaround to become a bad habit.
Does anyone have any suggestions?
Pundit authorization classes are plain old ruby classes. Using pundit authorize method you can pass an object to be authorized and an optional query. When you for example use authorize method as
authorize #product, :update?
the authorize method will call the update? method on the ProductPolicy class. If no query passed, controller action name + ? mark will be set as the query. Take a look at Pundit authorize method definition.
So for passing extra parameters to Pundit authorize method, you should override authorize method with an extra argument:
module Pundit
def authorize(record, query=nil, options=nil)
query ||= params[:action].to_s + "?"
#_pundit_policy_authorized = true
policy = policy(record)
if options.nil?
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
else
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query, options)
end
end
end
Then in your policy class you could use this extra param
class ProductPolicy
...
def update?(options)
...
end
def new?
...
end
...
end
As you can see, you can have policy methods that accept an extra options argument.
Then you can use authorize method in your controllers in one of these ways:
authorize #product
authorize #product, nil, { owner: User.find(1) }
authorize #product, :some_method?, { owner: User.find(1) }
authorize #product, :some_method?

How do you restrict access to a certain webpage?

I am trying to allow access to the log-in/sign-up page for a admin user only from my computer or any other way that lets me only see the web page for an admin sing-up-log-in.
Or what do typical web applications do to restrict access to the public towards a certain web page? If there is a bets-practice way, I would like to implement that.
I currently have Devise installed.
You can use the authenticate_user! Devise helper, adding it as callback within the needed controller and specifying the methods you want to control.
For instance if you have a Post model, then adding the authenticate_user! in the PostController it'll ask the user to be logged to have access to the methods in that specific controller:
class PostsController < ApplicationController
before_action :authenticate_user!
If you want to restrict just some specific methods then you can play with only and/or except.
See: Devise will create some helpers to use inside your controllers
and views. To set up a controller with user authentication, just add
this before_action (assuming your devise model is 'User')
Devise - Controller filters and helpers
According to your comment then you can create a method in the ApplicationController in order to restrict all of your controllers and methods.
This way you can define an array of addresses, and if the remote_ip coming from the request is in the array then you give access, if isn't then perform any other action:
ApplicationController < ActionController::Base
before_action :protect
private
def protect
addresses = ['127.0.0.1', ...]
if addresses.include?(request.remote_ip)
# give access
else
# restrict access
end
end
end
But if you need something more sophisticated then you'd have to see on your Nginx or Apache, whatever you're using to deploy your project.
I normally restrict webpage access through controller methods. My recent use case was going to a webpage only when payment was successful but redirecting when it was not, if any body issued a get request for that page directly, it would result in 404.
In your case, there can be multiple option for setup.
You can use cookies to see users credentials using Action Dispatcher
Use Devise for users and then you can fix a certain role to a user through adding a new migration and assign roles yourself after registering or let them choose.
I will expect you followed Devise route. In the controller action check for current user's role.
If User Not signed in (using current_user == nil)
redirect to home page and then return
else
if
its admin you go ahead and use the success page as partial and let them see the page using `<%= render 'pages/mypage'%>` and use return to end
else
just redirect back to home page with a notice "Don't try this".
redirect_to root_path, notice: 'Don't try this' and then use
return to end
end
(Just for fun, to see how many times a user did this wrong action, you can also have a table which stores current_user and number_of_wrong_attempt, and store their email whenever they try to go that page without permission before redirect in controller. After that you can email them with a background rake task which checks for a certain false attempt threshold that: "Hey! Your registration is being removed because you are doing unprohibited actions")

Using devise within the models

Is there a way to use devise functions within a model? Let's say, a scope?
For reference, this is what I was thinking of:
scope :available, -> {
if user_signed_in?
requestable
else
requestable.where.not(books: {owner_id: current_user.id})
end
}
I would need to use user_signed_in? and current_user in this scope. But I can't. Is there a way to do it?
current_user (and same goes for devise helpers such as user_signed_in?) is only available in your controllers. I've been around that question and ended up by passing current_user to my scopes and model's methods.
In the model:
scope :available, -> (current_user, signed_in) {
if signed_in
requestable
else
requestable.where.not(books: {owner_id: current_user.id})
end
}
In the controller:
MyModel.available(current_user, user_signed_in?)
An alternative would be to use request_store (rack based global data storage) to store the current_user of the current request
Methods like these are not meant to be used in models. Models are supposed to work outside the controller as well (i.e. directly from the Rails console).
For a more detailed reasoning see:
Access to current_user from within a model in Ruby on Rails
Further there is no global 'store' knowing which users are currently signed in - this is determined within a request, by veryfying the user sends valid session information with his request.

Devise User Inheritance and user_signed_in? and current_user

I'm using devise to help with my authentication and for various reasons I have a couple subclasses of the User model that helps separate the concerns for registration and some other business logic.
The subclasses of User are Affiliate and Admin (and I might have more in the future), all of which store all of the data in the User table (not separate tables).
All users can log in via the default users resource (/users/sign_in). However, this is where I'm having an issue.
If the user signs in via the /users/sign_in resource I can access the user_signed_in? and current_user helper and then access any other Active Record associations. This is what I want! Woo hoo! This works great.
However, the same is not true of a user who signs up as an Affiliate. Since devise automatically logs the user in (which I DO want) I expect that user_signed_in? to equal true and current_user to be the user that just signed in. This is not the case when a user signs up via the /affiliate/sign_up resource.
On the Affiliate model (remember, it subclasses user like such class Affiliate < User) I have generated (through devise) a separate set of controller/views for sign_up so I can customize this process a bit (as the sign_up process is a bit more customized for this particular type of user). When the user signs up via this resource /affiliate/sign_up the affiliate is then signed up, but NOW, the current_user is nil and user_signed_in? is false. But, the helpers current_affiliate is a hydrated object and affiliate_signed_in? is true.
What I want to do is be able to access ONE type of helper - the user_signed_in? helper and the current_user helper not the child affiliate_signed_in? and current_affiliate. I'd like to access: current_user and user_signed_in only. Seeing that Affiliate subclasses User, why doesn't user_signed_in? and current_user returned the current user (aka: the Affiliate)? Why do the current_affiliate and affiliate_signed_in? helpers work, but not the user-esque ones not?
Is there a way to make the framework always use current_user and user_signed_in? helpers since everything is subclassing the User model?
The way you set it up, it seems that you are able to log in as both a user and an affiliate at the same time. That means, that at a given moment there could be a current_user and a current_affiliate. If you want it to be current_user all the time, you can override the Devise helpers. But you have to make sure that a there can only be a user OR an affiliate logged in, through one session. Else Devise has no way of knowing which object to return for current_user.

Resources