I am building an app in Rails 4 using Cancancan for role authorization. I have an ability defined that works for individual records but not the :index action.
I have a user role called 'RVSP'. RVSPs are assigned WorkOrders, from which the RVSP will create a Record.
class WorkOrder < ActiveRecord::Base
has_one :record
belongs_to :rvsp, class_name: "User"
end
class Record < ActiveRecord::Base
belongs_to :work_order
end
Here's my problem: I want RVSP users to be able to read or update Records where the WorkOrder was assigned to that RVSP. Here's the ability definition so far:
can [:read, :update], Record, Record.for_rvsp(user.id) do |record|
work_order = record.work_order
user.id == work_order.rvsp_id
end
The scope Record.for_rvsp is this monstrosity:
scope :for_rvsp, -> (rvsp_id) { where( work_order: { rvsp: { id: rvsp_id } } ) }
So: How do I define the ability and/or query for all Records where the Record's WorkOrder's rvsp_id matches the current user's id? I suspect it's a join of some kind but I'm new to this and haven't done that before.
P.S. I thought of just adding a created_by column to Record, but I already have that through PaperTrail, which I'm using for an audit trail. But Record.versions.first.whodunnit is about the same as Record.work_order.rvsp_id as far as difficulty of the query is concerned. Thanks for any advice you can offer!
P.P.S. I'm using Postgres but I'd rather not do any database-specific SQL if I can avoid it.
I started banging on joins and it seems to be working! I replaced the scope above with the following class method:
def self.for_rvsp(rvsp_id)
joins(:work_order).where(work_orders: {rvsp_id: rvsp_id})
end
I also tidied up the ability in Ability.rb:
can [:read, :update], Record, Record.for_rvsp(user.id) do |record|
user.id == record.work_order.rvsp_id
end
So, as far as index actions are concerned, it appears that you need to do the same thing in two places: you define a scope that fetches the records you need, and then you define the ability in the block. The scope ensures that you load all the right records, and the ability block authorizes them. Seems to work for me anyway!
I know this is super late, but you can traverse the associations in CanCanCan.
can [:read, :update], Record, work_order: { rsvp_id: user.id }
Related
User has many Posts
Post has many Comments
User has many Comments
class Post < ApplicationRecord
..stuff...
scope :created_by, ->(user) { where(creator: user) }
scope :with_comments_by, ->(user) { joins(:comments).where('comments.creator_id = ?'. user.id) }
##########========= this is my failure:
scope :related_to, ->(user) { created_by(user).or(with_comments_by(user) }
(not my real models, just sticking with SO basic app structure)
This last scope doesn't work, as is clearly noted:
The two relations must be structurally compatible, they must be scoping the same model, and they must differ only by WHERE or HAVING.
So, how do I get around this? (please don't say a messy, long SQL sentence)
I want to be able to call Posts.related_to(user) and get one ActiveRecord collection of all posts the user created or commented on.
I was headed down this path, but I know this is perverse:
class Post < ApplicationRecord
..stuff...
scope :created_by, ->(user) { where(creator: user) }
scope :with_comments_by, ->(user) { joins(:comments).where('comments.creator_id = ?'. user.id) }
##########========= this is my failure:
# scope :related_to, ->(user) { created_by(user).or(with_comments_by(user) }
def self.related_to(user)
ary = []
ary << Post.created_by(user).map(&:id)
ary << Post.with_comments_by(user).map(&:id)
Post.find(ary.uniq)
# so...bad...so yucky
end
Help me, SO community. I'm trapped in my own mind.
Your with_comments_by scope isn't quite what you want. That scope should be finding posts whose comments have a comment from user so you should be saying exactly that:
scope :with_comments_by, ->(user) { where(id: Comment.select(:post_id).where(creator_id: user.id)) }
You should be able to use that scope with your or in related_to without any complaints.
This version of with_comments_by will also neatly take care of the duplicate posts that your JOIN could produce if someone has commented multiple times on one post.
I am trying to learn how to use scopes in my Rails 5 app.
I have asked a background question here.
have models in my Rails 5 app for User, Proposal and Potential.
Users create Proposals which they themselves and others can then create comments on.
The associations between models are:
User
has_many :proposals, dependent: :destroy
has_many :potentials
Proposal
belongs_to :user
has_many :potentials, inverse_of: :proposal
accepts_nested_attributes_for :potentials, reject_if: :all_blank, allow_destroy: true
Potential
belongs_to :proposal, inverse_of: :potentials
belongs_to :user
In my routes file, I have two resources for potentials. I'm not sure if I've gone off piste with this bit- I cant find an example of how to do this otherwise. I have both:
resources :potentials
as well as:
resources :proposals do
resources :potentials
Objective:
When the user who made the proposal tries to edit it, I only want that user to be able to edit the potentials that they created themselves.
The reason I have two routes set up for potentials is that the nested resource has a nested form fields inside the proposal form, so that the proposal creator can make a potential in that way. Any other user that sees the proposal and makes a potential does it via a separate form.
Any user (including the proposal creator, can edit the potential via that separate form), and the proposal creator can also edit any of its own proposals by the nested form in the proposal form.
At the moment, whenever I edit the proposal form (even when I don't edit the potential nested fields), all of the potentials are updated to insert the proposal creator's user id overriding the actual potential creator's user id.
Solution
I am trying to limit the edit action in the proposals controller, so that it only allows the proposal /potentials to be edited if they have the user_id == the proposal.user_id.
For this purpose, I have written scopes in my proposal.rb
scope :owner_potentials, ->{ where(user_id: potential.user_id ) }
scope :third_party_potentials, ->{ where(user_id: != potential.user_id) }
The solution in the post i liked above was to try using a scope. Since scopes are meant to work on the class, rather than an instance, I'm stuck in trying to figure out how to adapt them so that I can use the scope to search for all the compliant potentials (i.e. potentials where potential.user_id == proposal.user_id). That means Im not searching the Proposal class, Im searching the specific proposal.
This post suggested defining Event.all inside the relevant controller action, but then how would I limit that so it only applied to the specific potentials edit line? I have other lines in my edit action which should not be tested on the Proposal table, but just the instance. If this were able to work, I imagine I would then need to rewrite my scope to try to exclude all the other proposals.
Is there a way to use an edit action in a controller with a scope, on a specific instance?
I would suggest scopes like this:
scope :owner_potentials, -> (user_id) { where(user_id: user_id) }
scope :third_party_potentials, -> (user_id) { where.not(user_id: user_id) }
When calling these scopes you just need to pass current user's id.
Scopes define queries for the AR class they are defined in. You say you have written owner_potentials and third_party_potentials scopes in proposal.rb. But if these scopes are meant to return a collection of potentials, then these should be defined in the Potential class. If you need to access these scopes from a proposal record, you can chain scopes to associations, e.g.
class Potential
scope :owner_potentials, -> (user) { where(user: user) }
scope :third_party_potentials, -> (user) { where.not(user: user) }
end
...
class ProposalsController # Proposals::PotentialsController..? imo Proposals::PotentialsController#edit sounds like an endpoint for editing exactly one potential record and nothing else, which doesn't sound like what you want. Your call on how to structure the controller/routes though.
def edit
#proposal = ... # Whatever your logic is to find the proposal
#proposal.potentials.owner_potentials(current_user) # do something with the user's potentials
#proposal.potentials.third_party_potentials(current_user) # do something with the potentials the user doesn't own
end
end
You can see here how you chain an association (.potentials) to a scope (.owner_potentials).
Also, if you have an association, you can treat that association as a field in a where method, a la where(user: user) instead of where(user_id: user.id).
Last thing to note is that you probably want to change the name of the scopes with this refactor.
potentials.owner_potentials(user) is a bit redundant. Maybe something like potentials.owned_by(user) ?
I'm working on implementing a tagging system and I'm having problem querying for tagged objects with a scope.
For example, I would like to find all the user's items with a certain tag. With a class method I can currently find all the objects:
def self.tagged_with(name)
Tag.find_by_name(name).items
end
However, this has a problem. If I were to do something like: current_user.items.tagged_with(name) won't this existing method return ALL the items and not just items owned by the current_user? I suppose this is a simply querying issue but I can't figure out how to change a class method into something called on a collection. I have tried going the opposite way, to get a the collection through the tags, something like... tag.items.where(:user_id => current_user.id) but in this case, it's a many-to-many relationship and I haven't been able to get on thumb on this either.
What's the proper way to restrict a query like this?
Create an association on your User class that points to your Tag class.
class User < ActiveRecord::Base
has_many :tags
end
Then you can do:
current_user.tags.where(...)
If you don't already have an association in place, you'll need to create a migration to have the tags table reference your users table with a foreign key.
I think this will help you:
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
So, basically you can define your method tagged_with directly into the association!
This example is took from the documentations ActiveRecord::Associations
I have a model structure like this.
User
|
+-- Order
|
+-- OrderItem
|
+-- OrderItemNote
Order holds a direct relation with User, so I can do :user_id => user.id in the Ability class for User.role? :customer (for example) to let them edit their own orders.
However, short of adding :user_id columns to OrderItem and OrderItemNote I have no idea how to configure CanCan to allow the user to edit their own OrderItem and OrderItemNote objects.
Can anyone help out?
Update:
I went with jvnill's answer, because I'm using RailsAdmin (and it's simpler to do an Ability class, rather than a load_resource. In normal circumstances, CanCan's load_resource is a better way to go.
Note: while the first level of nesting is handled by:
can :manage, OrderItem, order: { user_id: user.id }
The deeper level nesting...:
can :manage, OrderItemNote, order_item: { order: { user_id: user.id } }
... threw a SQL error
no such column: order_items.orders
and it appears that CanCan treats deeper level references (ie. order in this case) as database field references, not through the model (I tried a few tricks with virtual attributes etc. to provide an order_id or even the user_id but anything other than a real database field name is not recognised.) - I'm on limited time to investigate this further, I'm leaving these notes, for myself, so I can look into it later.
In the end I decided the deeper level nesting was unnecessary, and included the note in the order item itself, so the model is like so:
User
|
+-- Order
|
+-- OrderItem (which includes a note)
simplest way to do this is like this
can :manage, OrderItem, order: { user_id: user.id }
can :manage, OrderItemNote, order_item: { order: { user_id: user.id } }
If loading all intermediate objects makes sense and if you have adapted routes (ie with all necessary ids):
class OrderItemNotesController
load_resource :order, through: :current_user
load_resource :order_item, through: :order
load_resource :order_item_note, through: :order_item
This way, you don't need any ability: all objects are retrieved through associations.
Should really be adapted to situations of course (and don't forget you can still do: load_and_authorize_resource if needed)
I am trying to implement specific object (row) authorisation using cancan, I want it to work in a way that a user can only make a change(update/edit) to a Record if he/she has the role for that specific Record. after consulting the cancan docs I tried doing the following:
class Ability
include CanCan::Ability
def initialize(user)
can :manage, Record do |record|
user.can_edit(record)
end
end
end
class User
has_many :assignments
has_many :roles_as_editor, :through => :assignments, :class_name => "Role", :source => :role, :conditions => {:edit => true}
def rec_as_editor
self.roles_as_editor.collect{ |x| Record.where(:cp_secondary_id => x.record_id) }.flatten.uniq
end
def can_edit(rec)
rec_as_editor.include?(rec)
end
end
The can_edit method takes in a Record object and ensures that a User has the role necessary to make a change to it by returning true or false. this method is tested and works correctly so the problem seems to be with the CanCan code because when i try editing a Record that the user dosent hold the role for it still allows me to make changes to the Record, anyone know why this wont work?
If you require any further information please let me know through a comment.
Thank You
Are you authorizing the resource in the controller?
you should have load_and_authorize_resource in your controller
or
def edit
#critical_process = CriticalProcess.find(params[:id])
#this here is what you use
authorize! :edit, #critical_process
end
in your edit method inside the critical process controller.
I personally prefer to keep this logic completely separate from models so that I don't have to dig into model code to find authorization issues. In other words, user.can_edit checks for authorization which is what the ability file is supposed to be in charge of. Shouldn't matter though... in this case I think you might have a problem inside the can_edit method. I have used code that looks nearly identical to yours without problems many times like this:
can :manage, Record do |record|
user.has_role?(:record_manager)
end
I suggest including your code for can_edit or use the debugger to see what value gets returned from can_edit.
I think the problem comes from the way you query for the records that are supposed to have the user as an editor.
I copy/pasted your code and built the other associations from scratch.
And testing it in the console it works as expected when I use it:
>> u = User.last
>> a = Ability.new(u)
>> a.can :edit, Role.last
false
The only thing I changed is the query for the records: it seemed to look for a record that owns the role (your Role has a record_id) but then looks for the same key under cp_secondary_id.
I think something is wrong in your query, but what depends on your schema and associations:
roles_as_editor.collect{ |x| Record.where(:cp_secondary_id => x.record_id) }.flatten.uniq
as I understood your code we are traversing associations like this:
User=>Assignment<=Role(boolean edit flag)<=Record