profiles: %{local: false, remote: false},
activities: %{local: false, remote: false}
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
]
}
]
+ },
+ %{
+ group: :pleroma,
+ key: Pleroma.Web.ApiSpec.CastAndValidate,
+ type: :group,
+ children: [
+ %{
+ key: :strict,
+ type: :boolean,
+ description:
+ "Enables strict input validation (useful in development, not recommended in production)",
+ suggestions: [false]
+ }
+ ]
}
]
hostname: "localhost",
pool_size: 10
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
+
if File.exists?("./config/dev.secret.exs") do
import_config "dev.secret.exs"
else
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
+
if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs"
else
* `remote`
* `activities` - statuses
* `local`
- * `remote`
\ No newline at end of file
+ * `remote`
+
+## Pleroma.Web.ApiSpec.CastAndValidate
+
+* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.
defp normalize_counter(counter) when is_integer(counter), do: counter
defp normalize_counter(_), do: 0
- defp maybe_update_follow_information(data) do
+ def maybe_update_follow_information(user_data) do
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
- {:ok, info} <- fetch_follow_information_for_user(data) do
- info = Map.merge(data[:info] || %{}, info)
- Map.put(data, :info, info)
+ {_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
+ {_, true} <-
+ {:collections_available,
+ !!(user_data[:following_address] && user_data[:follower_address])},
+ {:ok, info} <-
+ fetch_follow_information_for_user(user_data) do
+ info = Map.merge(user_data[:info] || %{}, info)
+
+ user_data
+ |> Map.put(:info, info)
else
+ {:user_type_check, false} ->
+ user_data
+
+ {:collections_available, false} ->
+ user_data
+
{:enabled, false} ->
- data
+ user_data
e ->
Logger.error(
- "Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
+ "Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
)
- data
+ user_data
end
end
--- /dev/null
+# Pleroma: A lightweight social networking server
+# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
+# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.CastAndValidate do
+ @moduledoc """
+ This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
+ (https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
+ The main difference is ignoring unexpected query params instead of throwing
+ an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
+ to disable this behavior. Also, the default rendering error module
+ is `Pleroma.Web.ApiSpec.RenderError`.
+ """
+
+ @behaviour Plug
+
+ alias Plug.Conn
+
+ @impl Plug
+ def init(opts) do
+ opts
+ |> Map.new()
+ |> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
+ end
+
+ @impl Plug
+ def call(%{private: %{open_api_spex: private_data}} = conn, %{
+ operation_id: operation_id,
+ render_error: render_error
+ }) do
+ spec = private_data.spec
+ operation = private_data.operation_lookup[operation_id]
+
+ content_type =
+ case Conn.get_req_header(conn, "content-type") do
+ [header_value | _] ->
+ header_value
+ |> String.split(";")
+ |> List.first()
+
+ _ ->
+ nil
+ end
+
+ private_data = Map.put(private_data, :operation_id, operation_id)
+ conn = Conn.put_private(conn, :open_api_spex, private_data)
+
+ case cast_and_validate(spec, operation, conn, content_type, strict?()) do
+ {:ok, conn} ->
+ conn
+
+ {:error, reason} ->
+ opts = render_error.init(reason)
+
+ conn
+ |> render_error.call(opts)
+ |> Plug.Conn.halt()
+ end
+ end
+
+ def call(
+ %{
+ private: %{
+ phoenix_controller: controller,
+ phoenix_action: action,
+ open_api_spex: private_data
+ }
+ } = conn,
+ opts
+ ) do
+ operation =
+ case private_data.operation_lookup[{controller, action}] do
+ nil ->
+ operation_id = controller.open_api_operation(action).operationId
+ operation = private_data.operation_lookup[operation_id]
+
+ operation_lookup =
+ private_data.operation_lookup
+ |> Map.put({controller, action}, operation)
+
+ OpenApiSpex.Plug.Cache.adapter().put(
+ private_data.spec_module,
+ {private_data.spec, operation_lookup}
+ )
+
+ operation
+
+ operation ->
+ operation
+ end
+
+ if operation.operationId do
+ call(conn, Map.put(opts, :operation_id, operation.operationId))
+ else
+ raise "operationId was not found in action API spec"
+ end
+ end
+
+ def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
+
+ defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ end
+
+ defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
+ case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
+ {:ok, conn} ->
+ {:ok, conn}
+
+ # Remove unexpected query params and cast/validate again
+ {:error, errors} ->
+ query_params =
+ Enum.reduce(errors, conn.query_params, fn
+ %{reason: :unexpected_field, name: name, path: [name]}, params ->
+ Map.delete(params, name)
+
+ %{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
+ path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
+ update_in(params, path, &List.delete(&1, value))
+
+ _, params ->
+ params
+ end)
+
+ conn = %Conn{conn | query_params: query_params}
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ end
+ end
+
+ defp list_items_to_string(list) do
+ Enum.map(list, fn
+ i when is_atom(i) -> to_string(i)
+ i -> i
+ end)
+ end
+
+ defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
+end
def call(conn, errors) do
errors =
Enum.map(errors, fn
+ %{name: nil, reason: :invalid_enum} = err ->
+ %OpenApiSpex.Cast.Error{err | name: err.value}
+
%{name: nil} = err ->
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
@local_mastodon_name "Mastodon-Local"
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
:skip_plug,
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
- plug(OpenApiSpex.Plug.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug(
@oauth_read_actions [:show, :index]
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
+ import ExUnit.CaptureLog
+ import Mock
import Pleroma.Factory
import Tesla.Mock
- import Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
u3: %{r1: r3_1.id, r2: r3_2.id},
u4: %{r1: r4_1.id}}
end
+
+ describe "maybe_update_follow_information/1" do
+ setup do
+ clear_config([:instance, :external_user_synchronization], true)
+
+ user = %{
+ local: false,
+ ap_id: "https://gensokyo.2hu/users/raymoo",
+ following_address: "https://gensokyo.2hu/users/following",
+ follower_address: "https://gensokyo.2hu/users/followers",
+ type: "Person"
+ }
+
+ %{user: user}
+ end
+
+ test "logs an error when it can't fetch the info", %{user: user} do
+ assert capture_log(fn ->
+ ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+
+ test "just returns the input if the user type is Application", %{
+ user: user
+ } do
+ user =
+ user
+ |> Map.put(:type, "Application")
+
+ refute capture_log(fn ->
+ assert ^user = ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+
+ test "it just returns the input if the user has no following/follower addresses", %{
+ user: user
+ } do
+ user =
+ user
+ |> Map.put(:following_address, nil)
+ |> Map.put(:follower_address, nil)
+
+ refute capture_log(fn ->
+ assert ^user = ActivityPub.maybe_update_follow_information(user)
+ end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+ end
+ end
end