Reputation: 152
class Photo < ActiveRecord::Base
has_many :item_photos
has_many :items, through: :item_photos
end
class Item < ActiveRecord::Base
has_many :item_photos
has_many :photos, through: :item_photos
accepts_nested_attributes_for :photos
end
class ItemPhotos < ActiveRecord::Base
belongs_to :photo
belongs_to :item
end
When I edit or create an Item, I also upload or remove Photos. However, more than one user can view an Item. These users should only be able to view their own Photos.
Item #1 has three Photos. Amy has access to one. Barry has access to two. Barry loads up /items/1
and edits it. He deletes one Photo, ignores the other, and adds two new Photos.
class ItemsController < ApplicationController
def update
if @item.update(item_params)
# Give Barry access to the Photo he just made.
@item.only_the_photos_barry_just_made.each do |p|
current_user.add_role :viewer, p
end
end
end
end
I don't want to pollute models/photo.rb
with methods to access session information (like current_user
). Is there an idiomatic way to get these records? If not, is there a clean way to get these records?
Thanks for any help.
Upvotes: 0
Views: 58
Reputation: 102423
A simple solution would be to add a :creator relation to photo.
rails g migration AddCreatorToPhotos creator:references
class Photo < ActiveRecord::Base
belongs_to :creator, class: User
# ...
end
And then you can simply add a rule in your Abilty
class:
class Ability
include CanCan::Ability
# Define abilities for the passed in user here.
# @see https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
def initialize(user = nil)
user ||= User.new # guest user (not logged in)
# ...
can :read, Photo do |photo|
photo.creator.id == user.id
end
end
end
You can then get the photos that can by read by the current user with:
@photos = @item.photos.accessible_by(current_ability)
If you want to authorize though roles instead you just need to alter the conditions in the authorization rule:
class Ability
include CanCan::Ability
# Define abilities for the passed in user here.
# @see https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
def initialize(user = nil)
user ||= User.new # guest user (not logged in)
# ...
can :read, Photo do |photo|
user.has_role? :viewer, photo
end
end
end
An approach to creating the role could be to add a callback to Photo
. But as you already have surmised, accessing the user via the session from a model is not a good approach.
Instead you can pass the to the user to Photo
when it is instantiated. You can either setup the belongs_to :creator, class: User
relationship or create a virtual attribute:
class Photo < ActiveRecord::Base
attr_accessor: :creator_id
end
You can then pass the user by a hidden field (remember to whitelist it!):
# GET /items/new
def new
@item = Item.new
@item.photos.build(creator: current_user) # or creator_id: current_user.id
end
<%= fields_for(:photos) do %>
<%= f.hidden_field :creator_id %>
<% end %>
So, how do we create our callback?
class Photo < ActiveRecord::Base
attr_accessor: :creator_id
after_commit :add_viewer_role_to_creator!, on: :create
def add_viewer_role_to_creator!
creator.add_role(:viewer, self)
true # We must return true or we break the callback chain
end
end
There is one issue though:
We don't want to allow malicious users to be assign their ID to existing photos on update.
We can do this by setting up some custom params whitelisting:
def item_params
whitelist = params.require(:item).permit(:foo, photos_attributes: [:a,:b, :creator_id]
current_user_photos = whitelist[:photos_attributes].select do |attrs|
attrs[:id].nil? && attrs[:creator_id] == current_user.id
end
whitelist.merge(photos_attributes: current_user_photos)
end
Upvotes: 2