Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Model.accessible_through #721

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/cancan/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ def model_adapter(model_class, action)
adapter_class.new(model_class, relevant_rules_for_query(action, model_class))
end

# @private
def relation_model_adapter(model_class, action, subject, relation)
::CanCan::ModelAdapters::AbstractAdapter
.adapter_class(model_class)
.new(model_class, relevant_rules_for_relation(model_class, action, subject, relation))
end

# See ControllerAdditions#authorize! for documentation.
def authorize!(action, subject, *args)
message = args.last.is_a?(Hash) && args.last.key?(:message) ? args.pop[:message] : nil
Expand Down
12 changes: 12 additions & 0 deletions lib/cancan/ability/rules.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ def relevant_rules_for_match(action, subject)
end
end

def relevant_rules_for_relation(model_class, action, subject, relation)
relevant_rules(action, subject).map do |rule|
case rule.conditions
when Hash
conditions = rule.conditions[relation] || rule.conditions[relation.to_sym]
Rule.new(rule.base_behavior, action, model_class, conditions, rule.block)
else
raise Error, "accessible_through is only available with hash conditions"
end
end
end

def relevant_rules_for_query(action, subject)
rules = relevant_rules(action, subject).reject do |rule|
# reject 'cannot' rules with attributes when doing queries
Expand Down
69 changes: 69 additions & 0 deletions lib/cancan/model_additions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,75 @@ def accessible_by(ability, action = :index, strategy: CanCan.accessible_by_strat
ability.model_adapter(self, action).database_records
end
end

# Provides a scope within the model to find instances of the model that are accessible
# by the given ability within the given action/subject permission pair.
#
# I.E.:
#
# Given the scenario below:
#
# class Department < ActiveRecord::Base
# end
#
# class User < ActiveRecord::Base
# belongs_to :department
# end
#
# class Ability
# include CanCan::Ability
#
# def initialize(user)
# can :contact, User, { department: { id: user.department_id } }
# can :contact, User, { department: { id: user.managing_department_ids } } if user.manager?
# end
# end
#
# The following would give you a list of departments that the given ability can contact their users:
#
# > user = User.new(department_id: 13, manager: false)
# > ability = Ability.new(user)
# > Department.accessible_through(ability, :contact, User).to_sql
# => SELECT * FROM departments WHERE id = 13
# #
# > user = User.new(department_id: 13, managing_department_ids: [2, 3, 4], manager: true)
# > ability = Ability.new(user)
# > Department.accessible_through(ability, :contact, User).to_sql
# => SELECT * FROM departments WHERE ((id = 13) OR (id IN (2, 3, 4)))
#
# Sometimes the name of the relation doesn't match the model:
#
# class User < ActiveRecord::Base
# has_many :managing_users, class_name: "User", foreign_key: :managed_by_id
# end
#
# class Ability
# include CanCan::Ability
#
# def initialize(user)
# can :contact, User, { managing_users: { id: user.department_id } }
# end
# end
#
# When that happens, you can override it with `relation`. This would give you a list of departments
# that the given ability can contact their users:
#
# > user = User.new(department_id: 13, manager: false)
# > ability = Ability.new(user)
# > Department.accessible_through(ability, :contact, User, relation: :managing_users).to_sql
# => SELECT * FROM departments WHERE id = 13
# >
# > user = User.new(department_id: 13, managing_department_ids: [2, 3, 4], manager: true)
# > ability = Ability.new(user)
# > Department.accessible_through(ability, :contact, User, relation: :managing_users).to_sql
# => SELECT * FROM departments WHERE ((id = 13) OR (id IN (2, 3, 4)))
#
def accessible_through(ability, action, subject, relation: model_name.element, strategy: CanCan.accessible_by_strategy)
CanCan.with_accessible_by_strategy(strategy) do
ability.relation_model_adapter(self, action, subject, relation)
.database_records
end
end
end

def self.included(base)
Expand Down
114 changes: 114 additions & 0 deletions spec/cancan/model_adapters/accessible_through_integration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# frozen_string_literal: true

require 'spec_helper'

# integration tests for latest ActiveRecord version.
RSpec.describe CanCan::ModelAdapters::ActiveRecord5Adapter do
let(:ability) { double.extend(CanCan::Ability) }
before :each do
connect_db
ActiveRecord::Migration.verbose = false

ActiveRecord::Schema.define do
create_table(:divisions) do |t|
end

create_table(:departments) do |t|
t.string :name
t.integer :division_id
end

create_table(:employees) do |t|
t.integer :department_id
end
end

class Division < ActiveRecord::Base
end

class Department < ActiveRecord::Base
belongs_to :division
has_many :employees
end

class Employee < ActiveRecord::Base
belongs_to :department
end
end

before do
@division1 = Division.create!
@division2 = Division.create!
@department1 = Department.create!(division: @division1)
@department2 = Department.create!(division: @division2, name: "People")
@department3 = Department.create!(division: @division2)
@user1 = Employee.create!(department: @department1)
@user2 = Employee.create!(department: @department2)
@user3 = Employee.create!(department: @department3)
end

it "selects the correct objects through the association" do
ability.can :message, Employee, { department: { id: @department1.id } }
ability.can :message, Employee, { department: { id: @department2.id } }

departments = Department.accessible_through(ability, :message, Employee)

expect(departments.pluck(:id)).to match_array [@department1.id, @department2.id]
end

it "treats no condition unconditional" do
ability.can :message, Employee, { department: { id: @department1.id } }
ability.can :message, Employee

# Finds all departments that ability can message employees from
departments = Department.accessible_through(ability, :message, Employee)

expect(departments.pluck(:id)).to match_array [@department1.id, @department2.id, @department3.id]
end

it "unallowing yields impossible condition" do
ability.can :message, Employee, { department: { id: @department1.id } }
ability.cannot :message, Employee

# Finds all departments that ability can message employees from
departments = Department.accessible_through(ability, :message, Employee)

expect(departments.pluck(:id)).to be_empty
end

describe 'preloading of associatons' do
it 'preloads associations correctly' do
ability.can :message, Employee, { department: { division: { id: @department1.id } } }

department = Department.accessible_through(ability, :message, Employee)
.includes(:division).first

expect(department).to eql @department1
expect(department.association(:division)).to be_loaded
end
end

describe 'filtering of results' do
it 'adds the where clause correctly' do
ability.can :message, Employee, { department: { division: { id: [@department1.id, @department2.id] } } }

department = Department.accessible_through(ability, :message, Employee)
.where("name LIKE 'Peo%'").first

expect(department).to eql @department2
end
end

if CanCan::ModelAdapters::ActiveRecordAdapter.version_greater_or_equal?('5.0.0')
describe 'selecting custom columns' do
it 'extracts custom columns correctly' do
ability.can :message, Employee, { department: { division: { id: @department2.id } } }

department = Department.accessible_through(ability, :message, Employee)
.select('name as title').first

expect(department.title).to eql @department2.name
end
end
end
end