Rails cancancan fetching records for specific user - ruby-on-rails

I need to fetch only specific records from the database on a certain condition using CanCanCan. This is my data model.
class User < ApplicationRecord
has_many :regions_users
end
class RegionsUser < ApplicationRecord
belongs_to :region
end
class Region < ApplicationRecord
has_many :regions_users
has_many :sites
end
class Site < ApplicationRecord
belongs_to :region
has_many :offers
end
class Offer < ApplicationRecord
belongs_to :site
end
For example, a user is assigned to two regions, which means that the user
user.regions_users returns and array of two RegionsUser [RegionsUser1, RegionsUser2]. RegionsUser1 belongs to a Region A and RegionsUser2 belongs to Region B so if I am logged in as the user I am a region user that can oversee the data belonging to those regions. Now, I need to display Offers that belong to those regions (Region A and Region B). That means, that the user cannot access an offer belonging to a region other than Region A and Region B, so accessing /offers/3 should raise Access Denied error.
I can pull the regions from RegionsUser:
region_ids = user.regions_users.pluck(:regions_id)
and then the Offers:
Offer.joins(:site).where(sites: { region_id: region_ids })
I read about defining abilities using a block described here
can :update, Project, ["priority < ?", 3] do |project|
project.priority < 3
end
but I can't think of any solution for my case. I would appreciate any ideas.
UPDATE
This kinda works, but instead of displaying Access Denied page it raises 404 Page Not Found error.
offers_controller.rb
def offer
#offer ||= Offer.accessible_by(current_ability).find(params[:id])
end
ability.rb
if user.region_user?
region_ids = user.regions_users.pluck(:region_id)
can :read, Offer, site: { region_id: region_ids }
end
UPDATE 2
I could catch ActiveRecord::RecordNotFound and raise CanCanCan::AccessDenied but that is a workaround. I would expect CanCanCan to handle the authorization part. I could just pull the records in the controller and raise an expcetion but that doesn't really relate to CanCanCan, does it?

I think you need load_and_authorize_resource in your controllers. I don't think it will raise the
raise CanCan::AccessDenied.new("Not authorized!", :read, Article) unless you do that.
The accessible_by(currrent_ability) simply scoped the result set to those defined in your ability file

Related

Rails Company-specific user permissions with Pundit

