George Edwards
George Edwards

Reputation: 9229

How to manage complex user abilities in Rails with CanCan

I am trying to create a web app, where users have a dashboard where they can opt into a number of widgets. I have the following many-to-many:

class CreateWidgets < ActiveRecord::Migration
  def change
    create_table :widgets do |t|
      t.string :name
      t.timestamps null: false
    end

    create_table :users_widgets, id: false do |t|
      t.belongs_to :user, index: true
      t.belongs_to :widget, index: true
    end
  end    
end

With a users model defined by the devise gem. So how can I most effectively set up some parameters which allow me to define the users ability to only see widgets they have "subscribed" to.

CanCan permissions are defined here:

class Ability
  include CanCan::Ability

  def initialize(user)
    can :read, Widget, :user_id => user.id
  end
end

Obviously, this doesn't work, as there is no parameter to track which user created the widget. So How would I set up the Widget / User models? Possibly add a list of entitlements to the user model? It could be a whole load of bool attributes, one for each type of widget, but obviously, that might get a bit messy, how should I tackle this in a security conscious way?

Update:

My models now read:

class User < ActiveRecord::Base
  has_many :subscriptions
  has_many :widgets, through: :subscriptions
end

class Widget < ActiveRecord::Base
  has_many :subscriptions
  has_many :users, through: :subscriptions
end

class Subscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :widget
  # make sure you add a unique DB index as well.
  validates_uniqueness_of :user_id, scope: :widget_id
end

and my model/ability.rb file reads:

class Ability
    include CanCan::Ability

    def initialize(user)
        can :read, Widget do |widget|
            widget.subscriptions.where(user: user).any?
        end
    end
end

But it still doesn't work, I expect this is due to the note about unique db index note, but I am not sure what to add for this?

Upvotes: 0

Views: 88

Answers (1)

max
max

Reputation: 102250

You would first need to create a many to many join model between User and Widget.

class User < ActiveRecord::Base
  has_many :subscriptions
  has_many :widgets, through: :subscriptions
end

class Widget < ActiveRecord::Base
  has_many :subscriptions
  has_many :users, through: :subscriptions
end

class Subscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :widget
  # make sure you add a unique DB index as well.
  validates_uniqueness_of :user_id, scope: :widget_id
end

You would then authorize by passing a block to can:

can :read, Widget do |widget|
  widget.subscriptions.where(user: user).any?
end

Upvotes: 3

Related Questions