I have a number of resources (Trips, Schedules, etc) with actions that should be limited to just the resource's owner.
How do you implement code with a #require_owner method defined in ApplicationController to achieve this? Ideally, the code will look up the inheritance chain for the owner so the before_filter will work on a :comment that belongs_to :trip that belongs_to :user.
class TripsController < ApplicationController
belongs_to :member
before_filter :require_owner
...
end
I don't fully follow the description (would a comment really be owned by the trip owner?), but expanding slightly on jonnii's answer, here is an example that restricts the trip controller:
class ApplicationController < ActionController::Base
...
protected
# relies on the presence of an instance variable named after the controller
def require_owner
object = instance_variable_get("##{self.controller_name.singularize}")
unless current_user && object.is_owned_by?(current_user)
resond_to do |format|
format.html { render :text => "Not Allowed", :status => :forbidden }
end
end
end
end
class TripsController < ApplicationController
before_filter :login_required # using restful_authentication, for example
# only require these filters for actions that act on single resources
before_filter :get_trip, :only => [:show, :edit, :update, :destroy]
before_filter :require_owner, :only => [:show, :edit, :update, :destroy]
...
protected
def get_trip
#trip = Trip.find(params[:id])
end
end
Assuming the model looks like this:
class Trip < ActiveRecord::Base
belongs_to :owner, :class_name => 'User'
...
def is_owned_by?(agent)
self.owner == agent
# or, if you can safely assume the agent is always a User, you can
# avoid the additional user query:
# self.owner_id == agent.id
end
end
The login_required method (provided by or relying on an auth plugin like restful_authentication or authlogic) makes sure that the user is logged in and provides the user with a current_user method, get_trip sets the trip instance variable which is then checked in require_owner.
This same pattern can be adapted to just about any other resource, provided the model has implemented the is_owned_by? method. If you are trying to check it when the resource is a comment, then you'd be in the CommentsController:
class CommentsController < ApplicationController
before_filter :login_required # using restful_authentication, for example
before_filter :get_comment, :only => [:show, :edit, :update, :destroy]
before_filter :require_owner, :only => [:show, :edit, :update, :destroy]
...
protected
def get_comment
#comment = Comment.find(params[:id])
end
end
with a Comment model that looks like:
class Comment < ActiveRecord::Base
belongs_to :trip
# either
# delegate :is_owned_by?, :to => :trip
# or the long way:
def is_owned_by?(agent)
self.trip.is_owned_by?(agent)
end
end
Make sure to check the logs as you are doing this since association-dependent checks can balloon into a lot of queries if you aren't careful.
There's a few different ways to do this. You should definitely check out the acl9 plugin (https://github.com/be9/acl9/wiki/tutorial:-securing-a-controller).
If you decide you want to do this yourself, I'd suggest doing something like:
class Trip < ...
def owned_by?(user)
self.user == user
end
end
class Comment < ...
delegate :owned_by?, :to => :trip
end
# in your comment controller, for example
before_filter :find_comment
before_filter :require_owner
def require_owner
redirect_unless_owner_of(#commemt)
end
# in your application controller
def redirect_unless_owner_of(model)
redirect_to root_url unless model.owned_by?(current_user)
end
Forgive me if there are any syntax errors =) I hope this helps!
Acl9 is a authorization plugin. I'd give you the link, but I don't have cut and paste on my iPhone. If no one else provides the link by the time I get to a computer, I'll get it for you. Or you can google. Whichever. :)
I have only just started using it, but it has an extremely simple interface. You just have to create a roles table and a roles_user. Let me know how it goes if you decide to use it.
Or just use inherited resources:
InheritedResources also introduces another method called begin_of_association_chain. It’s mostly used when you want to create resources based on the #current_user and you have urls like “account/projects”. In such cases you have to do #current_user.projects.find or #current_user.projects.build in your actions.
You can deal with it just by doing:
class ProjectsController < InheritedResources::Base
protected
def begin_of_association_chain
#current_user
end
end
Related
I have been trying to add the name of the BlogCategory that a BlogPost belongs to in a URL such as this:
sitename.com/blog/category-name/blog-post-title
At the very least, I want this to render for the show of the BlogPost but am okay with it being the url for every action such as new, edit, and destroy.
I'm using the friendly_id gem, if that makes a difference.
BlogCategory Model:
class BlogCategory < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
has_many :blog_posts
# This is a self referential relation. This is where records in a table may point to other records in the same table.
has_many :sub_categories, class_name: "BlogCategory", foreign_key: :parent_id
has_many :sub_category_blog_posts, through: :sub_categories, source: :blog_posts
belongs_to :parent, class_name: 'BlogCategory', foreign_key: :parent_id, optional: true
# This is a scope to load the top level categories and eager-load their posts, subcategories, and the subcategories' posts too.
scope :top_level, -> { where(parent_id: nil).includes :blog_posts, sub_categories: :blog_posts }
def should_generate_new_friendly_id?
slug.nil? || name_changed?
end
end
BlogCategory Controller:
class BlogCategoriesController < ApplicationController
before_action :admin_user, only: [:new, :create, :edit, :update, :destroy]
before_action :set_blog_link, only: [:show, :edit, :update, :destroy]
...
private
def cat_params
params.require(:blog_category).permit(:name, :parent_id, :sub_category, :summary)
end
def main_cat
#cat = BlogCategory.parent_id.nil?
end
def set_blog_link
#blog_link = BlogCategory.friendly.find(params[:id])
redirect_to action: action_name, id: #blog_link.friendly_id, status: 301 unless #blog_link.friendly_id == params[:id]
end
end
BlogPost Model:
class BlogPost < ApplicationRecord
extend FriendlyId
friendly_id :title, use: :history
belongs_to :blog_category
validates :title, presence: true, length: { minimum: 5 }
validates :summary, uniqueness: true
default_scope {order(created_at: :desc)}
def should_generate_new_friendly_id?
slug.nil? || title_changed?
end
end
BlogPost Controller:
class BlogPostsController < ApplicationController
before_action :admin_user, only: [:new, :create, :edit, :update, :destroy]
before_action :set_post_link, only: [:show, :edit, :update, :destroy]
before_action :find_post, only: :show
...
private
def post_params
params.require(:blog_post).permit(:title, :body, :summary, :thumbnail_link, :blog_category_id)
end
def find_post
#post = BlogPost.friendly.find(params[:id])
# If an old id or a numeric id was used to find the record, then
# the request path will not match the post_path, and we should do
# a 301 redirect that uses the current friendly id.
if request.path != blog_post_path(#post)
return redirect_to #post, :status => :moved_permanently
end
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
def set_post_link
#post_link = BlogPost.friendly.find(params[:id])
redirect_to action: action_name, id: #post_link.friendly_id, status: 301 unless #post_link.friendly_id == params[:id]
end
end
Here is relevant code from my routes.rb file:
Rails.application.routes.draw do
resources :blog_categories, path: 'blog'
resources :blog_posts
end
What I've tried
I've tried the following without success but have little understanding of what I'm doing:
get 'blog/:blog_category_name/:blog_post_title', to: 'blog_posts#show', as: 'blog_post'
and also tried
resources :blog_posts, path: 'blog/:blog_category_name/:blog_post_title', except: [:new, :create]
resources :blog_posts, only: [:new, :create]
With this in my BlogPost controller inside the show method/block:
#post_url = BlogPost.find_by(title: params[:blog_post_title], blog_category_id: params[:blog_category_name])
I even tried adding the params used in the routes to the permitted list under post_params.
I also tried making a new post to see if old posts weren't linking properly because of the url structure change.
The URL's I'm getting are not utilizing the parameters I'm passing to them.
What you're doing here is really just a nested resource but with a vanity route and and slugging which doesn't actually require such a heavy hand.
The typical controller for a nested resource would look like this:
class BlogPostsController < ApplicationController
before_action :set_blog_category
before_action :set_blog, only: [:show, :edit, :update, :delete]
# GET /blog/foo/bar - your custom vanity route
# the conventional route would be
# GET /blog_categories/foo/blog_posts/bar
def show
end
# GET /blog/foo/blogs_posts -> index
# GET /blog/foo/blogs_posts/new -> new
# POST /blog/foo/blogs_posts -> create
# ...
private
def set_blog_category
#blog_category = BlogCategory.friendly.find(params[:blog_category_id])
end
def set_blog
#blog = Blog.friendly.find(params[:id])
end
end
Besides the fact that you're using friendly.find you don't actually need to do anything to do the lookup via slugs instead of the id column. If you want to find the records only by their friendly id (and not allow numerical ids) use the find_by_friendly_id method instead.
Note that :id (or _id) in a parameter name is not equal to the id column - it's just a name for the unique indentifier segment in the URI pattern.
While you can configure the name of the param its actually kind of silly as in Rails things just work when you stick with the conventions.
You can just define the vanity route for this as:
resources :blog_categories, path: 'blog', only: [] do
# the typical routes nested under "blog_posts"
resources :blogs_posts, only: [:new, :create]
# your custom vanity route should be defined last to avoid conflicts
resources :blogs_posts, path: '/', only: :show
end
Generating the URL can be done either by calling the named blog_category_blog_path helper or by using the polymorphic route helpers:
blog_category_blog_path(#blog_category, #blog_post)
redirect_to [#blog_category, #blog_post]
form_with model: [#blog_category, #blog_post]
If you have legacy URLs using a different structure that you want to redirect I would consider using a separate controller or just doing the redirect in the routes to separate out the responsibilities from this controller.
You also should avoid duplicating the authorization/authentication logic across your controllers (your admin_user method). Thats how you get security holes.
I'm trying to put a very simple authorization on my Property class in Rails 5. I've added the can :read condition to ability.rb and used load_and_authorize_resource in my controller and I can't even get it to hit the pry, let alone authorize the :show action. Am I missing something obvious?
# ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
can :read, Property do |property|
binding.pry
PropertyUser.find_by(property_id: property.id, user_id: user.id)
end
end
end
# properties_controller.rb
class PropertiesController < ApplicationController
before_action :set_property, only: [:show, :edit, :update]
load_and_authorize_resource
skip_authorize_resource :only => [:new, :create]
def show
respond_to do |format|
format.html
format.js
end
end
private
def set_property
#property = Property.find(params[:id])
end
end
Thanks.
You might need to have this code in your user.rb
delegate :can?, :cannot?, to: :ability
def ability
Ability.new(self)
end
instead of load_and_authorize_resource, you can just use authorize_resource and then check. I don't think we need delegate here because the CanCanCan will do that automatically. It will automatically add these methods to User model.
I'm new to pundit and trying to come up with the best approach for handling nested resources for the index action. I found a similar question however it doesn't deal with admin privileges and I'm just not sure if my solution feels quite right.
Let's say I have two models, a User can have many notes and a Note which belongs to a single user. Users cannot look at notes from other users unless they're an admin. At the same time, admin's are able to create their own notes and therefore must also have the ability to retrieve a list of them via their own index action.
routes.rb
resources :users, only: :show do
resources :notes
end
notes_controller.rb
class NotesController < ApplicationController
#would probably move to application_controller.rb
after_action :verify_authorized
after_action :verify_policy_scoped
def index
user = User.find(params[:user_id])
#notes = policy_scope(user.notes)
authorize user
end
#additional code
end
note_policy.rb
class NotePolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.admin? && scope != user.notes
scope
else
user.notes
end
end
end
#additional code
end
user_policy.rb
class UserPolicy < ApplicationPolicy
def index?
user == record || user.admin?
end
#additional code
end
You are overthinking it:
class NotePolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.where(user: user)
end
end
def index?
record == user || user.admin?
end
# ...
end
Note here that its a good idea to chain from the scope being passed in from policy_scope. It lets your controller set up any scopes unrelated to authorization like for example pagination.
Also in index? we are cheating slightly. Instead of passing a note instance we are just passing the user.
class NotesController < ApplicationController
before_action :set_user!, only: [:index] # ...
before_action :set_note!, only: [:show, :edit, :update, :destroy]
def index
#notes = policy_scope(Note.all)
authorize(#user)
end
# ...
private
def set_user!
#user = User.find(params[:user_id])
end
def set_note!
#note = authorize( Note.find(params[:id]) )
end
end
Using before_action in this way is a pretty good pattern as it sets up all the "member" actions for authorization.
I'm trying to build a software for small banks, which involves deals, pools (of banks) and credit facilities. For information, a facility belongs to a pool of banks, which belongs to a deal.
Below is my issue when i try to create a "facility" :
Couldn't find Pool with 'id'=
I have 3 models : Deal, Pool, Facility
class Deal < ActiveRecord::Base
has_many :banks
has_many :pools, :dependent => :destroy
end
class Pool < ActiveRecord::Base
belongs_to :deal
end
class Facility < ActiveRecord::Base
belongs_to :pool
end
Below is my Facilitys controller :
class FacilitysController < ApplicationController
before_action :authenticate_user!
before_action :set_pool
before_action :set_facility, only: [:show, :edit, :update, :destroy]
def new
#pool = Pool.find(params[:id])
#facility = Facility.new
end
def edit
#facility = Facility.find(params[:id])
end
def create
#facility = Facility.new(facilitys_params)
if #facility.save
redirect_to root_path, notice: "Facility successfully created!"
else
render "New"
end
end
def show
#facility = Facility.find(params[:id])
#facility.pool_id = #pool.id
end
def update
#facility.update(facilitys_params)
if #facility.update(facilitys_params)
redirect_to deal_facility_url(#pool, #facility), notice: "Facility successfully updated!"
else
render :edit
end
end
def destroy
#facility.destroy
end
private
def set_pool
#pool = Pool.find(params[:id])
end
def set_facility
#facility = Facility.find(params[:id])
end
def facilitys_params
params.require(:facility).permit(:name)
end
end
My routes are
resources :deals do
resources :pools, except: [:index] do
resources :facilitys, except: [:index]
end
end
Looks like you're trying to use nested resources, but you've not provided enough information.
If your routes are not configured like this:
resources :pools do
resources :facilities
end
...then please add the relevant routes to your question.
If your routes are configured like that then good, but now your set_pool is incorrect. Take a look at the output of rake routes and you should see something like this for your Facility routes:
pool_facility_index GET /pools/:pool_id/facility(.:format) facility#index
POST /pools/:pool_id/facility(.:format) facility#create
new_pool_facility GET /pools/:pool_id/facility/new(.:format) facility#new
edit_pool_facility GET /pools/:pool_id/facility/:id/edit(.:format) facility#edit
pool_facility GET /pools/:pool_id/facility/:id(.:format) facility#show
PATCH /pools/:pool_id/facility/:id(.:format) facility#update
PUT /pools/:pool_id/facility/:id(.:format) facility#update
DELETE /pools/:pool_id/facility/:id(.:format) facility#destroy
Notice how there are two params mentioned in each route, :id and :pool_id. Now take a look at your set_pool method and see which param you're using to find the Pool. You need to change that to use params[:pool_id] too.
Have a nested resource as such
class Dealer < ActiveRecord::Base
has_many :vehicles
end
and
class Vehicle < ActiveRecord::Base
belongs_to :dealer
end
below are my routes.
resources :dealers do
resources :vehicles, :except => [:index]
end
resources :vehicles, :only => [:index]
looking at the wiki at the github page for cancan I did the following:
class VehiclesController < ApplicationController
load_and_authorize_resource :dealer
load_and_authorize_resource :vehicle, :through => :dealer
def index
#vehicles = Vehicle.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #vehicles }
end
end
end
but now when the admin tries to go to the index page with the abilities:
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :admin
can :manage, :all
end
end
I get
Couldn't find Dealer with id=
what do i need to change for admin to still be able to do all the actions and yet have others be checked before they can do any action.
The problem is not that he is not authorized to this action. The problem is that CanCan tries to fetch an instance of dealer to load all its vehicles and you have not provided a :dealer_id within params[:dealer_id]. Cancan assumes you would be loading only dealer's vehicles in this controller because you used an load_and_authorieze :through. This authorization should be used within Dealers::VehiclesController.
As long as you only loading vehicles just use load_and_authorize_resource :vehicle. And because load_and_authorize will set #vehicles for you within the before filter there is also no need to load the vehicles explicitly with Vehicle.all.
load_and_authorize is just a convenient method and it assumes some defaults. Once you will come to a point where you have some more complex use case. It will be time to throw away this method and just use CanCan's authorize!, can? and a properly configured Vehicle .accessible_by (for listing) methods.
When using load_and_authorize_resource :vehicle, through: :dealer it expects to receive a dealer_id in the request in order to authorize the dealer.
Since you use except: :index in your routes dealer_id will not be automatically included in the request.
If you don't want to authorize the dealer in the index action you can do something like this (taken from Can Can wiki)
class CommentsController < ApplicationController
load_and_authorize_resource :post
load_and_authorize_resource :through => :post
skip_authorize_resource :only => :show
skip_authorize_resource :post, :only => :show
end