lib/guardian/permissions.ex

defmodule Guardian.Permissions do
  @moduledoc """
  An optional plugin to Guardian to provide permissions for your tokens.

  These can be used for any token types since they only work on the `claims`.

  Permissions are set on a per implementation module basis.
  Each implementation module can have their own sets.
  Permissions are similar in concept to OAuth2 scopes. They're encoded into a token
  and the permissions granted last as long as the token does.
  This makes it unsuitable for highly dynamic permission schemes.
  They're best left to an application to implement.

  For example. (at the time of writing) some of the Facebook permissions are:

  * public_profile
  * user_about_me
  * user_actions.books
  * user_actions.fitness
  * user_actions.music

  To create permissions for your application similar to these:

  ```elixir
  defmodule MyApp.Auth.Token do

    use Guardian, otp_app: :my_app,
                           permissions: %{
                           default: [:public_profile, :user_about_me]
                           user_actions: %{
                             books: 0b1,
                             fitness: 0b100,
                             music: 0b1000,
                           }
                         }

    use Guardian.Permissions, encoding: Guardian.Permissions.BitwiseEncoding
    # Per default permissions will be encoded Bitwise, but other encoders also exist
    #  * Guardian.Permissions.TextEncoding
    #  * Guardian.Permissions.AtomEncoding
    # It is even possible to supply your own encoding module

    # snip

    def build_claims(claims, _resource, opts) do
      claims =
        claims
        |> encode_permissions_into_claims!(Keyword.get(opts, :permissions))
      {:ok, claims}
    end
  end
  ```

  This will take the permission set in the `opts` at `:permissions` and
  put it into the `"pems"` key of the claims as a map of `%{set_name => integer}`.

  The permissions can be defined as a list (positional value based on index)
  or a map where the value for each permission is manually provided.

  They can be provided either as options to `use Guardian` or in the config for
  your implementation module.

  Once you have a token, you can interact with it.

  ```elixir
  # Get the encoded permissions from the claims
  found_perms = MyApp.Auth.Token.decode_permissions_from_claims(claims)

  # Check if all permissions are present
  has_all_these_things? =
    claims
    |> MyApp.Auth.Token.decode_permissions_from_claims
    |> MyApp.Auth.Token.all_permissions?(%{default: [:user_about_me, :public_profile]})

  # Checks if any permissions are present
  show_any_media_things? =
    claims
    |> MyApp.Auth.Token.decode_permissions_from_claims
    |> MyApp.Auth.Token.any_permissions?(%{user_actions: [:books, :fitness, :music]})
  ```

  ### Using with Plug

  To use a plug for ensuring permissions you can use the `Guardian.Permissions` module as part of a
  Guardian pipeline.

  ```elixir
  # After a pipeline has setup the implementation module and error handler

  # Ensure that both the `public_profile` and `user_actions.books` permissions
  # are present in the token
  plug Guardian.Permissions, ensure: %{default: [:public_profile], user_actions: [:books]}

  # Allow the request to continue when the token contains any of the permission sets specified
  plug Guardian.Permissions, one_of: [
    %{default: [:public_profile], user_actions: [:books]},
    %{default: [:public_profile], user_actions: [:music]},
  ]

  # Look for permissions for a token in a different location
  plug Guardian.Permissions, key: :impersonate, ensure: %{default: [:public_profile]}
  ```

  If the token satisfies either the permissions listed in `ensure` or one of
  the sets in the `one_of` key the request will continue. If not, then
  `auth_error` callback will be called on the error handler with
  `auth_error(conn, {:unauthorized, reason}, options)`.
  """

  @type label :: atom
  @type permission_label :: String.t() | atom
  @type permission :: pos_integer
  @type permission_set :: [permission_label, ...] | %{optional(label) => permission}
  @type t :: %{optional(label) => permission_set}

  @type input_label :: permission_label
  @type input_set :: permission_set | permission
  @type input_permissions :: %{optional(input_label) => input_set}

  @type plug_option ::
          {:ensure, permission_set}
          | {:one_of, [permission_set, ...]}
          | {:key, atom}
          | {:module, module}
          | {:error_handler, module}

  defmodule PermissionNotFoundError do
    defexception [:message]
  end

  defmacro __using__(opts \\ []) do
    # Credo is incorrectly identifying an unless block with negated condition 2017-06-10
    # credo:disable-for-next-line /\.Refactor\./
    quote do
      alias Guardian.Permissions.PermissionNotFoundError
      import unquote(Keyword.get(opts, :encoding, Guardian.Permissions.BitwiseEncoding))

      defdelegate max(), to: Guardian.Permissions

      raw_perms = @config_permissions.()

      unless raw_perms do
        raise "Permissions are not defined for #{to_string(__MODULE__)}"
      end

      @normalized_perms Guardian.Permissions.normalize_permissions(raw_perms)
      @available_permissions Guardian.Permissions.available_from_normalized(@normalized_perms)

      @doc """
      Lists all permissions in a normalized way using
      `%{permission_set_name => [permission_name, ...]}`.
      """

      @spec available_permissions() :: Guardian.Permissions.t()
      def available_permissions, do: @available_permissions

      @doc """
      Decodes permissions from the permissions found in claims (encoded to integers) or
      from a list of permissions.

         iex> MyTokens.decode_permissions(%{default: [:public_profile]})
         %{default: [:public_profile]}

         iex> MyTokens.decode_permissions{%{"default" => 1, "user_actions" => 1}}
         %{default: [:public_profile], user_actions: [:books]}

      When using integers (after encoding to claims), unknown bit positions are ignored.

          iex> MyTokens.decode_permissions(%{"default" => -1})
          %{default: [:public_profile, :user_about_me]}
      """
      @spec decode_permissions(Guardian.Permissions.input_permissions() | nil) :: Guardian.Permissions.t()
      def decode_permissions(nil), do: %{}

      def decode_permissions(map) when is_map(map) do
        for {k, v} <- map, Map.get(@normalized_perms, to_string(k)) != nil, into: %{} do
          key = k |> to_string() |> String.to_atom()
          {key, do_decode_permissions(v, k)}
        end
      end

      @doc """
      Decodes permissions directly from a claims map. This does the same as `decode_permissions` but
      will fetch the permissions map from the `"pem"` key where `Guardian.Permissions places them
      when it encodes them into claims.
      """
      @spec decode_permissions_from_claims(Guardian.Token.claims()) :: Guardian.Permissions.t()
      def decode_permissions_from_claims(%{"pem" => perms}), do: decode_permissions(perms)
      def decode_permissions_from_claims(_), do: %{}

      @doc """
      Encodes the permissions provided into the claims in the `"pem"` key.
      Permissions are encoded into an integer inside the token corresponding
      with the value provided in the configuration.
      """
      @spec encode_permissions_into_claims!(
              Guardian.Token.claims(),
              Guardian.Permissions.input_permissions() | nil
            ) :: Guardian.Token.claims()
      def encode_permissions_into_claims!(claims, nil), do: claims

      def encode_permissions_into_claims!(claims, perms) do
        encoded_perms = encode_permissions!(perms)
        Map.put(claims, "pem", encoded_perms)
      end

      @doc """
      Checks to see if any of the permissions provided are present
      in the permissions (previously extracted from claims).

          iex> claims |> MyTokens.decode_permissions() |> any_permissions?(%{user_actions: [:books, :music]})
          true
      """
      @spec any_permissions?(
              Guardian.Permissions.input_permissions(),
              Guardian.Permissions.input_permissions()
            ) :: boolean
      def any_permissions?(has_perms, test_perms) when is_map(test_perms) do
        has_perms = decode_permissions(has_perms)
        test_perms = decode_permissions(test_perms)

        Enum.any?(test_perms, fn {k, needs} ->
          has_perms |> Map.get(k) |> do_any_permissions?(MapSet.new(needs))
        end)
      end

      defp do_any_permissions?(nil, _), do: false

      defp do_any_permissions?(list, needs) do
        matches = MapSet.intersection(needs, MapSet.new(list))
        MapSet.size(matches) > 0
      end

      @doc """
      Checks to see if all of the permissions provided are present
      in the permissions (previously extracted from claims).

          iex> claims |> MyTokens.decode_permissions() |> all_permissions?(%{user_actions: [:books, :music]})
          true

      """
      @spec all_permissions?(
              Guardian.Permissions.input_permissions(),
              Guardian.Permissions.input_permissions()
            ) :: boolean
      def all_permissions?(has_perms, test_perms) when is_map(test_perms) do
        has_perms_bits = decode_permissions(has_perms)
        test_perms_bits = decode_permissions(test_perms)

        Enum.all?(test_perms_bits, fn {k, needs} ->
          has = Map.get(has_perms_bits, k, [])
          MapSet.subset?(MapSet.new(needs), MapSet.new(has))
        end)
      end

      @doc """
      Encodes the permissions provided into numeric form.

          iex> MyTokens.encode_permissions!(%{user_actions: [:books, :music]})
          %{user_actions: 9}

      """
      @spec encode_permissions!(Guardian.Permissions.input_permissions() | nil) :: Guardian.Permissions.t()
      def encode_permissions!(nil), do: %{}

      def encode_permissions!(map) when is_map(map) do
        for {k, v} <- map, into: %{} do
          key = String.to_atom(to_string(k))
          {key, do_encode_permissions!(v, k)}
        end
      end

      @doc """
      Validates that all permissions provided exist in the configuration.

          iex> MyTokens.validate_permissions!(%{default: [:user_about_me]})

          iex> MyTokens.validate_permissions!(%{not: [:a, :thing]})
          raise Guardian.Permissions.PermissionNotFoundError

      """
      def validate_permissions!(map) when is_map(map) do
        Enum.all?(&do_validate_permissions!/1)
      end

      defp do_decode_permissions(other), do: do_decode_permissions(other, "default")

      defp do_decode_permissions(value, type) when is_atom(type),
        do: do_decode_permissions(value, to_string(type))

      defp do_decode_permissions(value, type) when is_integer(value) do
        decode(value, type, @normalized_perms)
      end

      defp do_decode_permissions(value, type) do
        do_validate_permissions!({type, value})
        decode(value, type, @normalized_perms)
      end

      defp do_encode_permissions!(value, type) when is_atom(type),
        do: do_encode_permissions!(value, to_string(type))

      defp do_encode_permissions!(value, type) when is_integer(value) do
        encode(value, type, @normalized_perms)
      end

      defp do_encode_permissions!(value, type) do
        do_validate_permissions!({type, value})
        encode(value, type, @normalized_perms)
      end

      defp do_validate_permissions!({type, value}) when is_atom(type),
        do: do_validate_permissions!({to_string(type), value})

      defp do_validate_permissions!({type, map}) when is_map(map) do
        list = map |> Map.keys() |> Enum.map(&to_string/1)
        do_validate_permissions!({type, list})
      end

      defp do_validate_permissions!({type, list}) when is_list(list) do
        perm_set = Map.get(@normalized_perms, type)

        if perm_set do
          provided_set = list |> Enum.map(&to_string/1) |> MapSet.new()
          known_set = perm_set |> Map.keys() |> MapSet.new()

          diff = MapSet.difference(provided_set, known_set)

          if MapSet.size(diff) > 0 do
            message = "#{to_string(__MODULE__)} Type: #{type} Missing Permissions: #{Enum.join(diff, ", ")}"
            raise PermissionNotFoundError, message: message
          end

          :ok
        else
          raise PermissionNotFoundError, message: "#{to_string(__MODULE__)} - Type: #{type}"
        end
      end

      defp do_validate_permissions!({type, value}) do
        do_validate_permissions!({type, [value]})
      end
    end
  end

  defdelegate init(opts), to: Guardian.Permissions.Plug
  defdelegate call(conn, opts), to: Guardian.Permissions.Plug

  @doc """
  Provides an encoded version of all permissions, and all possible future permissions
  for a permission set.
  """
  def max, do: -1

  @doc false
  def normalize_permissions(perms) do
    perms = Enum.into(perms, %{})

    for {k, v} <- perms, into: %{} do
      case v do
        # A list of permission names.
        # Positional values
        list
        when is_list(list) ->
          perms =
            for {perm, idx} <- Enum.with_index(list), into: %{} do
              {to_string(perm), trunc(:math.pow(2, idx))}
            end

          {to_string(k), perms}

        # A map of permissions. The permissions should be name => bit value
        map
        when is_map(map) ->
          perms = for {perm, val} <- map, into: %{}, do: {to_string(perm), val}
          {to_string(k), perms}
      end
    end
  end

  @doc false
  def available_from_normalized(perms) do
    for {k, v} <- perms, into: %{} do
      list = v |> Map.keys() |> Enum.map(&String.to_atom/1)
      {String.to_atom(k), list}
    end
  end
end