Advanced Role Permissions in Rails - ruby-on-rails

I have an app where there are different layers of permissions that can manage, or just simply create/view different objects. An example of my breakdown:
A host can view a reservation for a restaurant, and make an edit, but cannot create
A doorman can create a new reservation, and edit.
A customer service rep (on our side), can do pretty much everything.
A superadmin can do everything.
Is there a gem or mountable engine that I can use to take care of this? What would be the best practice?

Cancan is a good choice but lately I've been looking at Pundit as a better alternative. In your case you would have something like this:
# app/policies/reservation_policy.rb
ReservationPolicy = Struct.new(:user, :reservation) do
def create?
user.service_rep? || user.doorman?
end
end
Then in your controller:
# app/controllers/reservations_controller.rb
class ReservationsController < ApplicationController
def create
#reservation = Reservation.new(reservation_params)
authorize #reservation
#reservation.save
respond_with(#reservation)
end
end
This isn't tested and will need to be adapted to your exact situation but I hope it's a starting point.

Related

Determine in model that which controller is trigger the save action?

Developing rails app for both api and front end. so we have products controller for api and products controller for the front and Product model is one for both.
Like that
class Api::V1::ProductsController < ActionController::API
def create
#product.save
end
end
class ProductsController < ActionController::Base
def create
#product.save
render #product
end
end
class Product < ActiveRecord::Base
def weight=(value)
weight = convert_to_lb
super(weight)
end
end
Basically in product we have 'weight field' and this field is basically capture weight from the warehouse. it will be different unit for the user. so i'm going to save whatever weight is capture by unit, its lb,g or stone but it will convert to lb and store into database.
So i write the overide method for the conversation. but i want this override method should only call for front app only and not for the api. because api will always post weight in lb(its need to be convert in client side)
Can you guys anyone suggest the solution? what should i use or what should i do for this kind of scenario.suggest if its any other solution for that kind of situation as well. Thanks in advance.
It's better to keep Product model as simple as possible (Single-responsibility principle) and keep weight conversion outside.
I think it would be great to use Decorator pattern. Imagine class that works like this:
#product = ProductInKilogram.new(Product.find(params[:id]))
#product.update product_params
#product.weight # => kg weight here
So, you should use this new ProductInKilogram from Api::V1::ProductsController only.
You have options to implement that.
Inheritance
class ProductInKilogram < Product
def weight=(value)
weight = convert_to_lb
super(weight)
end
end
product = ProductInKilogram.find(1)
product.weight = 1
It's easy, but complexity of ProductInKilogram is high. For example you can't test such class in an isolation without database.
SimpleDelegator
class ProductInKilogram < SimpleDelegator
def weight=(value)
__getobj__.weight = convert_to_lb(value)
end
end
ProductInKilogram.new(Product.find(1))
Plain Ruby (My Favourite)
class ProductInKilogram
def initialize(obj)
#obj = obj
end
def weight=(value)
#obj.weight = convert_to_lb(value)
end
def weight
convert_to_kg #obj.weight
end
def save
#obj.save
end
# All other required methods
end
Looks a little bit verbose, but it is simple. It's quit easy to test such class, because it does nothing about persitance.
Links
Single-responsibility principle
Delegate gem
Decorator Pattern in Ruby

Access current user within liquid drop rails 5

I'm currently implementing liquid templates in my application. As part of that I have created a set of liquid drop (https://github.com/Shopify/liquid/wiki/Trying-to-Understand-Drops) classes to act as intermediates between my models and my templates. I'm currently using devise for authentication on rails 5.
In my product drop class I would like to be able to check if my current user owns the product:
class ProductDrop < Liquid::Drop
def initialize(model)
#model = model
end
def owned_by_user?
#somehow access the current_user provided by devise.
end
end
But haven't been able to figure out how to access the user.
I notice in this method on shopify: https://help.shopify.com/en/themes/liquid/objects/variant#variant-selected
They are able to access the current url to work out if the variant is selected. I thought perhaps it might be possible if they can access the url, to access the session and get the user identifier to look up the user.
So I can do something like:
def owned_by_user?
User.find_by_id(session[:user_id]).owns_product?(#model.id)
end
I'm not having any luck accessing the session. Does anyone have any suggestions or ideas? Or am I going about this completely the wrong way?
So after digging around in the liquid drop source code. I noticed that the context is accessible to the drop (https://github.com/Shopify/liquid/blob/master/lib/liquid/drop.rb). I totally missed it the first time I looked.
So the solution ended up being:
First add the user so it is available to the controller action the view is rendered for. this then gets added to the context by the liquid template handler (and therefore exists in the context)
class ApplicationController < ActionController::Base
before_action :set_common_variables
def set_common_variables
#user = current_user # Or how ever you access your currently logged in user
end
end
Add the method to the product to get the user from the liquid context
class ProductDrop < Liquid::Drop
def initialize(model)
#model = model
end
def name
#model.name
end
def user_owned?
return #context['user'].does_user_own_product?(#model.id)
end
end
Then add the method to the user to check if the user owns the product or not:
class UserDrop < Liquid::Drop
def initialize(model)
#model = model
end
def nick_name
#model.nick_name
end
def does_user_own_product?(id)
#model.products.exists?(id: id)
end
end
Obviously this needs error handling and so on. but hopefully that helps someone. Also if anyone knows of a better way, keen to hear it.

Rails: How to POST internally to another controller action?

This is going to sound strange, but hear me out...I need to be able to make the equivalent of a POST request to one of my other controllers. The SimpleController is basically a simplified version of a more verbose controller. How can I do this appropriately?
class VerboseController < ApplicationController
def create
# lots of required params
end
end
class SimpleController < ApplicationController
def create
# prepare the params required for VerboseController.create
# now call the VerboseController.create with the new params
end
end
Maybe I am over-thinking this, but I don't know how to do this.
Inter-controller communication in a Rails app (or any web app following the same model-adapter-view pattern for that matter) is something you should actively avoid. When you are tempted to do so consider it a sign that you are fighting the patterns and framework your app is built on and that you are relying on logic has been implemented at the wrong layer of your application.
As #ismaelga suggested in a comment; both controllers should invoke some common component to handle this shared behavior and keep your controllers "skinny". In Rails that's often a method on a model object, especially for the sort of creation behavior you seem to be worried about in this case.
You shouldn't be doing this. Are you creating a model? Then having two class methods on the model would be much better. It also separates the code much better. Then you can use the methods not only in controllers but also background jobs (etc.) in the future.
For example if you're creating a Person:
class VerboseController < ApplicationController
def create
Person.verbose_create(params)
end
end
class SimpleController < ApplicationController
def create
Person.simple_create(params)
end
end
Then in the Person-model you could go like this:
class Person
def self.verbose_create(options)
# ... do the creating stuff here
end
def self.simple_create(options)
# Prepare the options as you were trying to do in the controller...
prepared_options = options.merge(some: "option")
# ... and pass them to the verbose_create method
verbose_create(prepared_options)
end
end
I hope this can help a little. :-)

How to eager load associations with the current_user?

I'm using Devise for authentication in my Rails app. I'd like to eager load some of a users associated models in some of my controllers. Something like this:
class TeamsController < ApplicationController
def show
#team = Team.includes(:members).find params[:id]
current_user.includes(:saved_listings)
# normal controller stuff
end
end
How can I achieve this?
I ran into the same issue and although everyone keeps saying there's no need to do this, I found that there is, just like you. So this works for me:
# in application_controller.rb:
def current_user
#current_user ||= super && User.includes(:saved_listings).find(#current_user.id)
end
Note that this will load the associations in all controllers. For my use case, that's exactly what I need. If you really want it only in some controllers, you'll have to tweak this some more.
This will also call User.find twice, but with query caching that shouldn't be a problem, and since it prevents a number of additional DB hits, it still is a performance gain.
Override serialize_from_session in your User model.
class User
devise :database_authenticatable
def self.serialize_from_session key, salt
record = where(id: key).eager_load(:saved_listings, roles: :accounts).first
record if record && record.authenticatable_salt == salt
end
end
This will however, eager load on all requests.
I wanted to add what I think is a better solution. As noted in comments, existing solutions may hit your DB twice with the find request. Instead, we can use ActiveRecord::Associations::Preloader to leverage Rails' work around loading associations:
def current_user
#current_user ||= super.tap do |user|
::ActiveRecord::Associations::Preloader.new.preload(user, :saved_listings)
end
end
This will re-use the existing model in memory instead of joining and querying the entire table again.
Why not do it with default_scope on the model?
like so:
Class User < ActiveRecord::Base
...
default_scope includes(:saved_listings)
...
end

How to always set a value for account-scope in Rails?

I'm working on a multi-user, multi-account App where 1 account can have n users. It is very important that every user can only access info from its account. My approach is to add an account_id to every model in the DB and than add a filter in every controller to only select objects with the current account_id. I will use the authorization plugin.
Is this approach a good idea?
What is the best way to always set the account_id for every object that is created without writing
object.account = #current_account
in every CREATE action? Maybe a filter?
Also I'm not sure about the best way to implement the filter for the select options. I need something like a general condition: No matter what else appears in the SQL statement, there is always a "WHERE account_id = XY".
Thanks for your help!
This is similar to a User.has_many :emails scenario. You don't want the user to see other peoples emails by changing the ID in the URL, so you do this:
#emails = current_user.emails
In your case, you can probably do something like this:
class ApplicationController < ActionController::Base
def current_account
#current_account ||= current_user && current_user.account
end
end
# In an imagined ProjectsController
#projects = current_account.projects
#project = current_account.projects.find(params[:id])
I know, I know, if you access Session-variables or Instance variables in your Model you didn't understand the MVC pattern and "should go back to PHP". But still, this could be very useful if you have - like us - a lot of controllers and actions where you don't always want to write #current_account.object.do_something (not very DRY).
The solution I found is very easy:
Step 1:
Add your current_account to Thread.current, so for example
class ApplicationController < ActionController::Base
before_filter :get_current_account
protected
def get_current_account
# somehow get the current account, depends on your approach
Thread.current[:account] = #account
end
end
Step 2:
Add a current_account method to all your models
#/lib/ar_current_account.rb
ActiveRecord::Base.class_eval do
def self.current_account
Thread.current[:account]
end
end
Step 3: Voilá, in your Models you can do something like this:
class MyModel < ActiveRecord::Base
belongs_to :account
# Set the default values
def initialize(params = nil)
super
self.account_id ||= current_account.id
end
end
You could also work with something like the before_validation callback in active_record and then make with a validation sure the account is always set.
The same approach could be used if you always want to add the current_user to every created object.
What do you think?
To answer your second question, check out the new default_scope feature in Rails 2.3.
I understand that you don't want to bother about scoping you account all time. Lets be honest, it's a pain in the a**.
To add a bit magic and have this scoping done seamlessly give a look at the following gem
http://gemcutter.org/gems/account_scopper
Hope this helps,
--
Sebastien Grosjean - ZenCocoon

Resources