Commit 9d30baca authored by feld's avatar feld

Merge branch 'develop' into refactor/notification_settings

parents 80c21100 0fe36b31
......@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- MFR policy to set global expiration for all local Create activities
- OGP rich media parser merged with TwitterCard
- Configuration: `:instance, rewrite_policy` moved to `:mrf, policies`, `:instance, :mrf_transparency` moved to `:mrf, :transparency`, `:instance, :mrf_transparency_exclusions` moved to `:mrf, :transparency_exclusions`. Old config namespace is deprecated.
- Configuration: `:media_proxy, whitelist` format changed to host with scheme (e.g. `http://example.com` instead of `example.com`). Domain format is deprecated.
<details>
<summary>API Changes</summary>
......
......@@ -512,6 +512,7 @@
attachments_cleanup: 5,
new_users_digest: 1
],
plugins: [Oban.Plugins.Pruner],
crontab: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
......
......@@ -1768,8 +1768,8 @@
%{
key: :whitelist,
type: {:list, :string},
description: "List of domains to bypass the mediaproxy",
suggestions: ["example.com"]
description: "List of hosts with scheme to bypass the mediaproxy",
suggestions: ["http://example.com"]
}
]
},
......@@ -2008,13 +2008,15 @@
label: "Pleroma Admin Token",
type: :group,
description:
"Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the `admin_token` parameter",
"Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)",
children: [
%{
key: :admin_token,
type: :string,
description: "Admin token",
suggestions: ["We recommend a secure random string or UUID"]
suggestions: [
"Please use a high entropy string or UUID"
]
}
]
},
......
......@@ -113,6 +113,11 @@
config :pleroma, :instances_favicons, enabled: true
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
streaming_enabled: true,
public_endpoint: nil
if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs"
else
......
......@@ -252,6 +252,7 @@ This section describe PWA manifest instance-specific values. Currently this opti
* `background_color`: Describe the background color of the app. (Example: `"#191b22"`, `"aliceblue"`).
## :emoji
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
......@@ -260,13 +261,14 @@ This section describe PWA manifest instance-specific values. Currently this opti
memory for this amount of seconds multiplied by the number of files.
## :media_proxy
* `enabled`: Enables proxying of remote media to the instance’s proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
* `whitelist`: List of domains to bypass the mediaproxy
* `whitelist`: List of hosts with scheme to bypass the mediaproxy (e.g. `https://example.com`)
* `invalidation`: options for remove media from cache after delete object:
* `enabled`: Enables purge cache
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
* `enabled`: Enables purge cache
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
### Purge cache strategy
......@@ -278,6 +280,7 @@ Urls of attachments pass to script as arguments.
* `script_path`: path to external script.
Example:
```elixir
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
script_path: "./installation/nginx-cache-purge.example"
......@@ -629,8 +632,7 @@ Email notifications settings.
Configuration options described in [Oban readme](https://github.com/sorentwo/oban#usage):
* `repo` - app's Ecto repo (`Pleroma.Repo`)
* `verbose` - logs verbosity
* `prune` - non-retryable jobs [pruning settings](https://github.com/sorentwo/oban#pruning) (`:disabled` / `{:maxlen, value}` / `{:maxage, value}`)
* `log` - logs verbosity
* `queues` - job queues (see below)
* `crontab` - periodic jobs, see [`Oban.Cron`](#obancron)
......@@ -815,6 +817,8 @@ or
curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites"
```
Warning: it's discouraged to use this feature because of the associated security risk: static / rarely changed instance-wide token is much weaker compared to email-password pair of a real admin user; consider using HTTP Basic Auth or OAuth-based authentication instead.
### :auth
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator.
......
......@@ -35,6 +35,10 @@ def user_agent do
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
# Scrubbers are compiled at runtime and therefore will cause a conflict
# every time the application is restarted, so we disable module
# conflicts at runtime
Code.compiler_options(ignore_module_conflict: true)
Config.Holder.save_default()
Pleroma.HTML.compile_scrubbers()
Config.DeprecationWarnings.warn()
......
......@@ -54,6 +54,7 @@ def warn do
check_hellthread_threshold()
mrf_user_allowlist()
check_old_mrf_config()
check_media_proxy_whitelist_config()
end
def check_old_mrf_config do
......@@ -65,7 +66,7 @@ def check_old_mrf_config do
move_namespace_and_warn(@mrf_config_map, warning_preface)
end
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok
@spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
def move_namespace_and_warn(config_map, warning_preface) do
warning =
Enum.reduce(config_map, "", fn
......@@ -84,4 +85,16 @@ def move_namespace_and_warn(config_map, warning_preface) do
Logger.warn(warning_preface <> warning)
end
end
@spec check_media_proxy_whitelist_config() :: :ok | nil
def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist])
if Enum.any?(whitelist, &(not String.starts_with?(&1, "http"))) do
Logger.warn("""
!!!DEPRECATION WARNING!!!
Your config is using old format (only domain) for MediaProxy whitelist option. Setting should work for now, but you are advised to change format to scheme with port to prevent possible issues later.
""")
end
end
end
......@@ -4,6 +4,9 @@
defmodule Pleroma.Plugs.AdminSecretAuthenticationPlug do
import Plug.Conn
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
def init(options) do
......@@ -11,7 +14,10 @@ def init(options) do
end
def secret_token do
Pleroma.Config.get(:admin_token)
case Pleroma.Config.get(:admin_token) do
blank when blank in [nil, ""] -> nil
token -> token
end
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
......@@ -26,9 +32,9 @@ def call(conn, _) do
def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
if admin_token == secret_token() do
assign(conn, :user, %User{is_admin: true})
assign_admin_user(conn)
else
conn
handle_bad_token(conn)
end
end
......@@ -36,8 +42,19 @@ def authenticate(conn) do
token = secret_token()
case get_req_header(conn, "x-admin-token") do
[^token] -> assign(conn, :user, %User{is_admin: true})
_ -> conn
blank when blank in [[], [""]] -> conn
[^token] -> assign_admin_user(conn)
_ -> handle_bad_token(conn)
end
end
defp assign_admin_user(conn) do
conn
|> assign(:user, %User{is_admin: true})
|> OAuthScopesPlug.skip_plug()
end
defp handle_bad_token(conn) do
RateLimiter.call(conn, name: :authentication)
end
end
......@@ -108,31 +108,48 @@ defp csp_string do
|> :erlang.iolist_to_binary()
end
defp build_csp_multimedia_source_list do
media_proxy_whitelist =
Enum.reduce(Config.get([:media_proxy, :whitelist]), [], fn host, acc ->
add_source(acc, host)
end)
defp build_csp_from_whitelist([], acc), do: acc
media_proxy_base_url = build_csp_param(Config.get([:media_proxy, :base_url]))
defp build_csp_from_whitelist([last], acc) do
[build_csp_param_from_whitelist(last) | acc]
end
upload_base_url = build_csp_param(Config.get([Pleroma.Upload, :base_url]))
defp build_csp_from_whitelist([head | tail], acc) do
build_csp_from_whitelist(tail, [[?\s, build_csp_param_from_whitelist(head)] | acc])
end
s3_endpoint = build_csp_param(Config.get([Pleroma.Uploaders.S3, :public_endpoint]))
# TODO: use `build_csp_param/1` after removing support bare domains for media proxy whitelist
defp build_csp_param_from_whitelist("http" <> _ = url) do
build_csp_param(url)
end
captcha_method = Config.get([Pleroma.Captcha, :method])
defp build_csp_param_from_whitelist(url), do: url
captcha_endpoint = build_csp_param(Config.get([captcha_method, :endpoint]))
defp build_csp_multimedia_source_list do
media_proxy_whitelist =
[:media_proxy, :whitelist]
|> Config.get()
|> build_csp_from_whitelist([])
[]
|> add_source(media_proxy_base_url)
|> add_source(upload_base_url)
|> add_source(s3_endpoint)
captcha_method = Config.get([Pleroma.Captcha, :method])
captcha_endpoint = Config.get([captcha_method, :endpoint])
base_endpoints =
[
[:media_proxy, :base_url],
[Pleroma.Upload, :base_url],
[Pleroma.Uploaders.S3, :public_endpoint]
]
|> Enum.map(&Config.get/1)
[captcha_endpoint | base_endpoints]
|> Enum.map(&build_csp_param/1)
|> Enum.reduce([], &add_source(&2, &1))
|> add_source(media_proxy_whitelist)
|> add_source(captcha_endpoint)
end
defp add_source(iodata, nil), do: iodata
defp add_source(iodata, []), do: iodata
defp add_source(iodata, source), do: [[?\s, source] | iodata]
defp add_csp_param(csp_iodata, nil), do: csp_iodata
......
......@@ -7,37 +7,18 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do
import Plug.Conn
alias Pleroma.User
alias Pleroma.Web.OAuth
def init(options) do
options
end
def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do
token = assigns[:token]
cond do
not Pleroma.Config.enforce_oauth_admin_scope_usage?() ->
conn
token && OAuth.Scopes.contains_admin_scopes?(token.scopes) ->
# Note: checking for _any_ admin scope presence, not necessarily fitting requested action.
# Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements.
# Admin might opt out of admin scope for some apps to block any admin actions from them.
conn
true ->
fail(conn)
end
def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do
conn
end
def call(conn, _) do
fail(conn)
end
defp fail(conn) do
conn
|> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.")
|> render_error(:forbidden, "User is not an admin.")
|> halt()
end
end
......@@ -60,7 +60,7 @@ def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
if score < 0.8 do
{:ok, message}
else
{:reject, nil}
{:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
end
end
......
......@@ -39,14 +39,13 @@ def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message
{:ok, message}
{:old_user, false} ->
{:reject, nil}
{:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}
{:error, _} ->
{:reject, nil}
{:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"}
e ->
Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}")
{:reject, nil}
{:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"}
end
end
......
......@@ -43,7 +43,7 @@ defp delist_message(message, _threshold), do: {:ok, message}
defp reject_message(message, threshold) when threshold > 0 do
with {_, recipients} <- get_recipient_count(message) do
if recipients > threshold do
{:reject, nil}
{:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}
else
{:ok, message}
end
......@@ -87,7 +87,7 @@ def filter(%{"type" => "Create", "object" => %{"type" => object_type}} = message
{:ok, message} <- delist_message(message, delist_threshold) do
{:ok, message}
else
_e -> {:reject, nil}
e -> e
end
end
......
......@@ -24,7 +24,7 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} =
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
string_matches?(content, pattern) or string_matches?(summary, pattern)
end) do
{:reject, nil}
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
else
{:ok, message}
end
......@@ -89,8 +89,9 @@ def filter(%{"type" => "Create", "object" => %{"content" => _content}} = message
{:ok, message} <- check_replace(message) do
{:ok, message}
else
_e ->
{:reject, nil}
{:reject, nil} -> {:reject, "[KeywordPolicy] "}
{:reject, _} = e -> e
_e -> {:reject, "[KeywordPolicy] "}
end
end
......
......@@ -12,8 +12,9 @@ def filter(%{"type" => "Create"} = message) do
reject_actors = Pleroma.Config.get([:mrf_mention, :actors], [])
recipients = (message["to"] || []) ++ (message["cc"] || [])
if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
{:reject, nil}
if rejected_mention =
Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
{:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}
else
{:ok, message}
end
......
......@@ -28,7 +28,7 @@ defp check_date(%{"object" => %{"published" => published}} = message) do
defp check_reject(message, actions) do
if :reject in actions do
{:reject, nil}
{:reject, "[ObjectAgePolicy]"}
else
{:ok, message}
end
......@@ -47,9 +47,8 @@ defp check_delist(message, actions) do
{:ok, message}
else
# Unhandleable error: somebody is messing around, just drop the message.
_e ->
{:reject, nil}
{:reject, "[ObjectAgePolicy] Unhandled error"}
end
else
{:ok, message}
......@@ -69,9 +68,8 @@ defp check_strip_followers(message, actions) do
{:ok, message}
else
# Unhandleable error: somebody is messing around, just drop the message.
_e ->
{:reject, nil}
{:reject, "[ObjectAgePolicy] Unhandled error"}
end
else
{:ok, message}
......
......@@ -38,7 +38,7 @@ def filter(%{"type" => "Create"} = object) do
{:ok, object}
true ->
{:reject, nil}
{:reject, "[RejectNonPublic] visibility: #{visibility}"}
end
end
......
......@@ -21,7 +21,7 @@ defp check_accept(%{host: actor_host} = _actor_info, object) do
accepts == [] -> {:ok, object}
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, object}
MRF.subdomain_match?(accepts, actor_host) -> {:ok, object}
true -> {:reject, nil}
true -> {:reject, "[SimplePolicy] host not in accept list"}
end
end
......@@ -31,7 +31,7 @@ defp check_reject(%{host: actor_host} = _actor_info, object) do
|> MRF.subdomains_regex()
if MRF.subdomain_match?(rejects, actor_host) do
{:reject, nil}
{:reject, "[SimplePolicy] host in reject list"}
else
{:ok, object}
end
......@@ -114,7 +114,7 @@ defp check_report_removal(%{host: actor_host} = _actor_info, %{"type" => "Flag"}
|> MRF.subdomains_regex()
if MRF.subdomain_match?(report_removal, actor_host) do
{:reject, nil}
{:reject, "[SimplePolicy] host in report_removal list"}
else
{:ok, object}
end
......@@ -159,7 +159,7 @@ def filter(%{"type" => "Delete", "actor" => actor} = object) do
|> MRF.subdomains_regex()
if MRF.subdomain_match?(reject_deletes, actor_host) do
{:reject, nil}
{:reject, "[SimplePolicy] host in reject_deletes list"}
else
{:ok, object}
end
......@@ -177,7 +177,9 @@ def filter(%{"actor" => actor} = object) do
{:ok, object} <- check_report_removal(actor_info, object) do
{:ok, object}
else
_e -> {:reject, nil}
{:reject, nil} -> {:reject, "[SimplePolicy]"}
{:reject, _} = e -> e
_ -> {:reject, "[SimplePolicy]"}
end
end
......@@ -191,7 +193,9 @@ def filter(%{"id" => actor, "type" => obj_type} = object)
{:ok, object} <- check_banner_removal(actor_info, object) do
{:ok, object}
else
_e -> {:reject, nil}
{:reject, nil} -> {:reject, "[SimplePolicy]"}
{:reject, _} = e -> e
_ -> {:reject, "[SimplePolicy]"}
end
end
......
......@@ -134,12 +134,13 @@ defp process_tag(
if user.local == true do
{:ok, message}
else
{:reject, nil}
{:reject,
"[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}
end
end
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}),
do: {:reject, nil}
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}),
do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}
defp process_tag(_, message), do: {:ok, message}
......
......@@ -14,7 +14,7 @@ defp filter_by_list(%{"actor" => actor} = object, allow_list) do
if actor in allow_list do
{:ok, object}
else
{:reject, nil}
{:reject, "[UserAllowListPolicy] #{actor} not in the list"}
end
end
......
......@@ -11,22 +11,26 @@ def filter(%{"type" => "Undo", "object" => child_message} = message) do
with {:ok, _} <- filter(child_message) do
{:ok, message}
else
{:reject, nil} ->
{:reject, nil}
{:reject, _} = e -> e
end
end
def filter(%{"type" => message_type} = message) do
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
true <-
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type),
false <-
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type),
{_, true} <-
{:accepted,
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type)},
{_, false} <-
{:rejected,
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type)},
{:ok, _} <- filter(message["object"]) do
{:ok, message}
else
_ -> {:reject, nil}
{:reject, _} = e -> e
{:accepted, _} -> {:reject, "[VocabularyPolicy] #{message_type} not in accept list"}
{:rejected, _} -> {:reject, "[VocabularyPolicy] #{message_type} in reject list"}
_ -> {:reject, "[VocabularyPolicy]"}
end
end
......
......@@ -49,7 +49,8 @@ def is_representable?(%Activity{} = activity) do
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.debug("Federating #{id} to #{inbox}")
%{host: host, path: path} = URI.parse(inbox)
uri = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
......@@ -57,8 +58,8 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
signature =
Pleroma.Signature.sign(actor, %{
"(request-target)": "post #{path}",
host: host,
"(request-target)": "post #{uri.path}",
host: signature_host(uri),
"content-length": byte_size(json),
digest: digest,
date: date
......@@ -76,8 +77,9 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
{"digest", digest}
]
) do
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
do: Instances.set_reachable(inbox)
if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
Instances.set_reachable(inbox)
end
result
else
......@@ -96,6 +98,14 @@ def publish_one(%{actor_id: actor_id} = params) do
|> publish_one()
end
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
if port == URI.default_port(scheme) do
host
else
"#{host}:#{port}"
end
end
defp should_federate?(inbox, public) do
if public do
true
......
......@@ -62,15 +62,17 @@ def fix_summary(%{"summary" => _} = object) do
def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do
addrs = map[field]
cond do
is_binary(map[field]) ->
Map.put(map, field, [map[field]])
is_list(addrs) ->
<