Rails 6 - Pundit "policy wrapper" - ruby-on-rails

I am a little stuck with Pundit: It feels that the solution should be easy - but I am not getting it. Actually I have a bunch of models which are all dependent on one main model. My main model is a script. The script has many roles, many scenes, many costumes, etc. Additionally it has some joined connections like scene.roles. I simply want to authorize the user who created the script to do everything (adding roles, deleting scenes, just everything that is in the scope of her own script) and everybody else to do (and see) nothing. Do I need to create a policy for every model or can I just (re-)use somehow one "script policy"?
How would an authorization look like in a dependent controller (i.e. 'index' in roles or 'new' in scenes)?
The authentication is handled by Device. A user must be logged in to see or do anything.
This is my first post on stack overflow, happy to join the community:-)

Pundit policies are POROs (plain old ruby objects). So you can easily create a policy for the main model:
class ScriptPolicy
attr_reader :user, :script
def initialize(user, script)
#user = user
#script = script
end
def update?
user.actor?
end
def delete?
user.admin?
end
end
And then for every model that you have, simply create an empty class definition that inherits from the ScriptPolicy
class ScenePolicy < ScriptPolicy
end
Another approach would be to overwrite the policy name that Pundit is referencing directly in the child model. So assuming that you have a model Scene that shall use the same policy as Script, you can add a method:
class Scene < ActiveRecord::Base
def self.policy_class
ScriptPolicy
end
end
In the latter, you do not need to create empty policy classes for every model, but you trade it for the decreased flexibility in defining specific permissions for models.

When calling the authorize method in your controller, you can specify the policy class:
authorize #model, policy_class: ScriptPolicy
I find that this solution generates less boilerplate files.
To authorize a scope:
scenes = policy_scope(Scene, policy_scope_class: ScriptPolicy::Scope)
By the way, this is all covered in pundit's README: https://github.com/varvet/pundit

Related

Using current user in Rails in a model method

