Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I default an ancestral relationship with cancan to an internal node of the tree?

I am using cancan to authorize my controller actions. One of classes where access is authorized by cancan is a tree, implemented with acts_as_ancestry. I'm having problems using load_and_authorize_resource when the user is not permitted to access the root level, but rather is allowed access starting at an interior node.

Here are some relavant class definitions:

class User < ActiveRecord::Base
  belongs_to :organization, :inverse_of => :users
end

class Post < ActiveRecord::Base
  belongs_to :organization, :inverse_of => :posts
end

class Organization < ActiveRecord::Base
  has_ancestry :cache_depth => true
  has_many :users, :inverse_of => :organization
  has_many :posts, :inverse_of => :organization
end

The rules for managing posts are "You can manage posts in any organization below yours". My cancan abilities definition is this:

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new 

    # subtree_ids is added by acts_as_ancestry
    can :manage, Post, {:organization_id => user.organization.subtree_ids}
  end
end

In the controller, I have this (other actions omitted)

class PostsController < ApplicationController
  load_and_authorize_resource :post

  def index
  end

  def new
  end
end

Everything works fine when the authorized user belongs to the root organization. However, when I login as a user authorized at an internal node, the index action works fine, but when the new action is invoked, I get a can-can authorization error.

Here is what I see in the log:

Access denied on new #<Post id: nil, organization_id: 1>

The organization_id 1 (the root) is coming from the schema:

create_table "posts", :force => true do |t|
  t.integer  "organization_id", :default => 1
end

With cancan, the new action will build a new Post and assign it to @post. When it does this, it will initialize all the attributes with values taken from the can definition in Abilities.rb. However, it will not do anything if those attributes are Arrays, Hashes or Ranges and the default value ends up coming from the schema.

How can I authorize users to manage posts in their subtree, but when they create a new post, default it to their organization?

like image 994
John Naegle Avatar asked Oct 04 '22 14:10

John Naegle


2 Answers

In cancan, if the @post variable is already initialized by you, it will not call load_resource on it, only do the authorize part. See this part of the docs: https://github.com/ryanb/cancan/wiki/Authorizing-controller-actions, "Override loading".

So the simplest solution is to take control of the initialization yourself and make it what you need, like here:

class PostsController < ApplicationController
  before_filter :initialize_post, :only => [:new, :create]

  def initialize_post
    @post = current_user.organization.posts.build(params[:post]||{:name=>'Smashing Kittens'})
  end

  load_and_authorize_resource :post
  def index
  end

  def new
  end

  def create
  end
end

You can see it working in this test project that I created from your post: https://github.com/robmathews/cancan_test.

like image 61
Rob Avatar answered Oct 13 '22 10:10

Rob


I had a similar issue and ended up writing ancestry related permissions in blocks like so:

can :manage, Post do |post|
  post.organization.subtree_ids.include?(user.organization_id)
end
like image 20
Arjan Avatar answered Oct 13 '22 12:10

Arjan