Pundit 在调用授权对象的循环中引发 AuthorizationNotPerformedError

Pundit raising AuthorizationNotPerformedError on a loop where authorize object is called

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

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

<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

每次我尝试post这个方法时,我得到:

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

在我看来,我实际上是在循环期间保存新房间之前调用授权@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

为什么我会在这里收到专家错误?

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

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] 中。您应该将其添加到强参数白名单中。

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