アルパカログ

Elixir Phoenixでエンドポイント毎の認可を実装する方法

2020年12月07日
🔖プログラミング🔖Elixir

Webの管理画面など、ユーザー毎にアクセスできるエンドポイントを制限(認可)したいということがあります。

ElixirのWebアプリケーションフレームワークであるPhoenixは、バージョンによってディレクトリ構造がガラッと変わるなど、バージョンアップで比較的大きな変更があるため、認可のためのライブラリはすぐには見つかりません。

そこでこのエントリでは、Phoenixのプラグ機能を使ってエンドポイント毎の認可を実装する方法を説明します。なお、Phoenixのバージョンは1.4系を想定しています。

Plugの作成

Phoenixでエンドポイント毎の認可制御を行うためにはPlugを作成するのがおそらくもっとも簡単です。

Phoenixでは自作したPlugを各モジュールで読み込んだり、ルーティングのパイプラインとして仕込むことができます。

エンドポイント毎の認可を考えたとき、Controller plugsとRouter plugsの2通りの方法が考えられますが、今回はController plugsとして作成していきます。

認可を行うMyApp.Plug.EnsureAuthorizedを下記のように定義します。

defmodule MyApp.Plug.EnsureAuthorized do
  @moduledoc """
  エンドポイント毎のアクセス認可を行うPlug
  """

  def init(default), do: default

  @spec call(Plug.Conn.t(), any()) :: Plug.Conn.t() | any()
  def call(conn, _params) do
    controller = conn |> Phoenix.Controller.controller_module() |> to_string()
    action = conn |> Phoenix.Controller.action_name() |> Atom.to_string()

    conn
    |> MyApp.Guardian.Plug.current_resource()
    |> authorized?(controller, action)
    |> case do
      true -> conn
      _ -> conn |> MyApp.FallbackController.call({:error, :forbidden})
    end
  end

  defp authorized?(user, controller, action) do
    # Check authorization
  end
end

Controller plugsでは、Phoenix.Controller.controller_module/1でルーティングされた現在のControllerモジュールを取得できます。また、アクション名はPhoenix.Controller.action_name/1で取得できます。どちらもAtomであることに注意してください。

認可を行うということはその前段で認証しており「ユーザーが誰であるか(もしくは認証されていないユーザーか)」がわかっていると思います。上記のサンプルでは認証にGuardianを使用しています。

現在のユーザーが、アクセスしているController, actionのペアに対して認可されているかをauthorized?/3でチェックしてください。

認可されていない場合はFallbackControllerに処理を任せるのが良いでしょう。FallbackController (Action Fallback) については下記を参照してください。

全てのエンドポイントの取得

エンドポイント毎の認可を実装するには、前提としてエンドポイント(Controllerおよびaction)が全てわかっている必要があります。

Controllerとactionの全てのペアはMyApp.Router.__routes__から下記のように取得します。

  @spec all_endpoints() :: list(%{controller: String.t(), action: String.t()})
  def all_endpoints() do
    endpoints_from_router =
      MyApp.Router.__routes__()
      |> Enum.map(fn route ->
        %{controller: "#{route.plug}", action: route.plug_opts |> Atom.to_string()}
      end)

    endpoints_from_router
    |> Enum.uniq_by(&(&1[:controller] <> "." <> &1[:action]))
  end

Phoenix.Router.resourcesを使ってルーティングを生成している場合、更新のupdateエンドポイントはPUTPATCHの両方を許容するため、同じパスのエンドポイントが2つになります。

そのため上記のサンプルではController, actionのペアで重複を除外しています。

認可プラグを追加する

認可プラグを全てのControllerで有効にします。

全てのControllerを対象にする場合、下記のようにMyApp.controller/0に追加するのが良いでしょう。

defmodule MyApp do
  def controller() do
    quote do
      use Phoenix.Controller, namespace: MyApp
      import Plug.Conn
      alias MyApp.Router.Helpers, as: Routes

      plug MyApp.Plug.EnsureAuthorized # <= 追加

      action_fallback MyApp.FallbackController
    end
  end
end

これで全てのController, actionの前にMyApp.Plug.EnsureAuthorizedが呼ばれ認可を行うことができるようになりました。

以上です。このエントリでは、Phoenixのプラグ機能を使ってエンドポイント毎の認可を実装する方法を説明しました。

タグ