I'm currently trying to implement simple audit for users (just for destroy method). This way I know if the user has been deleted by an admin or user deleted itself. I wanted to add deleted_by_id column to my model.
I was thinking to use before_destroy, and to retrieve the user info like described in this post :
http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/
module UserInfo
def current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
end
But this article is from 2007, I'm not sure will this work in multithreaded and is there something more up to date on this topic, has anyone done something like this lately to pass on the experience?
Using that technique would certainly work, but will violate the principle that wants the Model unaware of the controller state.
If you need to know who is responsible for a deletion, the correct approach is to pass such information as parameter.
Instead of using callbacks and threads (both represents unnecessary complexity in this case) simply define a new method in your model
class User
def delete_user(actor)
self.deleted_by_id = actor.id
# do what you need to do with the record
# such as .destroy or whatever
end
end
Then in your controller simply call
#user.delete_user(current_user)
This approach:
respects the MVC pattern
can be easily tested in isolation with minimal dependencies (it's a model method)
expose a custom API instead of coupling your app to ActiveRecord API
You can use paranoia gem to make soft deletes. And then I suggest destroying users through some kind of service. Check, really basic example below:
class UserDestroyService
def initialize(user, destroyer)
#user = user
#destroyer = destroyer
end
def perform
#user.deleted_by_id = #destroyer.id
#user.destroy
end
end
UserDestroyService.new(user, current_user).perform

Testing a Rails ApplicationController method with rspec

Just having trouble figuring out how to test an action that utilizes a private ApplicationController method. To explain:
A User has and belongs to many Organisations (through Roles), and vice versa. An Organisation has a bunch of related entities, but in the app a user is only dealing with a single Organisation at a time, so for all of my controller methods I want to scope with a current organisation. So in my ApplicationController:
private
# Returns the organisation that is being operated on in this session.
def current_org
# Get the org id from the session. If it doesn't exist, or that org is no longer operable by the current user,
# find an appropriate one and return that.
if (!session[:current_organisation_id] || !current_user.organisations.where(['organisations.id = ?', session[:current_organisation_id]]).first)
# If an org doesn't exist for this user, all hope is lost, just go to the home page
redirect_to '/' if (!current_user.organisations.first)
# Otherwise set the session with the new org
session[:current_organisation_id] = current_user.organisations.first.id;
end
# Return the current org!
current_user.organisations.first
end
First things first, I don't know if this is the best way to scope. I'd love some elegant default_scope thing, but with the use of session variables this seems problematic. Anyway, the above let's me do this in controllers:
class HousesController < ApplicationController
def index
#houses = current_org.houses
end
end
So now I want to use rspec to test my controllers. How can I make sure the current_org method can be called and returns one of the factory_girl Organisation models so that I can write the spec. I'm just confused at how best to bring the factory, spec, action and AppControlled method together.

Rails Authorization on Web Services

My rails app is pretty much a front-end to several web services. I persist my User model, and that's about it. I need to add authorization to my web app, using Devise for authentication. I've noticed CanCan and acl9 seem to work mostly on instances of ActiveRecord models. Would CanCan or acl9 still fit my needs? Any tips on using either of these libraries in my situation?
Should I look for something that works more on actions instead of instances?
Also, these are both Role based systems, and I'm thinking of using a permission based system. Would they still be a good fit?
I can't speak for acl9. However, the cancan wiki does claim that "It is easy to make your own [model] adapter if one is not provided." https://github.com/ryanb/cancan/wiki/Model-Adapter In other words, even though you're not using ActiveRecord, you might still be able to use cancan.
Then again, if you're not planning on having roles, your ability definitions in cancan might be a little redundant looking, eg.:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
can :create, Widget if user.has_permission(:create_widgets)
can :delete, Widget if user.has_permission(:delete_widgets)
can :herp, Derp if user.has_permission(:herp_derp)
end
end
It would be great if you could use cancan just for its controller action authorization methods, but I don't know if that's possible. Good luck.
Just to (finally :) answer for acl9.
Acl9 is composed of two very separate pieces, the Access Control Subsystem which is all the authorizing stuff you put in your controller, and the Role Subsystem which is setting/checking/removing roles from an authenticated user.
The only thing that the Access Control Subsystem ever calls is current_user.has_role?( role, obj=nil). So, the Role Subsystem has zero dependency on ActiveRecord, associations, database, etc. There is a helper (acts_as_authorization_subject) which adds an ActiveRecord-dependent has_role? method to a class, but that's entirely optional and you're free to implement your own has_role? method (which can also fallback to calling super to get the acl9 one) and implement your access checks however you please. So, you said that you do persist your user model, but let's say you want a role for your user to be the admin of a school, but that school is a web service call into some remote system.
## in your model
class User < ActiveRecord::Base
def has_role? role, obj=nil
role == :admin &&
obj == school &&
school[:admin] == id # <-- just making up how we know we're the admin of the remote school
end
end
def school
#school ||= School.get_by_user_id(id)
end
end
## in your controller
class SomeController < ApplicationController
before_action :set_school
access_control do
allow :admin, of: :school
end
private
def set_school
#school = School.get_by_id(params[:school_id])
end
end

Rails 3 ActiveRecord validation based on user permissions

I'm shifting code from an application built in a non-standard custom PHP framework into Ruby on Rails (version 3). In the PHP version all the controllers are really fat, with thin models, which I've always disagreed with, so I'm enjoying the way Rails does validation at the model level, which is probably 90% of what's happening in these fat controllers currently.
One problem I'm facing, and unsure how to resolve however, is that of differing validation rules based on who's making the change to the model. For example, an administrator, or the original creator of the record should be able to do things like flag a record as deleted (soft delete) whereas everybody else should not.
class Something < ActiveRecord::Base
...
validates :deleted, :owned_by_active_user => true
...
end
class OwnedByActiveUserValidator < ActiveModel::EachValidator
validate_each(record, attr_name, attr_value)
# Bad idea to have the model know about things such as sessions?
unless active_user.admin? || active_user.own?(record)
record.errors.add :base, "You do not have permission to delete this record"
end
end
end
Since the model itself is (in theory) unaware of the user who is making the change, what's the "rails way" to do this sort of thing? Should I set the active user as a virtual attribute on the record (not actually saved to DB), or should I just perform these checks in the controller? I have to admit, it does feel strange to have the model checking permissions on the active user, and it adds complexity when it comes to testing the model.
One reason I'm keen to keep as much of this as possible in the model, is because I want to provide both an API (accessed over OAuth) and a web site, without duplicating too much code, such as these types of permissions checks.
It is really the controller's job to handle authorization, or to delegate authorization to an authorization layer. The models should not know about, nor have to care about, who is currently logged in and what his/her permissions are - that's the job of the controller, or whatever auth helper layer the controller delegates that to.
You should make :deleted in-attr_accessible to mass assignment via new, create, or update_attributes. The controller should check the authenticated user's authorizations separately and call deleted= separately, if the authenticated user is authorized.
There are several authorization libraries and frameworks to help with authorization or to function as an authorization layer, such as cancan.
I would solve this with a before_filter in my controller, instead of with validations in my model.
class SomethingController < ApplicationController
before_filter :require_delete_permission, :only => [:destroy]
def destroy
# delete the record
end
private
def require_delete_permission
unless current_user.is_admin || record.owner == current_user
flash[:error] = 'You do not have delete permissions'
redirect_to somewhere
end
end
end
I have come across the same issue in Rails 2.3 and finally come up with this solution. In your model you define some atribute, depending on which you switch on/off validation. Than you your control you set this attribute depending on the date available to controller (such as user privileges in your case) as follows:
Class Model < ActiveRecord::Base
attr_accessor :perform_validation_of_field1 #This is an attribute which controller will use to turn on/off some validation logic depending on the current user
validates_presence_of :field1, :if => :perform_validation_of_field1
#This validation (or any similar one) will occur only if controller sets model.perform_validation_of_field1 to true.
end
Class MyController < ActionController::Base
def update
#item = Model.find(params[:id])
#item.update_attribute(params[:item])
#The controller decides whether to turn on optional validations depending on current user privileges (without the knowledge of internal implementation of this validation logic)
#item.perform_validation_of_field1 = true unless active_user.admin?
if #item.save
flash[:success] = 'The record has been saved'
redirect_to ...
else
flash.now[:error] = 'The record has not passed validation checks'
render :action => :edit
end
end
I think that in Rails 3 it can be done in similar manner.

Rails3 - Permission Model Before_Save Check?

I have a permission model in my app, that ties (Users, Roles, Projects) together.
What I'm looking to learn how to do is prevent a user for removing himself for their project...
Can you give me feedback on the following?
class Permission < ActiveRecord::Base
.
.
.
#admin_lock makes sure the user who created the project, is always the admin
before_save :admin_lock
def before_save
#Get the Project Object
project = Find(self.project_id)
if project.creator_id == current_user.id
# SOME HOW ABORT OR SEND BACK Not Allowed?
else
#continue, do nothing
end
end
end
Is that look like the right approach?
Also, I'm not sure how to do the following two things above:
How to abort prevent the save, and send back an error msg?
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Thanks for reading through
How to abort prevent the save, and send back an error msg?
return false during the callback chain tells activemodel to stop (similar to how adding errors to the model during a validation tells it to stop at that point)
self.errors.add_to_base "msg" will add an error to the model, which can then be rendered on the view.
Get the devise, current_user.id in the model, that doesn't seem possible, so how do Rails gurus do stuff like the above?
Models shouldn't really know about things like the current request, if at all possible, you should be locking things down at the controller/action level.
EDIT:
So, the role of controllers is to deal with everything involved in getting the correct information together based on the request, and passing it to the view (which becomes the response). People often say "make your models fat and your controllers skinny", but that could be said of any system that embraces object oriented design -- your logic should be in objects when possible.
That being said, the whole point of controllers is to deal with routing the right things to the right places, and authentication is definitely a concern of routing.
You could easily move the line comparing creator_id to user id in the action, and react based on that.
Now, sometimes you genuinely need that stuff in the model and there is no way around it. That becomes a problem, because you need to fight rails to get it there. One way would be to attr_accessor a current_user field on your model, and pass that in on initialize. Another would be to remove the fields from the params hash that a user is not allowed to change in the action. Neither is really that nice though.
Agreed with Matt that you should try to use the controller for the redirect. The model should have the logic to determine if the redirect is appropriate. Maybe something like
class ProjectsController < ApplicationController
def update
redirect_to(projects_url, :alert => "You can't remove yourself from this project.") and return if Role.unauthorized_action?(:update, params[:project])
#project = Project.find(params[:id])
if #project.update_attributes(params[:project])
...
end
class Role
def self.unauthorized_action?(action, params)
# your logic here
end
You should check out CanCan for some ideas.
In permission model take one field project_creater as boolean
In project modelbefore_create :set_project_ownership
def set_project_ownership
self.permissions.build(user_id: User.current.id, project_creater: true)
end
In project controllerbefore_filter :set_current_user
In Application controllerdef set_current_user
User.current = current_user
end

Resources