0
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.

max
  • 76,662
  • 13
  • 84
  • 137
Brian Graham
  • 142
  • 1
  • 7
  • It would make more sense to add a `belongs_to :creator, class: User` relation to photo and use that for authorization. Are you using `CanCan` as well? – max May 27 '15 at 01:30

1 Answers1

2

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)

Edit:

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

Edit 2:

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
max
  • 76,662
  • 13
  • 84
  • 137
  • Thanks for your solution, I appreciate the clarity. Many users will have relationships with the Photo. Barry will not be the only user who has full access to the Photo; others will, too. I'd like to establish authority through roles, instead of only having one slot. – Brian Graham May 27 '15 at 02:10
  • Thanks for replying. Your edited `Ability` class is very close to what I originally had, testing to see whether the user has the role w/r/t the photo. My question is: How do I put that role onto the photo? The magic method in my sample is `only_the_photos_barry_just_made`, i.e. only those records created, not records updated. – Brian Graham May 27 '15 at 12:48
  • 1
    I added an example of how add the role in a model callback. – max May 27 '15 at 14:16