Pundit在调用authorize对象的循环上引发AuthorizationNotPerformedError

时间:2016-02-07 20:12:15

标签: ruby-on-rails authorization pundit

我有两个rails app: 一个是“前端应用程序”,负责显示数据,从用户获取输入并将数据发送到API(第二个应用程序)。第二个是处理数据库操作并将JSON发送到前端应用程序的API。

我有一个操作,我的用户决定他想要在rooms中创建多少hotel,以及他应该在每个beds中创建多少room。前端应用程序上的表单如下所示:

<h1>Add Rooms</h1>
<form action="http://localhost:3000/hotels/<%=params[:hotel_id]%>/rooms/multiple_create" method="post">
  <input name="authenticity_token" value="<%= form_authenticity_token %>" type="hidden">
  <div class="form-group">
    <%= label_tag 'room[room_number]', "Number of rooms" %>
    <input type="number" name="room[room_number]" required>
  </div>
  <div class="form-group">
    <%= label_tag "room[bed_number]", "Number of beds by room" %>
    <input type="number" name= "room[bed_number]" required>
  </div>
  <div class="form-group">
    <%= label_tag "room[content]", "Room Type" %>
    <input type="text" name= "room[content]" required>
  </div>
  <input type="submit">
    <% if @errors %>
    <ul class="list-unstyled">
      <%@errors.each do |error|%>
        <li class="has-error"><%=error%></li>
      <% end -%>
    </ul>
  <% end -%>
</form>

此表单链接到前端应用上的RoomsController#multiple_create操作,该操作负责将表单数据发送到API:

class RoomsController < ApplicationController

  def multiple_new
  end

  def multiple_create
    @response =   HTTParty.post(ENV['API_ADDRESS']+'api/v1/hotels/'+ params[:hotel_id]+'/rooms/multiple_create',
    :body => { :room =>{
                  :room_number => params[:room][:room_number],
                  :bed_number => params[:room][:bed_number],
                }
             }.to_json,
    :headers => { 'X-User-Email' => session[:user_email], 'X-User-Token'=> session[:user_token], 'Content-Type' => 'application/json' } )
    # Erreur Mauvais Auth Token
    if @response["error"] == "You need to sign in or sign up before continuing."
      redirect_to unauthorized_path
    # erreur de validation
    elsif @response["error"]
      raise
      @errors = @response["errors"]
      render :multiple_new
    else
      raise
      redirect_to account_path(account_id)
    end
  end
end

我的room_controller.rb中有相应的方法,负责创建房间,每个房间的床位和每张床的插槽。在API上我使用Pundit进行授权。

  def multiple_create
    i = 0
    start_date = Date.today
    ActiveRecord::Base.transaction do
      while i < params[:room_number].to_i
        @room = @hotel.rooms.build(room_params)
        authorize @room
        if @room.save
          (0...params[:bed_number].to_i).each do
            @bed = @room.beds.create!
            for a in 0...60
              @bed.slots.create!(available: true, date: start_date + a.days )
            end
          end
          i+=1
        else
          break
        end
      end
    end
    if i == params[:room_number]
      render json: {success: "All rooms and beds where successfully created"}
    else
      render json: {error: "There was a problem during room creation process. Please try again later"}
    end
  end

每当我尝试发布这种方法时,我得到:

Pundit::AuthorizationNotPerformedError - Pundit::AuthorizationNotPerformedError:
  pundit (1.0.1) lib/pundit.rb:103:in `verify_authorized

在我看来,在循环中保存新房间之前,我实际上是在调用authorize @room。我的room_policy.rb

中有一个方法multiple_create
  def multiple_create?
    (user && record.hotel.account.admin == user) || (user && user.manager && (user.account == record.hotel.account))
  end

在我的API base_controller中,我有:

after_action :verify_authorized, except: :index

为什么我在这里得到这个权威错误?

1 个答案:

答案 0 :(得分:0)

这是一个非常有创意的解决方案 - 但是有一种更简单,更好的方式来处理插入/更新多个记录。

class Hotel
  has_many :rooms
  accepts_nested_attributes_for :rooms
end

accepts_nested_attributes_for表示您可以使用以下内容创建会议室

@hotel = Hotel.new(
  rooms_attributes: [
     { foo: 'bar' },
     { foo: 'baz' }
  ]
)

@hotel.save # will insert both the hotel and rooms

您可以创建一个这样的表单:

<%= form_for(@hotel) |f| %>
  <%= f.fields_for :rooms do |rf| %>
   <%= f.foo %>
  <% end %>
<% end %>

并在控制器中处理它,如下所示:

class HotelsController

  def update
    @hotel.update(hotel_params)
    respond_with(@hotel)
  end

  private 
    def hotel_params
      params.require(:hotel).permit(rooms_attributes: [ :foo ])
    end
end

您可以使用多个accepts_nested_attributes_forfields更深入地嵌套它。但总的来说,当你超过一个级别时,这就是一个严重的代码味道。将其拆分为多个控制器。

请注意,您确实不需要采取create_multipleupdate_multiple行动。

回到问题的核心,我如何验证它? 保持简单愚蠢

def update
  authorize @hotel
  # ...
end

并在HotelPolicy

中处理
def update?
  return false unless user
  record.account.admin == user || (user.manager && (user.account == record.account))
end

修改

根据您对要做的事情的描述,您只需在酒店模型上添加自定义的getter / setter。

class Hotel < ActiveRecord::Base
  has_many :rooms

  # custom getter for binding form inputs
  def number_of_rooms
    rooms.count
  end

  # custom setter which actually creates the associated records
  # this subtracts the existing number of rooms so that setting
  # hotel.number_of_rooms = 50 on an existing record with 20 rooms
  # will result in a total of 50 not 70.
  def number_of_rooms=(number)
    (number.to_i - number_of_rooms).times { rooms.new }
  end
end

当你创建或更新这样的记录时:

[14] pry(main)> h = Hotel.new(number_of_rooms: 3)
=> #<Hotel:0x007feeb6ee2b20 id: nil, created_at: nil, updated_at: nil>
[15] pry(main)> h.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "hotels" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2016-02-08 13:17:06.723803"], ["updated_at", "2016-02-08 13:17:06.723803"]]
  SQL (0.2ms)  INSERT INTO "rooms" ("hotel_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["hotel_id", 3], ["created_at", "2016-02-08 13:17:06.726157"], ["updated_at", "2016-02-08 13:17:06.726157"]]
  SQL (0.4ms)  INSERT INTO "rooms" ("hotel_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["hotel_id", 3], ["created_at", "2016-02-08 13:17:06.728636"], ["updated_at", "2016-02-08 13:17:06.728636"]]
  SQL (0.1ms)  INSERT INTO "rooms" ("hotel_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["hotel_id", 3], ["created_at", "2016-02-08 13:17:06.730291"], ["updated_at", "2016-02-08 13:17:06.730291"]]
   (1.6ms)  commit transaction
=> true
[16] pry(main)> 

它将添加构建N个关联的房间。然后,您可以正常保存记录。

只需添加number_of_rooms的表单输入,即可将其添加到正常的酒店更新/创建操作中:

<%= form_for(@hotel) |f| %>
  <% # ... %>
  <%= f.label :number_of_rooms %>
  <%= f.number_field :number_of_rooms %>
<% end %>

此输入将在params[:hotel][:number_of_rooms]中。您应该将其添加到强参数白名单中。

请注意,您无法为每个房间设置属性。按照上面的建议进行授权。