Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop

This commit is contained in:
sadposter 2019-12-11 10:01:04 +00:00
commit 97f67e0786
28 changed files with 526 additions and 72 deletions

View file

@ -50,6 +50,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
- User notification settings: Add `privacy_option` option. - User notification settings: Add `privacy_option` option.
- User settings: Add _This account is a_ option.
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
@ -78,6 +80,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Add Emoji reactions - Pleroma API: Add Emoji reactions
- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance - Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users - Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
- ActivityPub: Configurable `type` field of the actors.
- Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
- Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field.
</details> </details>
### Fixed ### Fixed

View file

@ -563,7 +563,10 @@
base_path: "/oauth", base_path: "/oauth",
providers: ueberauth_providers providers: ueberauth_providers
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies config :pleroma,
:auth,
enforce_oauth_admin_scope_usage: false,
oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false

View file

@ -2094,6 +2094,15 @@
type: :group, type: :group,
description: "Authentication / authorization settings", description: "Authentication / authorization settings",
children: [ children: [
%{
key: :enforce_oauth_admin_scope_usage,
type: :boolean,
description:
"OAuth admin scope requirement toggle. " <>
"If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <>
"(client app must support admin scopes). If `false` and token doesn't have admin scope(s)," <>
"`is_admin` user flag grants access to admin-specific actions."
},
%{ %{
key: :auth_template, key: :auth_template,
type: :string, type: :string,

View file

@ -2,6 +2,13 @@
Authentication is required and the user must be an admin. Authentication is required and the user must be an admin.
Configuration options:
* `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle.
If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes).
If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions.
Note that client app needs to explicitly support admin scopes and request them when obtaining auth token.
## `GET /api/pleroma/admin/users` ## `GET /api/pleroma/admin/users`
### List users ### List users

View file

@ -66,6 +66,8 @@ Has these additional fields under the `pleroma` object:
- `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
- `discoverable`: boolean, true when the user allows discovery of the account in search results and other services.
- `actor_type`: string, the type of this account.
## Conversations ## Conversations
@ -146,6 +148,8 @@ Additional parameters can be added to the JSON body/Form data:
- `skip_thread_containment` - if true, skip filtering out broken threads - `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts - `allow_following_move` - if true, allows automatically follow moved following accounts
- `pleroma_background_image` - sets the background image of the user. - `pleroma_background_image` - sets the background image of the user.
- `discoverable` - if true, discovery of this account in search results and other services is allowed.
- `actor_type` - the type of this account.
### Pleroma Settings Store ### Pleroma Settings Store
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about. Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.

View file

@ -52,7 +52,9 @@ def run(["migrate_from_db", env, delete?]) do
|> Enum.each(fn config -> |> Enum.each(fn config ->
IO.write( IO.write(
file, file,
"config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n" "config :#{config.group}, #{config.key}, #{
inspect(Config.from_binary(config.value), limit: :infinity)
}\r\n\r\n"
) )
if delete? do if delete? do

View file

@ -8,7 +8,6 @@ defmodule Mix.Tasks.Pleroma.User do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.OAuth
@shortdoc "Manages Pleroma users" @shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md") @moduledoc File.read!("docs/administration/CLI_tasks/user.md")
@ -383,8 +382,7 @@ def run(["sign_out", nickname]) do
start_pleroma() start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
OAuth.Token.delete_user_tokens(user) User.global_sign_out(user)
OAuth.Authorization.delete_user_authorizations(user)
shell_info("#{nickname} signed out from all apps.") shell_info("#{nickname} signed out from all apps.")
else else
@ -422,10 +420,7 @@ defp set_moderator(user, value) do
end end
defp set_admin(user, value) do defp set_admin(user, value) do
{:ok, user} = {:ok, user} = User.admin_api_update(user, %{is_admin: value})
user
|> Changeset.change(%{is_admin: value})
|> User.update_and_set_cache()
shell_info("Admin status of #{user.nickname}: #{user.is_admin}") shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
user user

View file

@ -65,4 +65,16 @@ def delete(key) do
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage])
def oauth_admin_scopes(scopes) when is_list(scopes) do
Enum.flat_map(
scopes,
fn scope ->
["admin:#{scope}"] ++
if enforce_oauth_admin_scope_usage?(), do: [], else: [scope]
end
)
end
end end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
import Plug.Conn import Plug.Conn
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
alias Pleroma.Config
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
@behaviour Plug @behaviour Plug
@ -15,6 +16,8 @@ def init(%{scopes: _} = options), do: options
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :| op = options[:op] || :|
token = assigns[:token] token = assigns[:token]
scopes = transform_scopes(scopes, options)
matched_scopes = token && filter_descendants(scopes, token.scopes) matched_scopes = token && filter_descendants(scopes, token.scopes)
cond do cond do
@ -60,6 +63,15 @@ def filter_descendants(scopes, supported_scopes) do
) )
end end
@doc "Transforms scopes by applying supported options (e.g. :admin)"
def transform_scopes(scopes, options) do
if options[:admin] do
Config.oauth_admin_scopes(scopes)
else
scopes
end
end
defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
if options[:skip_instance_privacy_check] do if options[:skip_instance_privacy_check] do
conn conn

View file

@ -5,19 +5,38 @@
defmodule Pleroma.Plugs.UserIsAdminPlug do defmodule Pleroma.Plugs.UserIsAdminPlug do
import Pleroma.Web.TranslationHelpers import Pleroma.Web.TranslationHelpers
import Plug.Conn import Plug.Conn
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth
def init(options) do def init(options) do
options options
end end
def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do 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 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.
conn
true ->
fail(conn)
end
end end
def call(conn, _) do def call(conn, _) do
fail(conn)
end
defp fail(conn) do
conn conn
|> render_error(:forbidden, "User is not admin.") |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.")
|> halt |> halt()
end end
end end

View file

@ -127,6 +127,7 @@ defmodule Pleroma.User do
field(:invisible, :boolean, default: false) field(:invisible, :boolean, default: false)
field(:allow_following_move, :boolean, default: true) field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false) field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, :string}, default: []) field(:also_known_as, {:array, :string}, default: [])
embeds_one( embeds_one(
@ -346,6 +347,7 @@ def remote_user_creation(params) do
:following_count, :following_count,
:discoverable, :discoverable,
:invisible, :invisible,
:actor_type,
:also_known_as :also_known_as
] ]
) )
@ -396,6 +398,7 @@ def update_changeset(struct, params \\ %{}) do
:raw_fields, :raw_fields,
:pleroma_settings_store, :pleroma_settings_store,
:discoverable, :discoverable,
:actor_type,
:also_known_as :also_known_as
] ]
) )
@ -438,6 +441,7 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
:discoverable, :discoverable,
:hide_followers_count, :hide_followers_count,
:hide_follows_count, :hide_follows_count,
:actor_type,
:also_known_as :also_known_as
] ]
) )
@ -858,6 +862,13 @@ def get_friends(user, page \\ nil) do
|> Repo.all() |> Repo.all()
end end
def get_friends_ap_ids(user) do
user
|> get_friends_query(nil)
|> select([u], u.ap_id)
|> Repo.all()
end
def get_friends_ids(user, page \\ nil) do def get_friends_ids(user, page \\ nil) do
user user
|> get_friends_query(page) |> get_friends_query(page)
@ -1132,7 +1143,8 @@ def muted_notifications?(%User{} = user, %User{} = target),
def blocks?(nil, _), do: false def blocks?(nil, _), do: false
def blocks?(%User{} = user, %User{} = target) do def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) || blocks_domain?(user, target) blocks_user?(user, target) ||
(!User.following?(user, target) && blocks_domain?(user, target))
end end
def blocks_user?(%User{} = user, %User{} = target) do def blocks_user?(%User{} = user, %User{} = target) do
@ -1835,13 +1847,28 @@ defp truncate_field(%{"name" => name, "value" => value}) do
end end
def admin_api_update(user, params) do def admin_api_update(user, params) do
user changeset =
|> cast(params, [ cast(user, params, [
:is_moderator, :is_moderator,
:is_admin, :is_admin,
:show_role :show_role
]) ])
|> update_and_set_cache()
with {:ok, updated_user} <- update_and_set_cache(changeset) do
if user.is_admin && !updated_user.is_admin do
# Tokens & authorizations containing any admin scopes must be revoked (revoking all).
# This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins).
global_sign_out(user)
end
{:ok, updated_user}
end
end
@doc "Signs user out of all applications"
def global_sign_out(user) do
OAuth.Authorization.delete_user_authorizations(user)
OAuth.Token.delete_user_tokens(user)
end end
def mascot_update(user, url) do def mascot_update(user, url) do

View file

@ -950,6 +950,8 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user) blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)
domain_blocks = user.domain_blocks || [] domain_blocks = user.domain_blocks || []
following_ap_ids = User.get_friends_ap_ids(user)
query = query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query) if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
@ -964,8 +966,22 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
activity.data, activity.data,
^blocked_ap_ids ^blocked_ap_ids
), ),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks), where:
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks) fragment(
"(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)",
activity.actor,
^domain_blocks,
activity.actor,
^following_ap_ids
),
where:
fragment(
"(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)",
o.data,
^domain_blocks,
o.data,
^following_ap_ids
)
) )
end end
@ -1217,6 +1233,7 @@ defp object_to_user_data(data) do
data = Transmogrifier.maybe_fix_user_object(data) data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false discoverable = data["discoverable"] || false
invisible = data["invisible"] || false invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
user_data = %{ user_data = %{
ap_id: data["id"], ap_id: data["id"],
@ -1232,6 +1249,7 @@ defp object_to_user_data(data) do
follower_address: data["followers"], follower_address: data["followers"],
following_address: data["following"], following_address: data["following"],
bio: data["summary"], bio: data["summary"],
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []) also_known_as: Map.get(data, "alsoKnownAs", [])
} }

View file

@ -91,7 +91,7 @@ def render("user.json", %{user: user}) do
%{ %{
"id" => user.ap_id, "id" => user.ap_id,
"type" => "Person", "type" => user.actor_type,
"following" => "#{user.ap_id}/following", "following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers", "followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox", "inbox" => "#{user.ap_id}/inbox",

View file

@ -30,13 +30,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:accounts"]} %{scopes: ["read:accounts"], admin: true}
when action in [:list_users, :user_show, :right_get, :invites] when action in [:list_users, :user_show, :right_get, :invites]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:accounts"]} %{scopes: ["write:accounts"], admin: true}
when action in [ when action in [
:get_invite_token, :get_invite_token,
:revoke_invite, :revoke_invite,
@ -58,35 +58,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:reports"]} when action in [:list_reports, :report_show] %{scopes: ["read:reports"], admin: true}
when action in [:list_reports, :report_show]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:reports"]} %{scopes: ["write:reports"], admin: true}
when action in [:report_update_state, :report_respond] when action in [:report_update_state, :report_respond]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:statuses"]} when action == :list_user_statuses %{scopes: ["read:statuses"], admin: true}
when action == :list_user_statuses
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:statuses"]} %{scopes: ["write:statuses"], admin: true}
when action in [:status_update, :status_delete] when action in [:status_update, :status_delete]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read"]} %{scopes: ["read"], admin: true}
when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write"]} %{scopes: ["write"], admin: true}
when action in [:relay_follow, :relay_unfollow, :config_update] when action in [:relay_follow, :relay_unfollow, :config_update]
) )

View file

@ -188,6 +188,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
{:ok, Map.merge(user.pleroma_settings_store, value)} {:ok, Map.merge(user.pleroma_settings_store, value)}
end) end)
|> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "actor_type", :actor_type)
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")

View file

@ -86,7 +86,7 @@ defp do_render("show.json", %{user: user} = opts) do
0 0
end end
bot = (user.source_data["type"] || "Person") in ["Application", "Service"] bot = user.actor_type in ["Application", "Service"]
emojis = emojis =
(user.source_data["tag"] || []) (user.source_data["tag"] || [])
@ -137,7 +137,8 @@ defp do_render("show.json", %{user: user} = opts) do
sensitive: false, sensitive: false,
fields: user.raw_fields, fields: user.raw_fields,
pleroma: %{ pleroma: %{
discoverable: user.discoverable discoverable: user.discoverable,
actor_type: user.actor_type
} }
}, },

View file

@ -222,7 +222,7 @@ def token_exchange(
{:user_active, true} <- {:user_active, !user.deactivated}, {:user_active, true} <- {:user_active, !user.deactivated},
{:password_reset_pending, false} <- {:password_reset_pending, false} <-
{:password_reset_pending, user.password_reset_pending}, {:password_reset_pending, user.password_reset_pending},
{:ok, scopes} <- validate_scopes(app, params), {:ok, scopes} <- validate_scopes(app, params, user),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build(user, token)) json(conn, Token.Response.build(user, token))
@ -471,7 +471,7 @@ defp do_create_authorization(
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id), %App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris), true <- redirect_uri in String.split(app.redirect_uris),
{:ok, scopes} <- validate_scopes(app, auth_attrs), {:ok, scopes} <- validate_scopes(app, auth_attrs, user),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)} do {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
Authorization.create_authorization(app, user, scopes) Authorization.create_authorization(app, user, scopes)
end end
@ -487,12 +487,12 @@ defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :re
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id) do: put_session(conn, :registration_id, registration_id)
@spec validate_scopes(App.t(), map()) :: @spec validate_scopes(App.t(), map(), User.t()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(app, params) do defp validate_scopes(%App{} = app, params, %User{} = user) do
params params
|> Scopes.fetch_scopes(app.scopes) |> Scopes.fetch_scopes(app.scopes)
|> Scopes.validate(app.scopes) |> Scopes.validate(app.scopes, user)
end end
def default_redirect_uri(%App{} = app) do def default_redirect_uri(%App{} = app) do

View file

@ -7,6 +7,9 @@ defmodule Pleroma.Web.OAuth.Scopes do
Functions for dealing with scopes. Functions for dealing with scopes.
""" """
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
@doc """ @doc """
Fetch scopes from request params. Fetch scopes from request params.
@ -53,15 +56,36 @@ def to_string(scopes), do: Enum.join(scopes, " ")
@doc """ @doc """
Validates scopes. Validates scopes.
""" """
@spec validate(list() | nil, list()) :: @spec validate(list() | nil, list(), User.t()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
def validate([], _app_scopes), do: {:error, :missing_scopes} def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []],
def validate(nil, _app_scopes), do: {:error, :missing_scopes} do: {:error, :missing_scopes}
def validate(scopes, app_scopes) do def validate(scopes, app_scopes, %User{} = user) do
case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do with {:ok, _} <- ensure_scopes_support(scopes, app_scopes),
{:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do
{:ok, scopes}
end
end
defp ensure_scopes_support(scopes, app_scopes) do
case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do
^scopes -> {:ok, scopes} ^scopes -> {:ok, scopes}
_ -> {:error, :unsupported_scopes} _ -> {:error, :unsupported_scopes}
end end
end end
defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do
if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do
{:ok, scopes}
else
{:error, :unsupported_scopes}
end
end
def contains_admin_scopes?(scopes) do
scopes
|> OAuthScopesPlug.filter_descendants(["admin"])
|> Enum.any?()
end
end end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write"]} %{scopes: ["write"], admin: true}
when action in [ when action in [
:create, :create,
:delete, :delete,

View file

@ -528,7 +528,10 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/feed", Feed.FeedController, :feed) get("/users/:nickname/feed", Feed.FeedController, :feed)
get("/users/:nickname", Feed.FeedController, :feed_redirect) get("/users/:nickname", Feed.FeedController, :feed_redirect)
end
scope "/", Pleroma.Web do
pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end end

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddActivitypubActorType do
use Ecto.Migration
def change do
alter table("users") do
add(:actor_type, :string, null: false, default: "Person")
end
end
end

View file

@ -224,4 +224,42 @@ test "filters scopes which directly match or are ancestors of supported scopes"
assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"] assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"]
end end
end end
describe "transform_scopes/2" do
clear_config([:auth, :enforce_oauth_admin_scope_usage])
setup do
{:ok, %{f: &OAuthScopesPlug.transform_scopes/2}}
end
test "with :admin option, prefixes all requested scopes with `admin:` " <>
"and [optionally] keeps only prefixed scopes, " <>
"depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting",
%{f: f} do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
assert f.(["read"], %{admin: true}) == ["admin:read", "read"]
assert f.(["read", "write"], %{admin: true}) == [
"admin:read",
"read",
"admin:write",
"write"
]
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"]
assert f.(["read", "write:reports"], %{admin: true}) == [
"admin:read",
"admin:write:reports"
]
end
test "with no supported options, returns unmodified scopes", %{f: f} do
assert f.(["read"], %{}) == ["read"]
assert f.(["read", "write"], %{}) == ["read", "write"]
end
end
end end

View file

@ -8,21 +8,22 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do
alias Pleroma.Plugs.UserIsAdminPlug alias Pleroma.Plugs.UserIsAdminPlug
import Pleroma.Factory import Pleroma.Factory
test "accepts a user that is admin" do describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do
clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
end
test "accepts a user that is an admin" do
user = insert(:user, is_admin: true) user = insert(:user, is_admin: true)
conn = conn = assign(build_conn(), :user, user)
build_conn()
|> assign(:user, user)
ret_conn = ret_conn = UserIsAdminPlug.call(conn, %{})
conn
|> UserIsAdminPlug.call(%{})
assert conn == ret_conn assert conn == ret_conn
end end
test "denies a user that isn't admin" do test "denies a user that isn't an admin" do
user = insert(:user) user = insert(:user)
conn = conn =
@ -34,10 +35,89 @@ test "denies a user that isn't admin" do
end end
test "denies when a user isn't set" do test "denies when a user isn't set" do
conn = UserIsAdminPlug.call(build_conn(), %{})
assert conn.status == 403
end
end
describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
end
setup do
admin_user = insert(:user, is_admin: true)
non_admin_user = insert(:user, is_admin: false)
blank_user = nil
{:ok, %{users: [admin_user, non_admin_user, blank_user]}}
end
test "if token has any of admin scopes, accepts a user that is an admin", %{conn: conn} do
user = insert(:user, is_admin: true)
token = insert(:oauth_token, user: user, scopes: ["admin:something"])
conn = conn =
build_conn() conn
|> assign(:user, user)
|> assign(:token, token)
ret_conn = UserIsAdminPlug.call(conn, %{})
assert conn == ret_conn
end
test "if token has any of admin scopes, denies a user that isn't an admin", %{conn: conn} do
user = insert(:user, is_admin: false)
token = insert(:oauth_token, user: user, scopes: ["admin:something"])
conn =
conn
|> assign(:user, user)
|> assign(:token, token)
|> UserIsAdminPlug.call(%{}) |> UserIsAdminPlug.call(%{})
assert conn.status == 403 assert conn.status == 403
end end
test "if token has any of admin scopes, denies when a user isn't set", %{conn: conn} do
token = insert(:oauth_token, scopes: ["admin:something"])
conn =
conn
|> assign(:user, nil)
|> assign(:token, token)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
test "if token lacks admin scopes, denies users regardless of is_admin flag",
%{users: users} do
for user <- users do
token = insert(:oauth_token, user: user)
conn =
build_conn()
|> assign(:user, user)
|> assign(:token, token)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
end
test "if token is missing, denies users regardless of is_admin flag", %{users: users} do
for user <- users do
conn =
build_conn()
|> assign(:user, user)
|> assign(:token, nil)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
end
end
end end

View file

@ -63,4 +63,84 @@ test "settings are migrated to file and deleted from db", %{temp_file: temp_file
assert file =~ "config :pleroma, :setting_first," assert file =~ "config :pleroma, :setting_first,"
assert file =~ "config :pleroma, :setting_second," assert file =~ "config :pleroma, :setting_second,"
end end
test "load a settings with large values and pass to file", %{temp_file: temp_file} do
Config.create(%{
group: "pleroma",
key: ":instance",
value: [
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000,
chat_limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
poll_limits: %{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
},
registrations_open: true,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
mrf_transparency: true,
mrf_transparency_exclusions: [],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
no_attachment_links: true,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000,
safe_dm_mentions: false,
healthcheck: false,
remote_post_retention_days: 90,
skip_thread_containment: true,
limit_to_local_content: :unauthenticated,
dynamic_configuration: false,
user_bio_length: 5000,
user_name_length: 100,
max_account_fields: 10,
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 2048,
external_user_synchronization: true,
extended_nickname_format: true,
multi_factor_authentication: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 2,
length: 6
]
]
]
})
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"])
assert Repo.all(Config) == []
assert File.exists?(temp_file)
{:ok, file} = File.read(temp_file)
assert file ==
"use Mix.Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n mrf_transparency: true,\n mrf_transparency_exclusions: [],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n no_attachment_links: true,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n dynamic_configuration: false,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n"
end
end end

View file

@ -914,6 +914,16 @@ test "unblocks domains" do
refute User.blocks?(user, collateral_user) refute User.blocks?(user, collateral_user)
end end
test "follows take precedence over domain blocks" do
user = insert(:user)
good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"})
{:ok, user} = User.block_domain(user, "meanies.social")
{:ok, user} = User.follow(user, good_eggo)
refute User.blocks?(user, good_eggo)
end
end end
describe "blocks_import" do describe "blocks_import" do

View file

@ -608,6 +608,39 @@ test "doesn't return activities from blocked domains" do
refute repeat_activity in activities refute repeat_activity in activities
end end
test "does return activities from followed users on blocked domains" do
domain = "meanies.social"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
blocker = insert(:user)
{:ok, blocker} = User.follow(blocker, domain_user)
{:ok, blocker} = User.block_domain(blocker, domain)
assert User.following?(blocker, domain_user)
assert User.blocks_domain?(blocker, domain_user)
refute User.blocks?(blocker, domain_user)
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
assert activity in activities
# And check that if the guy we DO follow boosts someone else from their domain,
# that should be hidden
another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"})
bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}})
bad_activity = insert(:note_activity, %{note: bad_note})
{:ok, repeat_activity, _} = CommonAPI.repeat(bad_activity.id, domain_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
refute repeat_activity in activities
end
test "doesn't return muted activities" do test "doesn't return muted activities" do
activity_one = insert(:note_activity) activity_one = insert(:note_activity)
activity_two = insert(:note_activity) activity_two = insert(:note_activity)

View file

@ -25,6 +25,60 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
:ok :ok
end end
clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
end
describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
end
test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do
user = insert(:user)
admin = insert(:user, is_admin: true)
url = "/api/pleroma/admin/users/#{user.nickname}"
good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
bad_token3 = nil
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, 200)
end
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, nil)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, :forbidden)
end
for bad_token <- [bad_token1, bad_token2, bad_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, bad_token)
|> get(url)
assert json_response(conn, :forbidden)
end
end
end
describe "DELETE /api/pleroma/admin/users" do describe "DELETE /api/pleroma/admin/users" do
test "single user" do test "single user" do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
@ -98,7 +152,7 @@ test "Create" do
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end end
test "Cannot create user with exisiting email" do test "Cannot create user with existing email" do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
user = insert(:user) user = insert(:user)
@ -129,7 +183,7 @@ test "Cannot create user with exisiting email" do
] ]
end end
test "Cannot create user with exisiting nickname" do test "Cannot create user with existing nickname" do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
user = insert(:user) user = insert(:user)
@ -1560,7 +1614,8 @@ test "returns 403 when requested by a non-admin" do
|> assign(:user, user) |> assign(:user, user)
|> get("/api/pleroma/admin/reports") |> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "User is not admin."} assert json_response(conn, :forbidden) ==
%{"error" => "User is not an admin or OAuth admin scope is not granted."}
end end
test "returns 403 when requested by anonymous" do test "returns 403 when requested by anonymous" do

View file

@ -66,6 +66,7 @@ test "Represent a user account" do
note: "valid html", note: "valid html",
sensitive: false, sensitive: false,
pleroma: %{ pleroma: %{
actor_type: "Person",
discoverable: false discoverable: false
}, },
fields: [] fields: []
@ -106,7 +107,8 @@ test "Represent a Service(bot) account" do
insert(:user, %{ insert(:user, %{
follower_count: 3, follower_count: 3,
note_count: 5, note_count: 5,
source_data: %{"type" => "Service"}, source_data: %{},
actor_type: "Service",
nickname: "shp@shitposter.club", nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036] inserted_at: ~N[2017-08-15 15:47:06.597036]
}) })
@ -134,6 +136,7 @@ test "Represent a Service(bot) account" do
note: user.bio, note: user.bio,
sensitive: false, sensitive: false,
pleroma: %{ pleroma: %{
actor_type: "Service",
discoverable: false discoverable: false
}, },
fields: [] fields: []
@ -278,7 +281,8 @@ test "represent an embedded relationship" do
insert(:user, %{ insert(:user, %{
follower_count: 0, follower_count: 0,
note_count: 5, note_count: 5,
source_data: %{"type" => "Service"}, source_data: %{},
actor_type: "Service",
nickname: "shp@shitposter.club", nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036] inserted_at: ~N[2017-08-15 15:47:06.597036]
}) })
@ -311,6 +315,7 @@ test "represent an embedded relationship" do
note: user.bio, note: user.bio,
sensitive: false, sensitive: false,
pleroma: %{ pleroma: %{
actor_type: "Service",
discoverable: false discoverable: false
}, },
fields: [] fields: []