A basic hotel setup where users
are team members of a hotel
. It uses cancancan and devise with Rails 5.2
rails g scaffold User name
rails g scaffold Hotel name
rails g scaffold TeamMembership user:references hotel:references
rails g scaffold Reservation starts_on:date ends_on:date hotel:references
rails g scaffold CheckIn hotel:references reservation:references
hotels
are connected to users
via has_many :users, through: :team_memberships
. And vice versa for users
to hotels
.
config/routes.rb
resources :hotels do
resources :reservations
resources :check_ins
end
app/controllers/check_ins_controller.rb
class CheckInsController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource :hotel
load_and_authorize_resource :check_in, :through => :hotel
[...]
app/models/ability.rb
[...]
can [:read, :destroy], CheckIn, hotel_id: user.hotel_ids
can [:create], CheckIn
[...]
Somewhere in a view I have this code:
<% if can? :create, CheckIn %>
<%= link_to 'Create Check-In', new_hotel_check_in_path(@hotel) %>
<% end %>
It should only be visible to team members of the @hotel
.
The first line of the ability.rb
works fine but the second line doesn't work because with that anybody can create a new check_in
but only team_memberships
should be able to create a new check_in
for their hotel.
What is the best way to solve this? Obviously the link shouldn't be displayed but also the /hotels/:hotel_id/check_ins/new
URL should not be accessible for anybody who is not a team member.
This is a common problem and this is where the business
logic intersects the authorization
logic.
There are many opinions on this matter.
1) Many people believe that this kind of intersection is unacceptable. They would advise you to do what you need this way (separating business and authorization logic)
<% if can?(:create, CheckIn) && current_user.member_of?(@hotel) %>
<%= link_to 'Create Check-In', new_hotel_check_in_path(@hotel) %>
<% end %>
2) If you are sure you need this you can do it like this:
Add a new permission to Hotel
model:
can [:check_in], Hotel do |hotel|
user.member_of?(hotel)
end
Then in the view:
<% if can?(:create, CheckIn) && can?(:check_in, @hotel) %>
<%= link_to 'Create Check-In', new_hotel_check_in_path(@hotel) %>
<% end %>
In the controller:
class CheckInsController < ApplicationController
# ...
def new
authorize! :check_in, @hotel
# ...
end
end