I'm actually not sure if this is a Pundit or general permissions architectural problem, but I setup a simple Pundit policy to restrict the actions a member within a company can perform. Users are joined as a Member to a company in a has_many, through: relationship. The Member model has a role attribute of owner or user.
Given a User that is a member of a Store, how can I restrict the access in a controller for the User's association to the Store? Below is a Admin::MembersController where a store owner can invite other members. How can I restrict this to the given User in pundit through their member association to the store? The policy below doesn't work, returning an array of records. If I were to check against only the first record it works but I feel that is because of my limited understanding.
All of the tutorials and documentation I see online for CCC and Pundit
involve application-wide permissions. But I need more granular
control.
For example, my application has hundreds of companies. Each company
has a user who is an "owner" and they login each day to look at their
earnings information. That owner/user wants to invite Joe Smith to the
application so they can also look at the data and make changes. But
they don't want Joe Smith to be able to see certain types of data. So
we restrict Joe Smith's access to certain data for that company.
class Admin::MembersController < Admin::BaseController
def index
#company_members = current_company.members
authorize([:admin, #company_members])
end
end
Policy
class Admin::MemberPolicy < ApplicationPolicy
def index?
return [ record.user_id, record.store_id ].include? user.id
## this works return [ record.first.user_id, record.first.store_id ].include? user.id
end
end
User.rb
class User < ApplicationRecord
# Automatically remove the associated `members` join records
has_many :members, dependent: :destroy
has_many :stores, through: :members
end
Member.rb
class Member < ApplicationRecord
belongs_to :store
belongs_to :user
enum role: [ :owner, :user ]
end
Store.rb
class Store < ApplicationRecord
has_many :members
has_many :users, through: :members
end
I got some insight from the contributors on Pundit; the most reasonable way to go about it this is to use a domain object which represents the context that a user is in - there is information about this in the Readme (https://github.com/varvet/pundit#additional-context). The UserContext object will provide references to a user and organization.
class ApplicationController
include Pundit
def pundit_user
if session[:organization_id]
UserContext.new(current_user, Organization.find(session[:organization_id]))
else
UserContext.new(current_user)
end
end
end
class UserContext
attr_reader :user, :organization
def initialize(user, organization = nil)
#user = user
#organization = organization
end
end
I think what you are looking for is scopes in pundit. You want to restrict certain data access to members of store and show that data to owner of that store.
For that purpose you need to change your query according to the user role
Something like this:
class EarningPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
# here check for owner and show all data
if user.members.owner # here you query your memberships to find the owner role.
scope.all
else
# here show only data relevant to member
scope.where(published: true)
end
end
end
end
You can now use this class like this in your controller
def index
#earnings = earning_scope(Earning)
end
Hope it helps

Multi tenant rails app with devise

I'm trying to build a multi tenanted app in which which different banks are separated by subdomain. This part is working fine. Now there is one more level of multitenancy for bank products.
Each bank has multiple products
A devise user can belong to only on product
This means that you will have to register twice for two products of the same bank even though they are under same subdomain(client requirement can't change)
Because of this you can have same email address for two products. Uniqueness is scoped to product_id
So I have to select a product while signing in and signing up
This is how I'm trying to implement above solution
around_filter :scope_current_bank, :scope_current_product
before_filter :authenticate_user!
helper_method :current_bank, :current_product
def current_bank
#current_bank = Bank.find_by_subdomain!(request.subdomains.first)
end
def current_product
if user_signed_in?
#current_product = current_bank.products.find_by_id(params[:product_id])
else
#current_product = current_user.product
end
end
def scope_current_bank
Bank.current_id = current_bank.id
yield
ensure
Bank.current_id = nil
end
def scope_current_product
Product.current_id = (current_product.id rescue nil)
yield
ensure
Product.current_id = nil
end
Now the problem is while user is sigining in, the scope_current_product method calls user_signed_in?, obviously it fails because product_id is nil. Now it enters the else block after which I expect it to call authenticate_user! as its a before_filter but it does not happen as authentication was already done. So I get a message saying authentication failed.
Is their any way to call authenticate_user again?
Although not a direct answer, hopefully this will give you some ideas:
Authorization
Perhaps you should look at - Is there a difference between authentication and authorization? - there's a good RailsCast about this
I think your issue comes down to the idea you need to authenticate the user once (login / logout), but should then authorize that user to work with different resources
Code
A devise user can belong to only on product - I would recommend this:
#app/models/product_user.rb
Class ProductUser < ActiveRecord::Base
belongs_to :product
belongs_to :user
end
#app/models/product.rb
Class Product < ActiveRecord::Base
has_many :product_users
has_many :users, through: :product_users
end
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :product_users
has_many :products, through: :product_users
end
This is a typical has_many :through association:
#user.products
#product.users
CanCan
It means you can use CanCan to do something like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user
can :manage, Product, users.exists?(user.id)
else
can :read, :all
end
end
end
This allows you to control which products the user can edit / access. Obviously my code needs to be tweaked, but I hope it shows you the value of authorization over trying to do multiple authentications

Cancan Authorization Forum

I'm working on a project using RoR, a social message board (internet forum), in which every user can create multiple Boards and join multiple Boards from other users.
I didn't want to reinvent the wheel so I'm using Devise for Authentication and CanCan for Authorization. However I'm having some issues implementing CanCan because of the following:
class Board < ActiveRecord::Base
has_many :memberships
has_many :users , :through => :memberships
end
class User < ActiveRecord::Base
has_many :memberships
has_many :boards, :through => :memberships
end
class Membership < ActiveRecord::Base
ROLE = ['Administrator', 'Moderator','Member', 'Banned']
belongs_to :user
belongs_to :board
end
The role doesn't belong to the user himself, it belongs to the relationship between the user and the board, that is the Membership. So it's not enough knowing who is the current_user I also need to know which board is being displayed, so I think I would have to send the Membership instead of the user to the Ability class initializer? Any guidance would be greatly appreciated.
You're on the right path.
If you haven't already, create this as an entirely new Ability. e.g. BoardAbility. I've found it useful to not be shy about passing-in additional dependencies, and to have CanCan do as much of the evaluation that's reasonable.
class BoardAbility
include CanCan::Ability
attr_reader :requested_by, :requested_resource
def initialize requested_by, requested_resource
return nil unless (requested_by.is_a?(User) && requested_resource.is_a?(Board))
#requested_by = requested_by
#requested_resource = requested_resource
default_rules
end
private
def default_rules
# common abilities to all users
can :flag_offensive, :all
can :view_thread_count, :all
# find this user's role to this board to define more abilities
role = Membership.where(user_id: requested_by.id, board_id: requested_resource.id).pluck(:role).first
if ['Administrator', 'Moderator'].include? role
can :ban_users, Board, {id: requested_resource.id}
end
end
end
Then in your BoardController define a private method to signify that we aren't using the default CanCan Ability class.
def current_ability
#current_ability ||= BoardAbility.new(current_user, #board)
end
Then when you're in your BoardController, use the usual CanCan DSL.
authorize! :ban_user, #board

How to implement "business rules" in Rails?

What is the way to implement "business rules" in Rails?
Let us say I have a car and want to sell it:
car = Cars.find(24)
car.sell
car.sell method will check a few things:
does current_user own the car?
check: car.user_id == current_user.id
is the car listed for sale in the sales catalog?
check: car.catalogs.ids.include? car.id
if all o.k. then car is marked as sold.
I was thinking of creating a class called Rules:
class Rules
def initialize(user,car)
#user = user
#car = car
end
def can_sell_car?
#car.user_id == #user.id && #car.catalogs.ids.include? #car.id
end
end
And using it like this:
def Car
def sell
if Rules.new(current_user,self).can_sell_car
..sell the car...
else
#error_message = "Cannot sell this car"
nil
end
end
end
As for getting the current_user, I was thinking of storing it in a global variable?
I think that whenever a controller action is called, it's always a "fresh" call right? If so then storing the current user as a global variable should not introduce any risks..(like some other user being able to access another user's details)
Any insights are appreciated!
UPDATE
So, the global variable route is out! Thanks to PeterWong for pointing out that global variables persist!
I've now thinking of using this way:
class Rules
def self.can_sell_car?(current_user, car)
......checks....
end
end
And then calling Rules.can_sell_car?(current_user,#car) from the controller action.
Any thoughts on this new way?
I'd use the following tables:
For buyers and sellers:
people(id:int,name:string)
class Person << ActiveRecord::Base
has_many :cars, :as => :owner
has_many :sales, :as => :seller, :class_name => 'Transfer'
has_many :purchases, :as => :buyer, :class_name => 'Transfer'
end
cars(id:int,owner_id:int, vin:string, year:int,make:string,model:string,listed_at:datetime)
listed_at is the flag to see if a Car is for sale or not
class Car << ActiveRecord::Base
belongs_to :owner, :class_name => 'Person'
has_many :transfers
def for_sale?
not listed_at.nil?
end
end
transfers(id:int,car_id:int,seller_id:int,buyer_id:int)
class Transfer << ActiveRecord::Base
belongs_to :car
belongs_to :seller, :class_name => 'Person'
belongs_to :buyer, :class_name => 'Person'
validates_with Transfer::Validator
def car_owned_by_seller?
seller_id == car.owner_id
end
end
Then you can use this custom validator to setup your rules.
class Transfer::Validator << ActiveModel::Validator
def validate(transfer)
transfer.errors[:base] = "Seller doesn't own car" unless transfer.car_owned_by_seller?
transfer.errors[:base] = "Car isn't for sale" unless transfer.car.for_sale?
end
end
First, the standard rails practice is to keep all business logic in the models, not the controllers. It looks like you're heading that direction, so that's good -- BUT: be aware, there isn't a good clean way to get to the current_user from the model.
I wouldn't make a new Rules model (although you can if you really want to do it that way), I would just involve the user model and the car. So, for instance:
class User < ActiveRecord::Base
...
def sell_car( car )
if( car.user_id == self.id && car.for_sale? )
# sell car
end
end
...
end
class Car < ActiveRecord::Base
...
def for_sale?
!catalog_id.nil?
end
...
end
Obviously I'm making assumptions about how your Catalog works, but if cars that are for_sale belong_to a catalog, then that method would work - otherwise just adjust the method as necessary to check if the car is listed in a catalog or not. Honestly it would probably be a good idea to set a boolean value on the Car model itself, this way users could simply toggle the car being for sale or not for sale whenever you want them to ( either by marking the car for sale, or by adding the car to a catalog, etc. ).
I hope this gives you some direction! Please feel free to ask questions.
EDIT: Another way to do this would be to have methods in your models like:
user.buy_car( car )
car.transfer_to( user )
There are many ways to do it putting the logic in the object its interacting with.
I would think this would a prime candidate for using a database, and then you could use Ruby to query the different tables.
You might take a look at the declarative authorization gem - https://github.com/stffn/declarative_authorization
While it's pre-configured for CRUD actions, you can easily add your own actions (buy, sell) and put their business logic in the authorization_rules.rb config file. Then, in your controllers, views, and even models!, you can easily ask permitted_to? :buy, #car
I'm doing something similar with users and what they can do with photo galleries. I'm using devise for users and authentication, and then I set up several methods in the user model that determine if the user has various permissions (users have many galleries through permissions) to act on that gallery. I think it looks like the biggest problem you are having is with determining your current user, which can be handled quite easily with Devise, and then you can add a method to the user model and check current_user.can_sell? to authorized a sale.

Creating associations by using checkboxes

A User can only have two types of Subscriptions: DailySubscription and WeeklySubscription. When the user is at the new and edit action, I'd like them to check off either of the subscriptions they would like to get.
I'm comfortable using nested fields (as per Ryan Bates' screencast here) but I think when I add inheritance, it really complicating matters. Is there a better way?
class User < ActiveRecord::Base
has_many :subscriptions
end
class Subscription < ActiveRecord::Base
belongs_to :user
# type field is defined in the migration for Single Table Inheritance
end
class DailySubscription < Subscription
# Business logic here
end
class WeeklySubscription < Subscription
# Different business logic here
end
My initial efforts with the controller are wacky:
class UsersController < ApplicationController
def new
#user = User.new
# I can't use #user. subscriptions.build as Rails doesn't
# know what type of model to add!
#user.subscriptions = [DailySubscription.new, WeeklySubscription.new]
end
...
end
I think I am conceptually really missing something here but I can't figure it out. Help!
Judging from your description, your user has only two possible subscription choices: daily and/or weekly. Therefore you dont need to have a has_many association because two has_ones would suffice(note polymorphic subscribeable below:
class User < ActiveRecord::Base
has_one :daily_subscription, :as => :subscribeable
has_one :weekly_subscription, :as => :subscribeable
end
class Subscription < ActiveRecord::Base
belongs_to :subscribeable, :polymorphic => true
# type field is defined in the migration for Single Table Inheritance
end
class DailySubscription < Subscription
# Business logic here
end
class WeeklySubscription < Subscription
# Different business logic here
end
furthermore for the controller you just need to initialize User. Upon initialization, #user.daily_subscription and weekly_subscription will be null as determined by .blank? method. When you go ahead and create the user in the create method, you will need to populate these fields with instances of corresponding subscriptions.
class UsersController < ApplicationController
def new
#user = User.new
# bam -- youre done.
end
...
end

Resources