Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85711163
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
194 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/benchmarks/load_testing/users.ex b/benchmarks/load_testing/users.ex
index 1815490a4..0a33cbfdb 100644
--- a/benchmarks/load_testing/users.ex
+++ b/benchmarks/load_testing/users.ex
@@ -1,189 +1,189 @@
defmodule Pleroma.LoadTesting.Users do
@moduledoc """
Module for generating users with friends.
"""
import Ecto.Query
import Pleroma.LoadTesting.Helper, only: [to_sec: 1]
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.User.Query
@defaults [
users: 20_000,
friends: 100
]
@max_concurrency 10
@spec generate(keyword()) :: User.t()
def generate(opts \\ []) do
opts = Keyword.merge(@defaults, opts)
generate_users(opts[:users])
main_user =
Repo.one(from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1))
make_friends(main_user, opts[:friends])
User.get_by_id(main_user.id)
end
def generate_users(max) do
IO.puts("Starting generating #{max} users...")
{time, users} =
:timer.tc(fn ->
Task.async_stream(
1..max,
&generate_user(&1),
max_concurrency: @max_concurrency,
timeout: 30_000
)
|> Enum.to_list()
end)
IO.puts("Generating users took #{to_sec(time)} sec.\n")
users
end
defp generate_user(i) do
remote = Enum.random([true, false])
%User{
name: "Test テスト User #{i}",
email: "user#{i}@example.com",
nickname: "nick#{i}",
- password_hash: Pleroma.Password.hash_pwd_salt("test"),
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"),
bio: "Tester Number #{i}",
local: !remote
}
|> user_urls()
|> Repo.insert!()
end
defp user_urls(%{local: true} = user) do
urls = %{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user)
}
Map.merge(user, urls)
end
defp user_urls(%{local: false} = user) do
base_domain = Enum.random(["domain1.com", "domain2.com", "domain3.com"])
ap_id = "https://#{base_domain}/users/#{user.nickname}"
urls = %{
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following"
}
Map.merge(user, urls)
end
def make_friends(main_user, max) when is_integer(max) do
IO.puts("Starting making friends for #{max} users...")
{time, _} =
:timer.tc(fn ->
number_of_users =
(max / 2)
|> Kernel.trunc()
main_user
|> get_users(%{limit: number_of_users, local: :local})
|> run_stream(main_user)
main_user
|> get_users(%{limit: number_of_users, local: :external})
|> run_stream(main_user)
end)
IO.puts("Making friends took #{to_sec(time)} sec.\n")
end
def make_friends(%User{} = main_user, %User{} = user) do
{:ok, _, _} = User.follow(main_user, user)
{:ok, _, _} = User.follow(user, main_user)
end
@spec get_users(User.t(), keyword()) :: [User.t()]
def get_users(user, opts) do
criteria = %{limit: opts[:limit]}
criteria =
if opts[:local] do
Map.put(criteria, opts[:local], true)
else
criteria
end
criteria =
if opts[:friends?] do
Map.put(criteria, :friends, user)
else
criteria
end
query =
criteria
|> Query.build()
|> random_without_user(user)
query =
if opts[:friends?] == false do
friends_ids =
%{friends: user}
|> Query.build()
|> Repo.all()
|> Enum.map(& &1.id)
from(u in query, where: u.id not in ^friends_ids)
else
query
end
Repo.all(query)
end
defp random_without_user(query, user) do
from(u in query,
where: u.id != ^user.id,
order_by: fragment("RANDOM()")
)
end
defp run_stream(users, main_user) do
Task.async_stream(users, &make_friends(main_user, &1),
max_concurrency: @max_concurrency,
timeout: 30_000
)
|> Stream.run()
end
@spec prepare_users(User.t(), keyword()) :: map()
def prepare_users(user, opts) do
friends_limit = opts[:friends_used]
non_friends_limit = opts[:non_friends_used]
%{
user: user,
friends_local: fetch_users(user, friends_limit, :local, true),
friends_remote: fetch_users(user, friends_limit, :external, true),
non_friends_local: fetch_users(user, non_friends_limit, :local, false),
non_friends_remote: fetch_users(user, non_friends_limit, :external, false)
}
end
defp fetch_users(user, limit, local, friends?) do
user
|> get_users(limit: limit, local: local, friends?: friends?)
|> Enum.shuffle()
end
end
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
index 29488c876..02dce7d49 100644
--- a/lib/pleroma/mfa.ex
+++ b/lib/pleroma/mfa.ex
@@ -1,155 +1,155 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA do
@moduledoc """
The MFA context.
"""
alias Pleroma.User
alias Pleroma.MFA.BackupCodes
alias Pleroma.MFA.Changeset
alias Pleroma.MFA.Settings
alias Pleroma.MFA.TOTP
@doc """
Returns MFA methods the user has enabled.
## Examples
iex> Pleroma.MFA.supported_method(User)
"totp, u2f"
"""
@spec supported_methods(User.t()) :: String.t()
def supported_methods(user) do
settings = fetch_settings(user)
Settings.mfa_methods()
|> Enum.reduce([], fn m, acc ->
if method_enabled?(m, settings) do
acc ++ [m]
else
acc
end
end)
|> Enum.join(",")
end
@doc "Checks that user enabled MFA"
def require?(user) do
fetch_settings(user).enabled
end
@doc """
Display MFA settings of user
"""
def mfa_settings(user) do
settings = fetch_settings(user)
Settings.mfa_methods()
|> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
|> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
end
@doc false
def fetch_settings(%User{} = user) do
user.multi_factor_authentication_settings || %Settings{}
end
@doc "clears backup codes"
def invalidate_backup_code(%User{} = user, hash_code) do
%{backup_codes: codes} = fetch_settings(user)
user
|> Changeset.cast_backup_codes(codes -- [hash_code])
|> User.update_and_set_cache()
end
@doc "generates backup codes"
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
def generate_backup_codes(%User{} = user) do
with codes <- BackupCodes.generate(),
- hashed_codes <- Enum.map(codes, &Pleroma.Password.hash_pwd_salt/1),
+ hashed_codes <- Enum.map(codes, &Pleroma.Password.Pbkdf2.hash_pwd_salt/1),
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
{:ok, _} <- User.update_and_set_cache(changeset) do
{:ok, codes}
else
{:error, msg} ->
%{error: msg}
end
end
@doc """
Generates secret key and set delivery_type to 'app' for TOTP method.
"""
@spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def setup_totp(user) do
user
|> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
|> User.update_and_set_cache()
end
@doc """
Confirms the TOTP method for user.
`attrs`:
`password` - current user password
`code` - TOTP token
"""
@spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
def confirm_totp(%User{} = user, attrs) do
with settings <- user.multi_factor_authentication_settings.totp,
{:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
user
|> Changeset.confirm_totp()
|> User.update_and_set_cache()
end
end
@doc """
Disables the TOTP method for user.
`attrs`:
`password` - current user password
"""
@spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def disable_totp(%User{} = user) do
user
|> Changeset.disable_totp()
|> Changeset.disable()
|> User.update_and_set_cache()
end
@doc """
Force disables all MFA methods for user.
"""
@spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def disable(%User{} = user) do
user
|> Changeset.disable_totp()
|> Changeset.disable(true)
|> User.update_and_set_cache()
end
@doc """
Checks if the user has MFA method enabled.
"""
def method_enabled?(method, settings) do
with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
true
else
_ -> false
end
end
@doc """
Checks if the user has enabled at least one MFA method.
"""
def enabled?(settings) do
Settings.mfa_methods()
|> Enum.map(fn m -> method_enabled?(m, settings) end)
|> Enum.any?()
end
end
diff --git a/lib/pleroma/password.ex b/lib/pleroma/password.ex
deleted file mode 100644
index e96249650..000000000
--- a/lib/pleroma/password.ex
+++ /dev/null
@@ -1,55 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Password do
- @moduledoc """
- This module implements Pleroma.Password passwords in terms of Plug.Crypto.
- """
-
- alias Plug.Crypto.KeyGenerator
-
- def decode64(str) do
- str
- |> String.replace(".", "+")
- |> Base.decode64!(padding: false)
- end
-
- def encode64(bin) do
- bin
- |> Base.encode64(padding: false)
- |> String.replace("+", ".")
- end
-
- def verify_pass(password, hash) do
- ["pbkdf2-" <> digest, iterations, salt, hash] = String.split(hash, "$", trim: true)
-
- salt = decode64(salt)
-
- iterations = String.to_integer(iterations)
-
- digest = String.to_atom(digest)
-
- binary_hash =
- KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
-
- encode64(binary_hash) == hash
- end
-
- def hash_pwd_salt(password, opts \\ []) do
- salt =
- Keyword.get_lazy(opts, :salt, fn ->
- :crypto.strong_rand_bytes(16)
- end)
-
- digest = Keyword.get(opts, :digest, :sha512)
-
- iterations =
- Keyword.get(opts, :iterations, Pleroma.Config.get([:password, :iterations], 160_000))
-
- binary_hash =
- KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
-
- "$pbkdf2-#{digest}$#{iterations}$#{encode64(salt)}$#{encode64(binary_hash)}"
- end
-end
diff --git a/lib/pleroma/password/pbkdf2.ex b/lib/pleroma/password/pbkdf2.ex
index 747bc1d1d..2fd5f4491 100644
--- a/lib/pleroma/password/pbkdf2.ex
+++ b/lib/pleroma/password/pbkdf2.ex
@@ -1,55 +1,55 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Password.Pbkdf2 do
@moduledoc """
- This module implements Pleroma.Password.Pbkdf2 passwords in terms of Plug.Crypto.
+ This module implements Pbkdf2 passwords in terms of Plug.Crypto.
"""
alias Plug.Crypto.KeyGenerator
def decode64(str) do
str
|> String.replace(".", "+")
|> Base.decode64!(padding: false)
end
def encode64(bin) do
bin
|> Base.encode64(padding: false)
|> String.replace("+", ".")
end
def verify_pass(password, hash) do
["pbkdf2-" <> digest, iterations, salt, hash] = String.split(hash, "$", trim: true)
salt = decode64(salt)
iterations = String.to_integer(iterations)
digest = String.to_atom(digest)
binary_hash =
KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
encode64(binary_hash) == hash
end
def hash_pwd_salt(password, opts \\ []) do
salt =
Keyword.get_lazy(opts, :salt, fn ->
:crypto.strong_rand_bytes(16)
end)
digest = Keyword.get(opts, :digest, :sha512)
iterations =
Keyword.get(opts, :iterations, Pleroma.Config.get([:password, :iterations], 160_000))
binary_hash =
KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
"$pbkdf2-#{digest}$#{iterations}$#{encode64(salt)}$#{encode64(binary_hash)}"
end
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 04e6ffd51..6a81adfd6 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,2460 +1,2460 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
import Ecto, only: [assoc: 2]
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker
require Logger
@type t :: %__MODULE__{}
@type account_status ::
:active
| :deactivated
| :password_reset_pending
| :confirmation_pending
| :approval_pending
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
# AP ID user relationships (blocks, mutes etc.)
# Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
@user_relationships_config [
block: [
blocker_blocks: :blocked_users,
blockee_blocks: :blocker_users
],
mute: [
muter_mutes: :muted_users,
mutee_mutes: :muter_users
],
reblog_mute: [
reblog_muter_mutes: :reblog_muted_users,
reblog_mutee_mutes: :reblog_muter_users
],
notification_mute: [
notification_muter_mutes: :notification_muted_users,
notification_mutee_mutes: :notification_muter_users
],
# Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
inverse_subscription: [
subscribee_subscriptions: :subscriber_users,
subscriber_subscriptions: :subscribee_users
]
]
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
schema "users" do
field(:bio, :string, default: "")
field(:raw_bio, :string)
field(:email, :string)
field(:name, :string)
field(:nickname, :string)
field(:password_hash, :string)
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
field(:public_key, :string)
field(:ap_id, :string)
field(:avatar, :map, default: %{})
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
field(:last_digest_emailed_at, :naive_datetime)
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:following_count, :integer, default: 0)
field(:is_locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
field(:approval_pending, :boolean, default: false)
field(:registration_reason, :string, default: nil)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:domain_blocks, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
field(:is_moderator, :boolean, default: false)
field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true)
field(:mastofe_settings, :map, default: nil)
field(:uri, ObjectValidators.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
field(:emoji, :map, default: %{})
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: [])
field(:raw_fields, {:array, :map}, default: [])
field(:is_discoverable, :boolean, default: false)
field(:invisible, :boolean, default: false)
field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil)
embeds_one(
:notification_settings,
Pleroma.User.NotificationSetting,
on_replace: :update
)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
has_many(:deliveries, Delivery)
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
for {relationship_type,
[
{outgoing_relation, outgoing_relation_target},
{incoming_relation, incoming_relation_source}
]} <- @user_relationships_config do
# Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
# :notification_muter_mutes, :subscribee_subscriptions
has_many(outgoing_relation, UserRelationship,
foreign_key: :source_id,
where: [relationship_type: relationship_type]
)
# Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
# :notification_mutee_mutes, :subscriber_subscriptions
has_many(incoming_relation, UserRelationship,
foreign_key: :target_id,
where: [relationship_type: relationship_type]
)
# Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
# :notification_muted_users, :subscriber_users
has_many(outgoing_relation_target, through: [outgoing_relation, :target])
# Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
# :notification_muter_users, :subscribee_users
has_many(incoming_relation_source, through: [incoming_relation, :source])
end
# `:blocks` is deprecated (replaced with `blocked_users` relation)
field(:blocks, {:array, :string}, default: [])
# `:mutes` is deprecated (replaced with `muted_users` relation)
field(:mutes, {:array, :string}, default: [])
# `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
field(:muted_reblogs, {:array, :string}, default: [])
# `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
field(:muted_notifications, {:array, :string}, default: [])
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
embeds_one(
:multi_factor_authentication_settings,
MFA.Settings,
on_replace: :delete
)
timestamps()
end
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
@user_relationships_config do
# `def blocked_users_relation/2`, `def muted_users_relation/2`,
# `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
# `def subscriber_users/2`
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
target_users_query = assoc(user, unquote(outgoing_relation_target))
if restrict_deactivated? do
restrict_deactivated(target_users_query)
else
target_users_query
end
end
# `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
# `def notification_muted_users/2`, `def subscriber_users/2`
def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
__MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
user,
restrict_deactivated?
])
|> Repo.all()
end
# `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
# `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
__MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
user,
restrict_deactivated?
])
|> select([u], u.ap_id)
|> Repo.all()
end
end
def cached_blocked_users_ap_ids(user) do
@cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
blocked_users_ap_ids(user)
end)
end
def cached_muted_users_ap_ids(user) do
@cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
muted_users_ap_ids(user)
end)
end
defdelegate following_count(user), to: FollowingRelationship
defdelegate following(user), to: FollowingRelationship
defdelegate following?(follower, followed), to: FollowingRelationship
defdelegate following_ap_ids(user), to: FollowingRelationship
defdelegate get_follow_requests(user), to: FollowingRelationship
defdelegate search(query, opts \\ []), to: User.Search
@doc """
Dumps Flake Id to SQL-compatible format (16-byte UUID).
E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
"""
def binary_id(source_id) when is_binary(source_id) do
with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
dumped_id
else
_ -> source_id
end
end
def binary_id(source_ids) when is_list(source_ids) do
Enum.map(source_ids, &binary_id/1)
end
def binary_id(%User{} = user), do: binary_id(user.id)
@doc "Returns status account"
@spec account_status(User.t()) :: account_status()
def account_status(%User{deactivated: true}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
def account_status(%User{local: true, approval_pending: true}), do: :approval_pending
def account_status(%User{local: true, confirmation_pending: true}) do
if Config.get([:instance, :account_activation_required]) do
:confirmation_pending
else
:active
end
end
def account_status(%User{}), do: :active
@spec visible_for(User.t(), User.t() | nil) ::
:visible
| :invisible
| :restricted_unauthenticated
| :deactivated
| :confirmation_pending
def visible_for(user, for_user \\ nil)
def visible_for(%User{invisible: true}, _), do: :invisible
def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
def visible_for(%User{} = user, nil) do
if restrict_unauthenticated?(user) do
:restrict_unauthenticated
else
visible_account_status(user)
end
end
def visible_for(%User{} = user, for_user) do
if superuser?(for_user) do
:visible
else
visible_account_status(user)
end
end
def visible_for(_, _), do: :invisible
defp restrict_unauthenticated?(%User{local: true}) do
Config.restrict_unauthenticated_access?(:profiles, :local)
end
defp restrict_unauthenticated?(%User{local: _}) do
Config.restrict_unauthenticated_access?(:profiles, :remote)
end
defp visible_account_status(user) do
status = account_status(user)
if status in [:active, :password_reset_pending] do
:visible
else
status
end
end
@spec superuser?(User.t()) :: boolean()
def superuser?(%User{local: true, is_admin: true}), do: true
def superuser?(%User{local: true, is_moderator: true}), do: true
def superuser?(_), do: false
@spec invisible?(User.t()) :: boolean()
def invisible?(%User{invisible: true}), do: true
def invisible?(_), do: false
def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} ->
href
_ ->
unless options[:no_default] do
Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
end
end
end
def banner_url(user, options \\ []) do
case user.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
end
end
# Should probably be renamed or removed
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
@spec ap_following(User.t()) :: String.t()
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true)
end
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
else
params
end
end
defp truncate_if_exists(params, key, max_length) do
if Map.has_key?(params, key) and is_binary(params[key]) do
{value, _chopped} = String.split_at(params[key], max_length)
Map.put(params, key, value)
else
params
end
end
defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
defp fix_follower_address(%{nickname: nickname} = params),
do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
defp fix_follower_address(params), do: params
def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
name =
case params[:name] do
name when is_binary(name) and byte_size(name) > 0 -> name
_ -> params[:nickname]
end
params =
params
|> Map.put(:name, name)
|> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
|> truncate_fields_param()
|> fix_follower_address()
struct
|> cast(
params,
[
:bio,
:emoji,
:ap_id,
:inbox,
:shared_inbox,
:nickname,
:public_key,
:avatar,
:ap_enabled,
:banner,
:is_locked,
:last_refreshed_at,
:uri,
:follower_address,
:following_address,
:hide_followers,
:hide_follows,
:hide_followers_count,
:hide_follows_count,
:follower_count,
:fields,
:following_count,
:is_discoverable,
:invisible,
:actor_type,
:also_known_as,
:accepts_chat_messages
]
)
|> cast(params, [:name], empty_values: [])
|> validate_required([:ap_id])
|> validate_required([:name], trim: false)
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> validate_fields(true)
|> validate_non_local()
end
defp validate_non_local(cng) do
local? = get_field(cng, :local)
if local? do
cng
|> add_error(:local, "User is local, can't update with this changeset.")
else
cng
end
end
def update_changeset(struct, params \\ %{}) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
struct
|> cast(
params,
[
:bio,
:raw_bio,
:name,
:emoji,
:avatar,
:public_key,
:inbox,
:shared_inbox,
:is_locked,
:no_rich_text,
:default_scope,
:banner,
:hide_follows,
:hide_followers,
:hide_followers_count,
:hide_follows_count,
:hide_favorites,
:allow_following_move,
:also_known_as,
:background,
:show_role,
:skip_thread_containment,
:fields,
:raw_fields,
:pleroma_settings_store,
:is_discoverable,
:actor_type,
:accepts_chat_messages
]
)
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_inclusion(:actor_type, ["Person", "Service"])
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|> put_change_if_present(:banner, &put_upload(&1, :banner))
|> put_change_if_present(:background, &put_upload(&1, :background))
|> put_change_if_present(
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
|> validate_fields(false)
end
defp put_fields(changeset) do
if raw_fields = get_change(changeset, :raw_fields) do
raw_fields =
raw_fields
|> Enum.filter(fn %{"name" => n} -> n != "" end)
fields =
raw_fields
|> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
changeset
|> put_change(:raw_fields, raw_fields)
|> put_change(:fields, fields)
else
changeset
end
end
defp parse_fields(value) do
value
|> Formatter.linkify(mentions_format: :full)
|> elem(0)
end
defp put_emoji(changeset) do
emojified_fields = [:bio, :name, :raw_fields]
if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
emoji = Map.merge(bio, name)
emoji =
changeset
|> get_field(:raw_fields)
|> Enum.reduce(emoji, fn x, acc ->
Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
end)
put_change(changeset, :emoji, emoji)
else
changeset
end
end
defp put_change_if_present(changeset, map_field, value_function) do
with {:ok, value} <- fetch_change(changeset, map_field),
{:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value)
else
_ -> changeset
end
end
defp put_upload(value, type) do
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: type) do
{:ok, object.data}
end
end
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
|> cast(params, [:email])
|> delete_change(:also_known_as)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> validate_inclusion(:actor_type, ["Person", "Service"])
end
@spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
if params["password"] do
reset_password(user, changeset, params)
else
User.update_and_set_cache(changeset)
end
end
def password_update_changeset(struct, params) do
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
|> put_password_hash()
|> put_change(:password_reset_pending, false)
end
@spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
def reset_password(%User{id: user_id} = user, struct, params) do
multi =
Multi.new()
|> Multi.update(:user, password_update_changeset(struct, params))
|> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
|> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
case Repo.transaction(multi) do
{:ok, %{user: user} = _} -> set_cache(user)
{:error, _, changeset, _} -> {:error, changeset}
end
end
def update_password_reset_pending(user, value) do
user
|> change()
|> put_change(:password_reset_pending, value)
|> update_and_set_cache()
end
def force_password_reset_async(user) do
BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
end
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def force_password_reset(user), do: update_password_reset_pending(user, true)
# Used to auto-register LDAP accounts which won't have a password hash stored locally
def register_changeset_ldap(struct, params = %{password: password})
when is_nil(password) do
params = Map.put_new(params, :accepts_chat_messages, true)
params =
if Map.has_key?(params, :email) do
Map.put_new(params, :email, params[:email])
else
params
end
struct
|> cast(params, [
:name,
:nickname,
:email,
:accepts_chat_messages
])
|> validate_required([:name, :nickname])
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end
def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
reason_limit = Config.get([:instance, :registration_reason_length], 500)
params = Map.put_new(params, :accepts_chat_messages, true)
need_confirmation? =
if is_nil(opts[:need_confirmation]) do
Config.get([:instance, :account_activation_required])
else
opts[:need_confirmation]
end
need_approval? =
if is_nil(opts[:need_approval]) do
Config.get([:instance, :account_approval_required])
else
opts[:need_approval]
end
struct
|> confirmation_changeset(need_confirmation: need_confirmation?)
|> approval_changeset(need_approval: need_approval?)
|> cast(params, [
:bio,
:raw_bio,
:email,
:name,
:nickname,
:password,
:password_confirmation,
:emoji,
:accepts_chat_messages,
:registration_reason
])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> validate_change(:email, fn :email, email ->
valid? =
Config.get([User, :email_blacklist])
|> Enum.all?(fn blacklisted_domain ->
!String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
end)
if valid?, do: [], else: [email: "Invalid email"]
end)
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_length(:registration_reason, max: reason_limit)
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _) do
if Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
changeset
end
end
defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end
defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
changeset
|> put_change(:follower_address, followers)
end
defp autofollow_users(user) do
candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all()
follow_all(user, autofollowed_users)
end
defp autofollowing_users(user) do
candidates = Config.get([:instance, :autofollowing_nicknames])
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all()
|> Enum.each(&follow(&1, user, :follow_accept))
{:ok, :success}
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
end
end
def post_register_action(%User{confirmation_pending: true} = user) do
with {:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
def post_register_action(%User{approval_pending: true} = user) do
with {:ok, _} <- send_user_approval_email(user),
{:ok, _} <- send_admin_approval_emails(user) do
{:ok, user}
end
end
def post_register_action(%User{approval_pending: false, confirmation_pending: false} = user) do
with {:ok, user} <- autofollow_users(user),
{:ok, _} <- autofollowing_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- send_welcome_email(user),
{:ok, _} <- send_welcome_message(user),
{:ok, _} <- send_welcome_chat_message(user) do
{:ok, user}
end
end
defp send_user_approval_email(user) do
user
|> Pleroma.Emails.UserEmail.approval_pending_email()
|> Pleroma.Emails.Mailer.deliver_async()
{:ok, :enqueued}
end
defp send_admin_approval_emails(user) do
all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, :enqueued}
end
def send_welcome_message(user) do
if User.WelcomeMessage.enabled?() do
User.WelcomeMessage.post_message(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def send_welcome_chat_message(user) do
if User.WelcomeChatMessage.enabled?() do
User.WelcomeChatMessage.post_message(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def send_welcome_email(%User{email: email} = user) when is_binary(email) do
if User.WelcomeEmail.enabled?() do
User.WelcomeEmail.send_email(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def send_welcome_email(_), do: {:ok, :noop}
@spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
def try_send_confirmation_email(%User{confirmation_pending: true, email: email} = user)
when is_binary(email) do
if Config.get([:instance, :account_activation_required]) do
send_confirmation_email(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def try_send_confirmation_email(_), do: {:ok, :noop}
@spec send_confirmation_email(Uset.t()) :: User.t()
def send_confirmation_email(%User{} = user) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
user
end
def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
def needs_update?(%User{local: false} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
end
def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
follow(follower, followed, :follow_pending)
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
follow(follower, followed)
end
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if not ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower, followed}
end
end
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.each(&follow(follower, &1, :follow_accept))
set_cache(follower)
end
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
cond do
followed.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is deactivated."}
deny_follow_blocked and blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
FollowingRelationship.follow(follower, followed, state)
end
end
def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
{:error, "Not subscribed!"}
end
@spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
def unfollow(%User{} = follower, %User{} = followed) do
case do_unfollow(follower, followed) do
{:ok, follower, followed} ->
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
error ->
error
end
end
@spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
defp do_unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
nil ->
{:error, "Not subscribed!"}
end
end
@doc "Returns follow state as Pleroma.FollowingRelationship.State value"
def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following)
get_follow_state(follower, following, following_relationship)
end
def get_follow_state(
%User{} = follower,
%User{} = following,
following_relationship
) do
case {following_relationship, following.local} do
{nil, false} ->
case Utils.fetch_latest_follow(follower, following) do
%Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
FollowingRelationship.state_to_enum(state)
_ ->
nil
end
{%{state: state}, _} ->
state
{nil, _} ->
nil
end
end
def locked?(%User{} = user) do
user.is_locked || false
end
def get_by_id(id) do
Repo.get_by(User, id: id)
end
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
def get_all_by_ap_id(ap_ids) do
from(u in __MODULE__,
where: u.ap_id in ^ap_ids
)
|> Repo.all()
end
def get_all_by_ids(ids) do
from(u in __MODULE__, where: u.id in ^ids)
|> Repo.all()
end
# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) do
domain = URI.parse(ap_id).host
name = List.last(String.split(ap_id, "/"))
nickname = "#{name}@#{domain}"
get_cached_by_nickname(nickname)
end
def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do
@cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
@cachex.put(:user_cache, "nickname:#{user.nickname}", user)
@cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user}
end
def update_and_set_cache(struct, params) do
struct
|> update_changeset(params)
|> update_and_set_cache()
end
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
end
end
def get_user_friends_ap_ids(user) do
from(u in User.get_friends_query(user), select: u.ap_id)
|> Repo.all()
end
@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
def get_cached_user_friends_ap_ids(user) do
@cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
get_user_friends_ap_ids(user)
end)
end
def invalidate_cache(user) do
@cachex.del(:user_cache, "ap_id:#{user.ap_id}")
@cachex.del(:user_cache, "nickname:#{user.nickname}")
@cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
@cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
with {:ok, nil} <- @cachex.get(:user_cache, key),
user when not is_nil(user) <- get_by_ap_id(ap_id),
{:ok, true} <- @cachex.put(:user_cache, key, user) do
user
else
{:ok, user} -> user
nil -> nil
end
end
def get_cached_by_id(id) do
key = "id:#{id}"
ap_id =
@cachex.fetch!(:user_cache, key, fn _ ->
user = get_by_id(id)
if user do
@cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
{:commit, user.ap_id}
else
{:ignore, ""}
end
end)
get_cached_by_ap_id(ap_id)
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
@cachex.fetch!(:user_cache, key, fn _ ->
case get_or_fetch_by_nickname(nickname) do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
end)
end
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
restrict_to_local = Config.get([:instance, :limit_to_local_content])
cond do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
get_cached_by_nickname(nickname_or_id)
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
get_cached_by_nickname(nickname_or_id)
true ->
nil
end
end
@spec get_by_nickname(String.t()) :: User.t() | nil
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
Repo.get_by(User, nickname: local_nickname(nickname))
end
end
def get_by_email(email), do: Repo.get_by(User, email: email)
def get_by_nickname_or_email(nickname_or_email) do
get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
end
def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
{:ok, user}
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
{:ok, user}
else
_e -> {:error, "not found " <> nickname}
end
end
end
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do
User.Query.build(%{followers: user, deactivated: false})
end
def get_followers_query(%User{} = user, page) do
user
|> get_followers_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_followers_query(User.t()) :: Ecto.Query.t()
def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_followers(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> Repo.all()
end
@spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_external_followers(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
|> Repo.all()
end
def get_followers_ids(%User{} = user, page \\ nil) do
user
|> get_followers_query(page)
|> select([u], u.id)
|> Repo.all()
end
@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_friends_query(%User{} = user, nil) do
User.Query.build(%{friends: user, deactivated: false})
end
def get_friends_query(%User{} = user, page) do
user
|> get_friends_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_friends_query(User.t()) :: Ecto.Query.t()
def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
def get_friends(%User{} = user, page \\ nil) do
user
|> get_friends_query(page)
|> Repo.all()
end
def get_friends_ap_ids(%User{} = user) do
user
|> get_friends_query(nil)
|> select([u], u.ap_id)
|> Repo.all()
end
def get_friends_ids(%User{} = user, page \\ nil) do
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
|> update([u], inc: [note_count: 1])
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def decrease_note_count(%User{} = user) do
User
|> where(id: ^user.id)
|> update([u],
set: [
note_count: fragment("greatest(0, note_count - 1)")
]
)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def update_note_count(%User{} = user, note_count \\ nil) do
note_count =
note_count ||
from(
a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id)
)
|> Repo.one()
user
|> cast(%{note_count: note_count}, [:note_count])
|> update_and_set_cache()
end
@spec maybe_fetch_follow_information(User.t()) :: User.t()
def maybe_fetch_follow_information(user) do
with {:ok, user} <- fetch_follow_information(user) do
user
else
e ->
Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
user
end
end
def fetch_follow_information(user) do
with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
user
|> follow_information_changeset(info)
|> update_and_set_cache()
end
end
defp follow_information_changeset(user, params) do
user
|> cast(params, [
:hide_followers,
:hide_follows,
:follower_count,
:following_count,
:hide_followers_count,
:hide_follows_count
])
end
@spec update_follower_count(User.t()) :: {:ok, User.t()}
def update_follower_count(%User{} = user) do
if user.local or !Config.get([:instance, :external_user_synchronization]) do
follower_count = FollowingRelationship.follower_count(user)
user
|> follow_information_changeset(%{follower_count: follower_count})
|> update_and_set_cache
else
{:ok, maybe_fetch_follow_information(user)}
end
end
@spec update_following_count(User.t()) :: {:ok, User.t()}
def update_following_count(%User{local: false} = user) do
if Config.get([:instance, :external_user_synchronization]) do
{:ok, maybe_fetch_follow_information(user)}
else
{:ok, user}
end
end
def update_following_count(%User{local: true} = user) do
following_count = FollowingRelationship.following_count(user)
user
|> follow_information_changeset(%{following_count: following_count})
|> update_and_set_cache()
end
@spec get_users_from_set([String.t()], keyword()) :: [User.t()]
def get_users_from_set(ap_ids, opts \\ []) do
local_only = Keyword.get(opts, :local_only, true)
criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria)
|> Repo.all()
end
@spec get_recipients_from_activity(Activity.t()) :: [User.t()]
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
to = [actor | to]
query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
query
|> Repo.all()
end
@spec mute(User.t(), User.t(), map()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()}
def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
notifications? = Map.get(params, :notifications, true)
expires_in = Map.get(params, :expires_in, 0)
with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
{:ok, user_notification_mute} <-
(notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
{:ok, nil} do
if expires_in > 0 do
Pleroma.Workers.MuteExpireWorker.enqueue(
"unmute_user",
%{"muter_id" => muter.id, "mutee_id" => mutee.id},
schedule_in: expires_in
)
end
@cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
{:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
end
end
def unmute(%User{} = muter, %User{} = mutee) do
with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
{:ok, user_notification_mute} <-
UserRelationship.delete_notification_mute(muter, mutee) do
@cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
{:ok, [user_mute, user_notification_mute]}
end
end
def unmute(muter_id, mutee_id) do
with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
{:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
unmute(muter, mutee)
else
{who, result} = error ->
Logger.warn(
"User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
)
{:error, error}
end
end
def subscribe(%User{} = subscriber, %User{} = target) do
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
else
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.create_inverse_subscription(target, subscriber)
end
end
def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
subscribe(subscriber, subscribee)
end
end
def unsubscribe(%User{} = unsubscriber, %User{} = target) do
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.delete_inverse_subscription(target, unsubscriber)
end
def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
unsubscribe(unsubscriber, user)
end
end
def block(%User{} = blocker, %User{} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
if following?(blocker, blocked) do
{:ok, blocker, _} = unfollow(blocker, blocked)
blocker
else
blocker
end
# clear any requested follows as well
blocked =
case CommonAPI.reject_follow_request(blocked, blocker) do
{:ok, %User{} = updated_blocked} -> updated_blocked
nil -> blocked
end
unsubscribe(blocked, blocker)
unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
add_to_block(blocker, blocked)
end
# helper to handle the block given only an actor's AP id
def block(%User{} = blocker, %{ap_id: ap_id}) do
block(blocker, get_cached_by_ap_id(ap_id))
end
def unblock(%User{} = blocker, %User{} = blocked) do
remove_from_block(blocker, blocked)
end
# helper to handle the block given only an actor's AP id
def unblock(%User{} = blocker, %{ap_id: ap_id}) do
unblock(blocker, get_cached_by_ap_id(ap_id))
end
def mutes?(nil, _), do: false
def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
def mutes_user?(%User{} = user, %User{} = target) do
UserRelationship.mute_exists?(user, target)
end
@spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
def muted_notifications?(nil, _), do: false
def muted_notifications?(%User{} = user, %User{} = target),
do: UserRelationship.notification_mute_exists?(user, target)
def blocks?(nil, _), do: false
def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) ||
(blocks_domain?(user, target) and not User.following?(user, target))
end
def blocks_user?(%User{} = user, %User{} = target) do
UserRelationship.block_exists?(user, target)
end
def blocks_user?(_, _), do: false
def blocks_domain?(%User{} = user, %User{} = target) do
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
%{host: host} = URI.parse(target.ap_id)
Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
end
def blocks_domain?(_, _), do: false
def subscribed_to?(%User{} = user, %User{} = target) do
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.inverse_subscription_exists?(target, user)
end
def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
with %User{} = target <- get_cached_by_ap_id(ap_id) do
subscribed_to?(user, target)
end
end
@doc """
Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
"""
@spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
def outgoing_relationships_ap_ids(_user, []), do: %{}
def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
when is_list(relationship_types) do
db_result =
user
|> assoc(:outgoing_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :target))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
|> group_by([user_rel, u], user_rel.relationship_type)
|> Repo.all()
|> Enum.into(%{}, fn [k, v] -> {k, v} end)
Enum.into(
relationship_types,
%{},
fn rel_type -> {rel_type, db_result[rel_type] || []} end
)
end
def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
when is_list(relationship_types) do
user
|> assoc(:incoming_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :source))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> maybe_filter_on_ap_id(ap_ids)
|> select([user_rel, u], u.ap_id)
|> distinct(true)
|> Repo.all()
end
defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
where(query, [user_rel, u], u.ap_id in ^ap_ids)
end
defp maybe_filter_on_ap_id(query, _ap_ids), do: query
def deactivate_async(user, status \\ true) do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end
def deactivate(user, status \\ true)
def deactivate(users, status) when is_list(users) do
Repo.transaction(fn ->
for user <- users, do: deactivate(user, status)
end)
end
def deactivate(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do
user
|> get_followers()
|> Enum.filter(& &1.local)
|> Enum.each(&set_cache(update_following_count(&1)))
# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
|> Enum.each(&do_unfollow(user, &1))
{:ok, user}
end
end
def approve(users) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- approve(user), do: user
end)
end)
end
def approve(%User{approval_pending: true} = user) do
with chg <- change(user, approval_pending: false),
{:ok, user} <- update_and_set_cache(chg) do
post_register_action(user)
{:ok, user}
end
end
def approve(%User{} = user), do: {:ok, user}
def confirm(users) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- confirm(user), do: user
end)
end)
end
def confirm(%User{confirmation_pending: true} = user) do
with chg <- confirmation_changeset(user, need_confirmation: false),
{:ok, user} <- update_and_set_cache(chg) do
post_register_action(user)
{:ok, user}
end
end
def confirm(%User{} = user), do: {:ok, user}
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
|> cast_embed(:notification_settings)
|> validate_required([:notification_settings])
|> update_and_set_cache()
end
@spec purge_user_changeset(User.t()) :: Changeset.t()
def purge_user_changeset(user) do
# "Right to be forgotten"
# https://gdpr.eu/right-to-be-forgotten/
change(user, %{
bio: "",
raw_bio: nil,
email: nil,
name: nil,
password_hash: nil,
keys: nil,
public_key: nil,
avatar: %{},
tags: [],
last_refreshed_at: nil,
last_digest_emailed_at: nil,
banner: %{},
background: %{},
note_count: 0,
follower_count: 0,
following_count: 0,
is_locked: false,
confirmation_pending: false,
password_reset_pending: false,
approval_pending: false,
registration_reason: nil,
confirmation_token: nil,
domain_blocks: [],
deactivated: true,
ap_enabled: false,
is_moderator: false,
is_admin: false,
mastofe_settings: nil,
mascot: nil,
emoji: %{},
pleroma_settings_store: %{},
fields: [],
raw_fields: [],
is_discoverable: false,
also_known_as: []
})
end
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
defp delete_and_invalidate_cache(%User{} = user) do
invalidate_cache(user)
Repo.delete(user)
end
defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
defp delete_or_deactivate(%User{local: true} = user) do
status = account_status(user)
case status do
:confirmation_pending ->
delete_and_invalidate_cache(user)
:approval_pending ->
delete_and_invalidate_cache(user)
_ ->
user
|> purge_user_changeset()
|> update_and_set_cache()
end
end
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
# Remove all relationships
user
|> get_followers()
|> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user)
unfollow(follower, user)
end)
user
|> get_friends()
|> Enum.each(fn followed ->
ActivityPub.unfollow(user, followed)
unfollow(user, followed)
end)
delete_user_activities(user)
delete_notifications_from_user_activities(user)
delete_outgoing_pending_follow_requests(user)
delete_or_deactivate(user)
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
User.Query.build(%{
external: true,
active: true,
order_by: :id
})
end
@spec external_users(keyword()) :: [User.t()]
def external_users(opts \\ []) do
query =
external_users_query()
|> select([u], struct(u, [:id, :ap_id]))
query =
if opts[:max_id],
do: where(query, [u], u.id > ^opts[:max_id]),
else: query
query =
if opts[:limit],
do: limit(query, ^opts[:limit]),
else: query
Repo.all(query)
end
def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
Notification
|> join(:inner, [n], activity in assoc(n, :activity))
|> where([n, a], fragment("? = ?", a.actor, ^ap_id))
|> Repo.delete_all()
end
def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
|> Repo.chunk_stream(50, :batches)
|> Stream.each(fn activities ->
Enum.each(activities, fn activity -> delete_activity(activity, user) end)
end)
|> Stream.run()
end
defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
{:ok, delete_data, _} <- Builder.delete(user, object) do
Pipeline.common_pipeline(delete_data, local: user.local)
else
{:find_object, nil} ->
# We have the create activity, but not the object, it was probably pruned.
# Insert a tombstone and try again
with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
{:ok, _tombstone} <- Object.create(tombstone_data) do
delete_activity(activity, user)
end
e ->
Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
Logger.error("Error: #{inspect(e)}")
end
end
defp delete_activity(%{data: %{"type" => type}} = activity, user)
when type in ["Like", "Announce"] do
{:ok, undo, _} = Builder.undo(user, activity)
Pipeline.common_pipeline(undo, local: user.local)
end
defp delete_activity(_activity, _user), do: "Doing nothing"
defp delete_outgoing_pending_follow_requests(user) do
user
|> FollowingRelationship.outgoing_pending_follow_requests_query()
|> Repo.delete_all()
end
def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
end
def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
def get_or_fetch_by_ap_id(ap_id) do
cached_user = get_cached_by_ap_id(ap_id)
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
case {cached_user, maybe_fetched_user} do
{_, {:ok, %User{} = user}} ->
{:ok, user}
{%User{} = user, _} ->
{:ok, user}
_ ->
{:error, :not_found}
end
end
@doc """
Creates an internal service actor by URI if missing.
Optionally takes nickname for addressing.
"""
@spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
def get_or_create_service_actor_by_ap_id(uri, nickname) do
{_, user} =
case get_cached_by_ap_id(uri) do
nil ->
with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
{:error, nil}
end
%User{invisible: false} = user ->
set_invisible(user)
user ->
{:ok, user}
end
user
end
@spec set_invisible(User.t()) :: {:ok, User.t()}
defp set_invisible(user) do
user
|> change(%{invisible: true})
|> update_and_set_cache()
end
@spec create_service_actor(String.t(), String.t()) ::
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
defp create_service_actor(uri, nickname) do
%User{
invisible: true,
local: true,
ap_id: uri,
nickname: nickname,
follower_address: uri <> "/followers"
}
|> change
|> unique_constraint(:nickname)
|> Repo.insert()
|> set_cache()
end
def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
key =
public_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
def public_key(_), do: {:error, "key not found"}
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key(user) do
{:ok, public_key}
else
_ -> :error
end
end
def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
def ap_enabled?(_), do: false
@doc "Gets or fetch a user by uri or nickname."
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
# wait a period of time and return newest version of the User structs
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
nil -> :error
end
end
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
nil -> :error
end
end
def parse_bio(bio) when is_binary(bio) and bio != "" do
bio
|> CommonUtils.format_input("text/plain", mentions_format: :full)
|> elem(0)
end
def parse_bio(_), do: ""
def parse_bio(bio, user) when is_binary(bio) and bio != "" do
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
bio
|> CommonUtils.format_input("text/plain",
mentions_format: :full,
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
end
def parse_bio(_, _), do: ""
def tag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
end)
end
def tag(nickname, tags) when is_binary(nickname),
do: tag(get_by_nickname(nickname), tags)
def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
def untag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
end)
end
def untag(nickname, tags) when is_binary(nickname),
do: untag(get_by_nickname(nickname), tags)
def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
defp update_tags(%User{} = user, new_tags) do
{:ok, updated_user} =
user
|> change(%{tags: new_tags})
|> update_and_set_cache()
updated_user
end
defp normalize_tags(tags) do
[tags]
|> List.flatten()
|> Enum.map(&String.downcase/1)
end
defp local_nickname_regex do
if Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else
@strict_local_nickname_regex
end
end
def local_nickname(nickname_or_mention) do
nickname_or_mention
|> full_nickname()
|> String.split("@")
|> hd()
end
def full_nickname(nickname_or_mention),
do: String.trim_leading(nickname_or_mention, "@")
def error_user(ap_id) do
%User{
name: ap_id,
ap_id: ap_id,
nickname: "erroruser@example.com",
inserted_at: NaiveDateTime.utc_now()
}
end
@spec all_superusers() :: [User.t()]
def all_superusers do
User.Query.build(%{super_users: true, local: true, deactivated: false})
|> Repo.all()
end
def muting_reblogs?(%User{} = user, %User{} = target) do
UserRelationship.reblog_mute_exists?(user, target)
end
def showing_reblogs?(%User{} = user, %User{} = target) do
not muting_reblogs?(user, target)
end
@doc """
The function returns a query to get users with no activity for given interval of days.
Inactive users are those who didn't read any notification, or had any activity where
the user is the activity's actor, during `inactivity_threshold` days.
Deactivated users will not appear in this list.
## Examples
iex> Pleroma.User.list_inactive_users()
%Ecto.Query{}
"""
@spec list_inactive_users_query(integer()) :: Ecto.Query.t()
def list_inactive_users_query(inactivity_threshold \\ 7) do
negative_inactivity_threshold = -inactivity_threshold
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
# Subqueries are not supported in `where` clauses, join gets too complicated.
has_read_notifications =
from(n in Pleroma.Notification,
where: n.seen == true,
group_by: n.id,
having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
select: n.user_id
)
|> Pleroma.Repo.all()
from(u in Pleroma.User,
left_join: a in Pleroma.Activity,
on: u.ap_id == a.actor,
where: not is_nil(u.nickname),
where: u.deactivated != ^true,
where: u.id not in ^has_read_notifications,
group_by: u.id,
having:
max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
is_nil(max(a.inserted_at))
)
end
@doc """
Enable or disable email notifications for user
## Examples
iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
Pleroma.User{email_notifications: %{"digest" => true}}
iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
Pleroma.User{email_notifications: %{"digest" => false}}
"""
@spec switch_email_notifications(t(), String.t(), boolean()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def switch_email_notifications(user, type, status) do
User.update_email_notifications(user, %{type => status})
end
@doc """
Set `last_digest_emailed_at` value for the user to current time
"""
@spec touch_last_digest_emailed_at(t()) :: t()
def touch_last_digest_emailed_at(user) do
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
{:ok, updated_user} =
user
|> change(%{last_digest_emailed_at: now})
|> update_and_set_cache()
updated_user
end
@spec need_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
def need_confirmation(%User{} = user, bool) do
user
|> confirmation_changeset(need_confirmation: bool)
|> update_and_set_cache()
end
def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
mascot
end
def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
# use instance-default
config = Config.get([:assets, :mascots])
default_mascot = Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, default_mascot)
%{
"id" => "default-mascot",
"url" => mascot[:url],
"preview_url" => mascot[:url],
"pleroma" => %{
"mime_type" => mascot[:mime_type]
}
}
end
def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
def ensure_keys_present(%User{} = user) do
with {:ok, pem} <- Keys.generate_rsa_pem() do
user
|> cast(%{keys: pem}, [:keys])
|> validate_required([:keys])
|> update_and_set_cache()
end
end
def get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
select: u.ap_id
)
|> Repo.all()
end
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
- change(changeset, password_hash: Pleroma.Password.hash_pwd_salt(password))
+ change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
def is_internal_user?(%User{nickname: nil}), do: true
def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
def is_internal_user?(_), do: false
# A hack because user delete activities have a fake id for whatever reason
# TODO: Get rid of this
def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
def get_delivered_users_by_object_id(object_id) do
from(u in User,
inner_join: delivery in assoc(u, :deliveries),
where: delivery.object_id == ^object_id
)
|> Repo.all()
end
def change_email(user, email) do
user
|> cast(%{email: email}, [:email])
|> validate_required([:email])
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> update_and_set_cache()
end
# Internal function; public one is `deactivate/2`
defp set_activation_status(user, deactivated) do
user
|> cast(%{deactivated: deactivated}, [:deactivated])
|> update_and_set_cache()
end
def update_banner(user, banner) do
user
|> cast(%{banner: banner}, [:banner])
|> update_and_set_cache()
end
def update_background(user, background) do
user
|> cast(%{background: background}, [:background])
|> update_and_set_cache()
end
def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
%{
admin: is_admin,
moderator: is_moderator
}
end
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Config.get([:instance, limit_name], 0)
changeset
|> validate_length(:fields, max: limit)
|> validate_change(:fields, fn :fields, fields ->
if Enum.all?(fields, &valid_field?/1) do
[]
else
[fields: "invalid"]
end
end)
end
defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Config.get([:instance, :account_field_name_length], 255)
value_limit = Config.get([:instance, :account_field_value_length], 255)
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
defp valid_field?(_), do: false
defp truncate_field(%{"name" => name, "value" => value}) do
{name, _chopped} =
String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
{value, _chopped} =
String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
%{"name" => name, "value" => value}
end
def admin_api_update(user, params) do
user
|> cast(params, [
:is_moderator,
:is_admin,
:show_role
])
|> update_and_set_cache()
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
def mascot_update(user, url) do
user
|> cast(%{mascot: url}, [:mascot])
|> validate_required([:mascot])
|> update_and_set_cache()
end
def mastodon_settings_update(user, settings) do
user
|> cast(%{mastofe_settings: settings}, [:mastofe_settings])
|> validate_required([:mastofe_settings])
|> update_and_set_cache()
end
@spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
def confirmation_changeset(user, need_confirmation: need_confirmation?) do
params =
if need_confirmation? do
%{
confirmation_pending: true,
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
}
else
%{
confirmation_pending: false,
confirmation_token: nil
}
end
cast(user, params, [:confirmation_pending, :confirmation_token])
end
@spec approval_changeset(User.t(), keyword()) :: Changeset.t()
def approval_changeset(user, need_approval: need_approval?) do
params = if need_approval?, do: %{approval_pending: true}, else: %{approval_pending: false}
cast(user, params, [:approval_pending])
end
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities do
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
params = %{pinned_activities: user.pinned_activities ++ [id]}
# if pinned activity was scheduled for deletion, we remove job
if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do
Oban.cancel_job(expiration.id)
end
user
|> cast(params, [:pinned_activities])
|> validate_length(:pinned_activities,
max: max_pinned_statuses,
message: "You have already pinned the maximum number of statuses"
)
else
change(user)
end
|> update_and_set_cache()
end
def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do
params = %{pinned_activities: List.delete(user.pinned_activities, id)}
# if pinned activity was scheduled for deletion, we reschedule it for deletion
if data["expires_at"] do
# MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
{:ok, expires_at} =
data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast()
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: id,
expires_at: expires_at
})
end
user
|> cast(params, [:pinned_activities])
|> update_and_set_cache()
end
def update_email_notifications(user, settings) do
email_notifications =
user.email_notifications
|> Map.merge(settings)
|> Map.take(["digest"])
params = %{email_notifications: email_notifications}
fields = [:email_notifications]
user
|> cast(params, fields)
|> validate_required(fields)
|> update_and_set_cache()
end
defp set_domain_blocks(user, domain_blocks) do
params = %{domain_blocks: domain_blocks}
user
|> cast(params, [:domain_blocks])
|> validate_required([:domain_blocks])
|> update_and_set_cache()
end
def block_domain(user, domain_blocked) do
set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
end
def unblock_domain(user, domain_blocked) do
set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship}
end
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship}
end
end
def set_invisible(user, invisible) do
params = %{invisible: invisible}
user
|> cast(params, [:invisible])
|> validate_required([:invisible])
|> update_and_set_cache()
end
def sanitize_html(%User{} = user) do
sanitize_html(user, nil)
end
# User data that mastodon isn't filtering (treated as plaintext):
# - field name
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
user
|> Map.put(:bio, HTML.filter_tags(user.bio, filter))
|> Map.put(:fields, fields)
end
def get_host(%User{ap_id: ap_id} = _user) do
URI.parse(ap_id).host
end
end
diff --git a/lib/pleroma/web/plugs/authentication_plug.ex b/lib/pleroma/web/plugs/authentication_plug.ex
index f7a2a3ab7..8d58169cf 100644
--- a/lib/pleroma/web/plugs/authentication_plug.ex
+++ b/lib/pleroma/web/plugs/authentication_plug.ex
@@ -1,72 +1,72 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.AuthenticationPlug do
@moduledoc "Password authentication plug."
alias Pleroma.Helpers.AuthHelper
alias Pleroma.User
import Plug.Conn
require Logger
def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(
%{
assigns: %{
auth_user: %{password_hash: password_hash} = auth_user,
auth_credentials: %{password: password}
}
} = conn,
_
) do
if checkpw(password, password_hash) do
{:ok, auth_user} = maybe_update_password(auth_user, password)
conn
|> assign(:user, auth_user)
|> AuthHelper.skip_oauth()
else
conn
end
end
def call(conn, _), do: conn
def checkpw(password, "$6" <> _ = password_hash) do
:crypt.crypt(password, password_hash) == password_hash
end
def checkpw(password, "$2" <> _ = password_hash) do
# Handle bcrypt passwords for Mastodon migration
Bcrypt.verify_pass(password, password_hash)
end
def checkpw(password, "$pbkdf2" <> _ = password_hash) do
- Pleroma.Password.verify_pass(password, password_hash)
+ Pleroma.Password.Pbkdf2.verify_pass(password, password_hash)
end
def checkpw(_password, _password_hash) do
Logger.error("Password hash not recognized")
false
end
def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do
do_update_password(user, password)
end
def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
do_update_password(user, password)
end
def maybe_update_password(user, _), do: {:ok, user}
defp do_update_password(user, password) do
User.reset_password(user, %{password: password, password_confirmation: password})
end
end
diff --git a/test/pleroma/mfa_test.exs b/test/pleroma/mfa_test.exs
index db68fc1ca..76ba1a99d 100644
--- a/test/pleroma/mfa_test.exs
+++ b/test/pleroma/mfa_test.exs
@@ -1,52 +1,52 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFATest do
use Pleroma.DataCase, async: true
import Pleroma.Factory
alias Pleroma.MFA
describe "mfa_settings" do
test "returns settings user's" do
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: "xx", confirmed: true}
}
)
settings = MFA.mfa_settings(user)
assert match?(^settings, %{enabled: true, totp: true})
end
end
describe "generate backup codes" do
test "returns backup codes" do
user = insert(:user)
{:ok, [code1, code2]} = MFA.generate_backup_codes(user)
updated_user = refresh_record(user)
[hash1, hash2] = updated_user.multi_factor_authentication_settings.backup_codes
- assert Pleroma.Password.verify_pass(code1, hash1)
- assert Pleroma.Password.verify_pass(code2, hash2)
+ assert Pleroma.Password.Pbkdf2.verify_pass(code1, hash1)
+ assert Pleroma.Password.Pbkdf2.verify_pass(code2, hash2)
end
end
describe "invalidate_backup_code" do
test "invalid used code" do
user = insert(:user)
{:ok, _} = MFA.generate_backup_codes(user)
user = refresh_record(user)
assert length(user.multi_factor_authentication_settings.backup_codes) == 2
[hash_code | _] = user.multi_factor_authentication_settings.backup_codes
{:ok, user} = MFA.invalidate_backup_code(user, hash_code)
assert length(user.multi_factor_authentication_settings.backup_codes) == 1
end
end
end
diff --git a/test/pleroma/password/pbkdf2_test.exs b/test/pleroma/password/pbkdf2_test.exs
index 4acbda939..e55348f9a 100644
--- a/test/pleroma/password/pbkdf2_test.exs
+++ b/test/pleroma/password/pbkdf2_test.exs
@@ -1,35 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Password.Pbkdf2Test do
use Pleroma.DataCase, async: true
- alias Pleroma.Password.Pbkdf2
+ alias Pleroma.Password.Pbkdf2, as: Password
test "it generates the same hash as pbkd2_elixir" do
- # hash = Pleroma.Password.Pbkdf2.hash_pwd_salt("password")
+ # hash = Pbkdf2.hash_pwd_salt("password")
hash =
"$pbkdf2-sha512$1$QJpEYw8iBKcnY.4Rm0eCVw$UBPeWQ91RxSv3snxsb/ZzMeG/2aa03c541bbo8vQudREGNta5t8jBQrd00fyJp8RjaqfvgdZxy2rhSwljyu21g"
# Use the same randomly generated salt
salt = Password.decode64("QJpEYw8iBKcnY.4Rm0eCVw")
assert hash == Password.hash_pwd_salt("password", salt: salt)
end
@tag skip: "Works when Pbkd2 is present. Source: trust me bro"
- test "Pleroma.Password.Pbkdf2 can verify passwords generated with it" do
- hash = Password.hash_pwd_salt("password")
-
- assert Pleroma.Password.Pbkdf2.verify_pass("password", hash)
+ test "Pbkdf2 can verify passwords generated with it" do
+ # Commented to prevent warnings.
+ # hash = Password.hash_pwd_salt("password")
+ # assert Pbkdf2.verify_pass("password", hash)
end
test "it verifies pbkdf2_elixir hashes" do
- # hash = Pleroma.Password.Pbkdf2.hash_pwd_salt("password")
+ # hash = Pbkdf2.hash_pwd_salt("password")
hash =
"$pbkdf2-sha512$1$QJpEYw8iBKcnY.4Rm0eCVw$UBPeWQ91RxSv3snxsb/ZzMeG/2aa03c541bbo8vQudREGNta5t8jBQrd00fyJp8RjaqfvgdZxy2rhSwljyu21g"
assert Password.verify_pass("password", hash)
end
end
diff --git a/test/pleroma/password_test.exs b/test/pleroma/password_test.exs
deleted file mode 100644
index 6ed0ca826..000000000
--- a/test/pleroma/password_test.exs
+++ /dev/null
@@ -1,35 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.PasswordTest do
- use Pleroma.DataCase, async: true
-
- alias Pleroma.Password
-
- test "it generates the same hash as pbkd2_elixir" do
- # hash = Pleroma.Password.hash_pwd_salt("password")
- hash =
- "$pbkdf2-sha512$1$QJpEYw8iBKcnY.4Rm0eCVw$UBPeWQ91RxSv3snxsb/ZzMeG/2aa03c541bbo8vQudREGNta5t8jBQrd00fyJp8RjaqfvgdZxy2rhSwljyu21g"
-
- # Use the same randomly generated salt
- salt = Password.decode64("QJpEYw8iBKcnY.4Rm0eCVw")
-
- assert hash == Password.hash_pwd_salt("password", salt: salt)
- end
-
- @tag skip: "Works when Pbkd2 is present. Source: trust me bro"
- test "Pleroma.Password can verify passwords generated with it" do
- hash = Password.hash_pwd_salt("password")
-
- assert Pleroma.Password.verify_pass("password", hash)
- end
-
- test "it verifies pbkdf2_elixir hashes" do
- # hash = Pleroma.Password.hash_pwd_salt("password")
- hash =
- "$pbkdf2-sha512$1$QJpEYw8iBKcnY.4Rm0eCVw$UBPeWQ91RxSv3snxsb/ZzMeG/2aa03c541bbo8vQudREGNta5t8jBQrd00fyJp8RjaqfvgdZxy2rhSwljyu21g"
-
- assert Password.verify_pass("password", hash)
- end
-end
diff --git a/test/pleroma/web/auth/basic_auth_test.exs b/test/pleroma/web/auth/basic_auth_test.exs
index b74516dd6..2816aae4c 100644
--- a/test/pleroma/web/auth/basic_auth_test.exs
+++ b/test/pleroma/web/auth/basic_auth_test.exs
@@ -1,46 +1,46 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.BasicAuthTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
test "with HTTP Basic Auth used, grants access to OAuth scope-restricted endpoints", %{
conn: conn
} do
user = insert(:user)
- assert Pleroma.Password.verify_pass("test", user.password_hash)
+ assert Pleroma.Password.Pbkdf2.verify_pass("test", user.password_hash)
basic_auth_contents =
(URI.encode_www_form(user.nickname) <> ":" <> URI.encode_www_form("test"))
|> Base.encode64()
# Succeeds with HTTP Basic Auth
response =
conn
|> put_req_header("authorization", "Basic " <> basic_auth_contents)
|> get("/api/v1/accounts/verify_credentials")
|> json_response(200)
user_nickname = user.nickname
assert %{"username" => ^user_nickname} = response
# Succeeds with a properly scoped OAuth token
valid_token = insert(:oauth_token, scopes: ["read:accounts"])
conn
|> put_req_header("authorization", "Bearer #{valid_token.token}")
|> get("/api/v1/accounts/verify_credentials")
|> json_response(200)
# Fails with a wrong-scoped OAuth token (proof of restriction)
invalid_token = insert(:oauth_token, scopes: ["read:something"])
conn
|> put_req_header("authorization", "Bearer #{invalid_token.token}")
|> get("/api/v1/accounts/verify_credentials")
|> json_response(403)
end
end
diff --git a/test/pleroma/web/auth/pleroma_authenticator_test.exs b/test/pleroma/web/auth/pleroma_authenticator_test.exs
index ec63e2d41..edaf9eecb 100644
--- a/test/pleroma/web/auth/pleroma_authenticator_test.exs
+++ b/test/pleroma/web/auth/pleroma_authenticator_test.exs
@@ -1,48 +1,48 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Web.Auth.PleromaAuthenticator
import Pleroma.Factory
setup do
password = "testpassword"
name = "AgentSmith"
- user = insert(:user, nickname: name, password_hash: Pleroma.Password.hash_pwd_salt(password))
+ user = insert(:user, nickname: name, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
{:ok, [user: user, name: name, password: password]}
end
test "get_user/authorization", %{name: name, password: password} do
name = name <> "1"
user = insert(:user, nickname: name, password_hash: Bcrypt.hash_pwd_salt(password))
params = %{"authorization" => %{"name" => name, "password" => password}}
res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
assert {:ok, returned_user} = res
assert returned_user.id == user.id
assert "$pbkdf2" <> _ = returned_user.password_hash
end
test "get_user/authorization with invalid password", %{name: name} do
params = %{"authorization" => %{"name" => name, "password" => "password"}}
res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
assert {:error, {:checkpw, false}} == res
end
test "get_user/grant_type_password", %{user: user, name: name, password: password} do
params = %{"grant_type" => "password", "username" => name, "password" => password}
res = PleromaAuthenticator.get_user(%Plug.Conn{params: params})
assert {:ok, user} == res
end
test "error credintails" do
res = PleromaAuthenticator.get_user(%Plug.Conn{params: %{}})
assert {:error, :invalid_credentials} == res
end
end
diff --git a/test/pleroma/web/auth/totp_authenticator_test.exs b/test/pleroma/web/auth/totp_authenticator_test.exs
index 6d2646b61..ac4209f2d 100644
--- a/test/pleroma/web/auth/totp_authenticator_test.exs
+++ b/test/pleroma/web/auth/totp_authenticator_test.exs
@@ -1,51 +1,51 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.TOTPAuthenticatorTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.MFA
alias Pleroma.MFA.BackupCodes
alias Pleroma.MFA.TOTP
alias Pleroma.Web.Auth.TOTPAuthenticator
import Pleroma.Factory
test "verify token" do
otp_secret = TOTP.generate_secret()
otp_token = TOTP.generate_token(otp_secret)
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
assert TOTPAuthenticator.verify(otp_token, user) == {:ok, :pass}
assert TOTPAuthenticator.verify(nil, user) == {:error, :invalid_token}
assert TOTPAuthenticator.verify("", user) == {:error, :invalid_token}
end
test "checks backup codes" do
[code | _] = backup_codes = BackupCodes.generate()
hashed_codes =
backup_codes
- |> Enum.map(&Pleroma.Password.hash_pwd_salt(&1))
+ |> Enum.map(&Pleroma.Password.Pbkdf2.hash_pwd_salt(&1))
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
backup_codes: hashed_codes,
totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true}
}
)
assert TOTPAuthenticator.verify_recovery_code(user, code) == {:ok, :pass}
refute TOTPAuthenticator.verify_recovery_code(code, refresh_record(user)) == {:ok, :pass}
end
end
diff --git a/test/pleroma/web/mongoose_im_controller_test.exs b/test/pleroma/web/mongoose_im_controller_test.exs
index 183a17a02..a7225d45c 100644
--- a/test/pleroma/web/mongoose_im_controller_test.exs
+++ b/test/pleroma/web/mongoose_im_controller_test.exs
@@ -1,81 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MongooseIMControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
test "/user_exists", %{conn: conn} do
_user = insert(:user, nickname: "lain")
_remote_user = insert(:user, nickname: "alice", local: false)
_deactivated_user = insert(:user, nickname: "konata", deactivated: true)
res =
conn
|> get(mongoose_im_path(conn, :user_exists), user: "lain")
|> json_response(200)
assert res == true
res =
conn
|> get(mongoose_im_path(conn, :user_exists), user: "alice")
|> json_response(404)
assert res == false
res =
conn
|> get(mongoose_im_path(conn, :user_exists), user: "bob")
|> json_response(404)
assert res == false
res =
conn
|> get(mongoose_im_path(conn, :user_exists), user: "konata")
|> json_response(404)
assert res == false
end
test "/check_password", %{conn: conn} do
- user = insert(:user, password_hash: Pleroma.Password.hash_pwd_salt("cool"))
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("cool"))
_deactivated_user =
insert(:user,
nickname: "konata",
deactivated: true,
- password_hash: Pleroma.Password.hash_pwd_salt("cool")
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("cool")
)
res =
conn
|> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "cool")
|> json_response(200)
assert res == true
res =
conn
|> get(mongoose_im_path(conn, :check_password), user: user.nickname, pass: "uncool")
|> json_response(403)
assert res == false
res =
conn
|> get(mongoose_im_path(conn, :check_password), user: "konata", pass: "cool")
|> json_response(404)
assert res == false
res =
conn
|> get(mongoose_im_path(conn, :check_password), user: "nobody", pass: "cool")
|> json_response(404)
assert res == false
end
end
diff --git a/test/pleroma/web/o_auth/ldap_authorization_test.exs b/test/pleroma/web/o_auth/ldap_authorization_test.exs
index 9ebd084a5..61b9ce6b7 100644
--- a/test/pleroma/web/o_auth/ldap_authorization_test.exs
+++ b/test/pleroma/web/o_auth/ldap_authorization_test.exs
@@ -1,135 +1,135 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do
use Pleroma.Web.ConnCase
alias Pleroma.Repo
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
import Mock
@skip if !Code.ensure_loaded?(:eldap), do: :skip
setup_all do: clear_config([:ldap, :enabled], true)
setup_all do: clear_config(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator)
@tag @skip
test "authorizes the existing user using LDAP credentials" do
password = "testpassword"
- user = insert(:user, password_hash: Pleroma.Password.hash_pwd_salt(password))
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
app = insert(:oauth_app, scopes: ["read", "write"])
host = Pleroma.Config.get([:ldap, :host]) |> to_charlist
port = Pleroma.Config.get([:ldap, :port])
with_mocks [
{:eldap, [],
[
open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
simple_bind: fn _connection, _dn, ^password -> :ok end,
close: fn _connection ->
send(self(), :close_connection)
:ok
end
]}
] do
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token} = json_response(conn, 200)
token = Repo.get_by(Token, token: token)
assert token.user_id == user.id
assert_received :close_connection
end
end
@tag @skip
test "creates a new user after successful LDAP authorization" do
password = "testpassword"
user = build(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
host = Pleroma.Config.get([:ldap, :host]) |> to_charlist
port = Pleroma.Config.get([:ldap, :port])
with_mocks [
{:eldap, [],
[
open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
simple_bind: fn _connection, _dn, ^password -> :ok end,
equalityMatch: fn _type, _value -> :ok end,
wholeSubtree: fn -> :ok end,
search: fn _connection, _options ->
{:ok, {:eldap_search_result, [{:eldap_entry, '', []}], []}}
end,
close: fn _connection ->
send(self(), :close_connection)
:ok
end
]}
] do
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token} = json_response(conn, 200)
token = Repo.get_by(Token, token: token) |> Repo.preload(:user)
assert token.user.nickname == user.nickname
assert_received :close_connection
end
end
@tag @skip
test "disallow authorization for wrong LDAP credentials" do
password = "testpassword"
- user = insert(:user, password_hash: Pleroma.Password.hash_pwd_salt(password))
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
app = insert(:oauth_app, scopes: ["read", "write"])
host = Pleroma.Config.get([:ldap, :host]) |> to_charlist
port = Pleroma.Config.get([:ldap, :port])
with_mocks [
{:eldap, [],
[
open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
simple_bind: fn _connection, _dn, ^password -> {:error, :invalidCredentials} end,
close: fn _connection ->
send(self(), :close_connection)
:ok
end
]}
] do
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"error" => "Invalid credentials"} = json_response(conn, 400)
assert_received :close_connection
end
end
end
diff --git a/test/pleroma/web/o_auth/mfa_controller_test.exs b/test/pleroma/web/o_auth/mfa_controller_test.exs
index dacf03b2b..17bbde85b 100644
--- a/test/pleroma/web/o_auth/mfa_controller_test.exs
+++ b/test/pleroma/web/o_auth/mfa_controller_test.exs
@@ -1,304 +1,304 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.MFAControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
alias Pleroma.MFA
alias Pleroma.MFA.BackupCodes
alias Pleroma.MFA.TOTP
alias Pleroma.Repo
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.OAuthController
setup %{conn: conn} do
otp_secret = TOTP.generate_secret()
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
- backup_codes: [Pleroma.Password.hash_pwd_salt("test-code")],
+ backup_codes: [Pleroma.Password.Pbkdf2.hash_pwd_salt("test-code")],
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
app = insert(:oauth_app)
{:ok, conn: conn, user: user, app: app}
end
describe "show" do
setup %{conn: conn, user: user, app: app} do
mfa_token =
insert(:mfa_token,
user: user,
authorization: build(:oauth_authorization, app: app, scopes: ["write"])
)
{:ok, conn: conn, mfa_token: mfa_token}
end
test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do
conn =
get(
conn,
"/oauth/mfa",
%{
"mfa_token" => mfa_token.token,
"state" => "a_state",
"redirect_uri" => "http://localhost:8080/callback"
}
)
assert response = html_response(conn, 200)
assert response =~ "Two-factor authentication"
assert response =~ mfa_token.token
assert response =~ "http://localhost:8080/callback"
end
test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do
conn =
get(
conn,
"/oauth/mfa",
%{
"mfa_token" => mfa_token.token,
"state" => "a_state",
"redirect_uri" => "http://localhost:8080/callback",
"challenge_type" => "recovery"
}
)
assert response = html_response(conn, 200)
assert response =~ "Two-factor recovery"
assert response =~ mfa_token.token
assert response =~ "http://localhost:8080/callback"
end
end
describe "verify" do
setup %{conn: conn, user: user, app: app} do
mfa_token =
insert(:mfa_token,
user: user,
authorization: build(:oauth_authorization, app: app, scopes: ["write"])
)
{:ok, conn: conn, user: user, mfa_token: mfa_token, app: app}
end
test "POST /oauth/mfa/verify, verify totp code", %{
conn: conn,
user: user,
mfa_token: mfa_token,
app: app
} do
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
conn =
conn
|> post("/oauth/mfa/verify", %{
"mfa" => %{
"mfa_token" => mfa_token.token,
"challenge_type" => "totp",
"code" => otp_token,
"state" => "a_state",
"redirect_uri" => OAuthController.default_redirect_uri(app)
}
})
target = redirected_to(conn)
target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string()
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
assert %{"state" => "a_state", "code" => code} = query
assert target_url == OAuthController.default_redirect_uri(app)
auth = Repo.get_by(Authorization, token: code)
assert auth.scopes == ["write"]
end
test "POST /oauth/mfa/verify, verify recovery code", %{
conn: conn,
mfa_token: mfa_token,
app: app
} do
conn =
conn
|> post("/oauth/mfa/verify", %{
"mfa" => %{
"mfa_token" => mfa_token.token,
"challenge_type" => "recovery",
"code" => "test-code",
"state" => "a_state",
"redirect_uri" => OAuthController.default_redirect_uri(app)
}
})
target = redirected_to(conn)
target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string()
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
assert %{"state" => "a_state", "code" => code} = query
assert target_url == OAuthController.default_redirect_uri(app)
auth = Repo.get_by(Authorization, token: code)
assert auth.scopes == ["write"]
end
end
describe "challenge/totp" do
test "returns access token with valid code", %{conn: conn, user: user, app: app} do
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
mfa_token =
insert(:mfa_token,
user: user,
authorization: build(:oauth_authorization, app: app, scopes: ["write"])
)
response =
conn
|> post("/oauth/mfa/challenge", %{
"mfa_token" => mfa_token.token,
"challenge_type" => "totp",
"code" => otp_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(:ok)
ap_id = user.ap_id
assert match?(
%{
"access_token" => _,
"me" => ^ap_id,
"refresh_token" => _,
"scope" => "write",
"token_type" => "Bearer"
},
response
)
end
test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
response =
conn
|> post("/oauth/mfa/challenge", %{
"mfa_token" => "XXX",
"challenge_type" => "totp",
"code" => otp_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert response == %{"error" => "Invalid code"}
end
test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do
mfa_token = insert(:mfa_token, user: user)
response =
conn
|> post("/oauth/mfa/challenge", %{
"mfa_token" => mfa_token.token,
"challenge_type" => "totp",
"code" => "XXX",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert response == %{"error" => "Invalid code"}
end
test "returns error when client credentails is wrong ", %{conn: conn, user: user} do
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
mfa_token = insert(:mfa_token, user: user)
response =
conn
|> post("/oauth/mfa/challenge", %{
"mfa_token" => mfa_token.token,
"challenge_type" => "totp",
"code" => otp_token,
"client_id" => "xxx",
"client_secret" => "xxx"
})
|> json_response(400)
assert response == %{"error" => "Invalid code"}
end
end
describe "challenge/recovery" do
setup %{conn: conn} do
app = insert(:oauth_app)
{:ok, conn: conn, app: app}
end
test "returns access token with valid code", %{conn: conn, app: app} do
otp_secret = TOTP.generate_secret()
[code | _] = backup_codes = BackupCodes.generate()
hashed_codes =
backup_codes
- |> Enum.map(&Pleroma.Password.hash_pwd_salt(&1))
+ |> Enum.map(&Pleroma.Password.Pbkdf2.hash_pwd_salt(&1))
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
backup_codes: hashed_codes,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
mfa_token =
insert(:mfa_token,
user: user,
authorization: build(:oauth_authorization, app: app, scopes: ["write"])
)
response =
conn
|> post("/oauth/mfa/challenge", %{
"mfa_token" => mfa_token.token,
"challenge_type" => "recovery",
"code" => code,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(:ok)
ap_id = user.ap_id
assert match?(
%{
"access_token" => _,
"me" => ^ap_id,
"refresh_token" => _,
"scope" => "write",
"token_type" => "Bearer"
},
response
)
error_response =
conn
|> post("/oauth/mfa/challenge", %{
"mfa_token" => mfa_token.token,
"challenge_type" => "recovery",
"code" => code,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert error_response == %{"error" => "Invalid code"}
end
end
end
diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs
index c6ee7b7e8..bf47afed8 100644
--- a/test/pleroma/web/o_auth/o_auth_controller_test.exs
+++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs
@@ -1,1305 +1,1305 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.Helpers.AuthHelper
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.Token
@session_opts [
store: :cookie,
key: "_test",
signing_salt: "cooldude"
]
setup do
clear_config([:instance, :account_activation_required])
clear_config([:instance, :account_approval_required])
end
describe "in OAuth consumer mode, " do
setup do
[
app: insert(:oauth_app),
conn:
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
]
end
setup do: clear_config([:auth, :oauth_consumer_strategies], ~w(twitter facebook))
test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert response = html_response(conn, 200)
assert response =~ "Sign in with Twitter"
assert response =~ o_auth_path(conn, :prepare_request)
end
test "GET /oauth/prepare_request encodes parameters as `state` and redirects", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/prepare_request",
%{
"provider" => "twitter",
"authorization" => %{
"scope" => "read follow",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state"
}
}
)
assert html_response(conn, 302)
redirect_query = URI.parse(redirected_to(conn)).query
assert %{"state" => state_param} = URI.decode_query(redirect_query)
assert {:ok, state_components} = Jason.decode(state_param)
expected_client_id = app.client_id
expected_redirect_uri = app.redirect_uris
assert %{
"scope" => "read follow",
"client_id" => ^expected_client_id,
"redirect_uri" => ^expected_redirect_uri,
"state" => "a_state"
} = state_components
end
test "with user-bound registration, GET /oauth/<provider>/callback redirects to `redirect_uri` with `code`",
%{app: app, conn: conn} do
registration = insert(:registration)
redirect_uri = OAuthController.default_redirect_uri(app)
state_params = %{
"scope" => Enum.join(app.scopes, " "),
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => ""
}
conn =
conn
|> assign(:ueberauth_auth, %{provider: registration.provider, uid: registration.uid})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Jason.encode!(state_params)
}
)
assert html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with user-unbound registration, GET /oauth/<provider>/callback renders registration_details page",
%{app: app, conn: conn} do
user = insert(:user)
state_params = %{
"scope" => "read write",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state"
}
conn =
conn
|> assign(:ueberauth_auth, %{
provider: "twitter",
uid: "171799000",
info: %{nickname: user.nickname, email: user.email, name: user.name, description: nil}
})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Jason.encode!(state_params)
}
)
assert response = html_response(conn, 200)
assert response =~ ~r/name="op" type="submit" value="register"/
assert response =~ ~r/name="op" type="submit" value="connect"/
assert response =~ user.email
assert response =~ user.nickname
end
test "on authentication error, GET /oauth/<provider>/callback redirects to `redirect_uri`", %{
app: app,
conn: conn
} do
state_params = %{
"scope" => Enum.join(app.scopes, " "),
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => ""
}
conn =
conn
|> assign(:ueberauth_failure, %{errors: [%{message: "(error description)"}]})
|> get(
"/oauth/twitter/callback",
%{
"oauth_token" => "G-5a3AAAAAAAwMH9AAABaektfSM",
"oauth_verifier" => "QZl8vUqNvXMTKpdmUnGejJxuHG75WWWs",
"provider" => "twitter",
"state" => Jason.encode!(state_params)
}
)
assert html_response(conn, 302)
assert redirected_to(conn) == app.redirect_uris
assert get_flash(conn, :error) == "Failed to authenticate: (error description)."
end
test "GET /oauth/registration_details renders registration details form", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/registration_details",
%{
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"nickname" => nil,
"email" => "john@doe.com"
}
}
)
assert response = html_response(conn, 200)
assert response =~ ~r/name="op" type="submit" value="register"/
assert response =~ ~r/name="op" type="submit" value="connect"/
end
test "with valid params, POST /oauth/register?op=register redirects to `redirect_uri` with `code`",
%{
app: app,
conn: conn
} do
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
redirect_uri = OAuthController.default_redirect_uri(app)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "a_state",
"nickname" => "availablenick",
"email" => "available@email.com"
}
}
)
assert html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with unlisted `redirect_uri`, POST /oauth/register?op=register results in HTTP 401",
%{
app: app,
conn: conn
} do
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
unlisted_redirect_uri = "http://cross-site-request.com"
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "a_state",
"nickname" => "availablenick",
"email" => "available@email.com"
}
}
)
assert html_response(conn, 401)
end
test "with invalid params, POST /oauth/register?op=register renders registration_details page",
%{
app: app,
conn: conn
} do
another_user = insert(:user)
registration = insert(:registration, user: nil, info: %{"nickname" => nil, "email" => nil})
params = %{
"op" => "register",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"nickname" => "availablenickname",
"email" => "available@email.com"
}
}
for {bad_param, bad_param_value} <-
[{"nickname", another_user.nickname}, {"email", another_user.email}] do
bad_registration_attrs = %{
"authorization" => Map.put(params["authorization"], bad_param, bad_param_value)
}
bad_params = Map.merge(params, bad_registration_attrs)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post("/oauth/register", bad_params)
assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/
assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken."
end
end
test "with valid params, POST /oauth/register?op=connect redirects to `redirect_uri` with `code`",
%{
app: app,
conn: conn
} do
- user = insert(:user, password_hash: Pleroma.Password.hash_pwd_salt("testpassword"))
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword"))
registration = insert(:registration, user: nil)
redirect_uri = OAuthController.default_redirect_uri(app)
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "a_state",
"name" => user.nickname,
"password" => "testpassword"
}
}
)
assert html_response(conn, 302)
assert redirected_to(conn) =~ ~r/#{redirect_uri}\?code=.+/
end
test "with unlisted `redirect_uri`, POST /oauth/register?op=connect results in HTTP 401`",
%{
app: app,
conn: conn
} do
- user = insert(:user, password_hash: Pleroma.Password.hash_pwd_salt("testpassword"))
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("testpassword"))
registration = insert(:registration, user: nil)
unlisted_redirect_uri = "http://cross-site-request.com"
conn =
conn
|> put_session(:registration_id, registration.id)
|> post(
"/oauth/register",
%{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "a_state",
"name" => user.nickname,
"password" => "testpassword"
}
}
)
assert html_response(conn, 401)
end
test "with invalid params, POST /oauth/register?op=connect renders registration_details page",
%{
app: app,
conn: conn
} do
user = insert(:user)
registration = insert(:registration, user: nil)
params = %{
"op" => "connect",
"authorization" => %{
"scopes" => app.scopes,
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "a_state",
"name" => user.nickname,
"password" => "wrong password"
}
}
conn =
conn
|> put_session(:registration_id, registration.id)
|> post("/oauth/register", params)
assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/
assert get_flash(conn, :error) == "Invalid Username/Password"
end
end
describe "GET /oauth/authorize" do
setup do
[
app: insert(:oauth_app, redirect_uris: "https://redirect.url"),
conn:
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
]
end
test "renders authentication page", %{app: app, conn: conn} do
conn =
get(
conn,
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "properly handles internal calls with `authorization`-wrapped params", %{
app: app,
conn: conn
} do
conn =
get(
conn,
"/oauth/authorize",
%{
"authorization" => %{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
%{app: app, conn: conn} do
token = insert(:oauth_token, app: app)
conn =
conn
|> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read",
"force_login" => "true"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "renders authentication page if user is already authenticated but user request with another client",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app: app)
conn =
conn
|> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => "another_client_id",
"redirect_uri" => OAuthController.default_redirect_uri(app),
"scope" => "read"
}
)
assert html_response(conn, 200) =~ ~s(type="submit")
end
test "with existing authentication and non-OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app: app)
conn =
conn
|> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"state" => "specific_client_state",
"scope" => "read"
}
)
assert URI.decode(redirected_to(conn)) ==
"https://redirect.url?access_token=#{token.token}&state=specific_client_state"
end
test "with existing authentication and unlisted non-OOB `redirect_uri`, redirects without credentials",
%{
app: app,
conn: conn
} do
unlisted_redirect_uri = "http://cross-site-request.com"
token = insert(:oauth_token, app: app)
conn =
conn
|> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => unlisted_redirect_uri,
"state" => "specific_client_state",
"scope" => "read"
}
)
assert redirected_to(conn) == unlisted_redirect_uri
end
test "with existing authentication and OOB `redirect_uri`, redirects to app with `token` and `state` params",
%{
app: app,
conn: conn
} do
token = insert(:oauth_token, app: app)
conn =
conn
|> AuthHelper.put_session_token(token.token)
|> get(
"/oauth/authorize",
%{
"response_type" => "code",
"client_id" => app.client_id,
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
"scope" => "read"
}
)
assert html_response(conn, 200) =~ "Authorization exists"
end
end
describe "POST /oauth/authorize" do
test "redirects with oauth authorization, " <>
"granting requested app-supported scopes to both admin- and non-admin users" do
app_scopes = ["read", "write", "admin", "secret_scope"]
app = insert(:oauth_app, scopes: app_scopes)
redirect_uri = OAuthController.default_redirect_uri(app)
non_admin = insert(:user, is_admin: false)
admin = insert(:user, is_admin: true)
scopes_subset = ["read:subscope", "write", "admin"]
# In case scope param is missing, expecting _all_ app-supported scopes to be granted
for user <- [non_admin, admin],
{requested_scopes, expected_scopes} <-
%{scopes_subset => scopes_subset, nil: app_scopes} do
conn =
post(
build_conn(),
"/oauth/authorize",
%{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"scope" => requested_scopes,
"state" => "statepassed"
}
}
)
target = redirected_to(conn)
assert target =~ redirect_uri
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
assert %{"state" => "statepassed", "code" => code} = query
auth = Repo.get_by(Authorization, token: code)
assert auth
assert auth.scopes == expected_scopes
end
end
test "authorize from cookie" do
user = insert(:user)
app = insert(:oauth_app)
oauth_token = insert(:oauth_token, user: user, app: app)
redirect_uri = OAuthController.default_redirect_uri(app)
conn =
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
|> AuthHelper.put_session_token(oauth_token.token)
|> post(
"/oauth/authorize",
%{
"authorization" => %{
"name" => user.nickname,
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"scope" => app.scopes,
"state" => "statepassed"
}
}
)
target = redirected_to(conn)
assert target =~ redirect_uri
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
assert %{"state" => "statepassed", "code" => code} = query
auth = Repo.get_by(Authorization, token: code)
assert auth
assert auth.scopes == app.scopes
end
test "redirect to on two-factor auth page" do
otp_secret = TOTP.generate_secret()
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
app = insert(:oauth_app, scopes: ["read", "write", "follow"])
conn =
build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => app.redirect_uris,
"scope" => "read write",
"state" => "statepassed"
}
})
result = html_response(conn, 200)
mfa_token = Repo.get_by(MFA.Token, user_id: user.id)
assert result =~ app.redirect_uris
assert result =~ "statepassed"
assert result =~ mfa_token.token
assert result =~ "Two-factor authentication"
end
test "returns 401 for wrong credentials", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app)
redirect_uri = OAuthController.default_redirect_uri(app)
result =
conn
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "wrong",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => Enum.join(app.scopes, " ")
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "Invalid Username/Password"
end
test "returns 401 for missing scopes" do
user = insert(:user, is_admin: false)
app = insert(:oauth_app, scopes: ["read", "write", "admin"])
redirect_uri = OAuthController.default_redirect_uri(app)
result =
build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => ""
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "This action is outside the authorized scopes"
end
test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
redirect_uri = OAuthController.default_redirect_uri(app)
result =
conn
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"state" => "statepassed",
"scope" => "read write follow"
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ redirect_uri
# Error message
assert result =~ "This action is outside the authorized scopes"
end
end
describe "POST /oauth/token" do
test "issues a token for an all-body request" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app),
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token, "me" => ap_id} = json_response(conn, 200)
token = Repo.get_by(Token, token: token)
assert token
assert token.scopes == auth.scopes
assert user.ap_id == ap_id
end
test "issues a token for `password` grant_type with valid credentials, with full permissions by default" do
password = "testpassword"
- user = insert(:user, password_hash: Pleroma.Password.hash_pwd_salt(password))
+ user = insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
app = insert(:oauth_app, scopes: ["read", "write"])
# Note: "scope" param is intentionally omitted
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token} = json_response(conn, 200)
token = Repo.get_by(Token, token: token)
assert token
assert token.scopes == app.scopes
end
test "issues a mfa token for `password` grant_type, when MFA enabled" do
password = "testpassword"
otp_secret = TOTP.generate_secret()
user =
insert(:user,
- password_hash: Pleroma.Password.hash_pwd_salt(password),
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
app = insert(:oauth_app, scopes: ["read", "write"])
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(403)
assert match?(
%{
"supported_challenge_types" => "totp",
"mfa_token" => _,
"error" => "mfa_required"
},
response
)
token = Repo.get_by(MFA.Token, token: response["mfa_token"])
assert token.user_id == user.id
assert token.authorization_id
end
test "issues a token for request with HTTP basic auth client credentials" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"])
{:ok, auth} = Authorization.create_authorization(app, user, ["scope1", "scope2"])
assert auth.scopes == ["scope1", "scope2"]
app_encoded =
(URI.encode_www_form(app.client_id) <> ":" <> URI.encode_www_form(app.client_secret))
|> Base.encode64()
conn =
build_conn()
|> put_req_header("authorization", "Basic " <> app_encoded)
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app)
})
assert %{"access_token" => token, "scope" => scope} = json_response(conn, 200)
assert scope == "scope1 scope2"
token = Repo.get_by(Token, token: token)
assert token
assert token.scopes == ["scope1", "scope2"]
end
test "issue a token for client_credentials grant type" do
app = insert(:oauth_app, scopes: ["read", "write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "client_credentials",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write"
end
test "rejects token exchange with invalid client credentials" do
user = insert(:user)
app = insert(:oauth_app)
{:ok, auth} = Authorization.create_authorization(app, user)
conn =
build_conn()
|> put_req_header("authorization", "Basic JTIxOiVGMCU5RiVBNCVCNwo=")
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => auth.token,
"redirect_uri" => OAuthController.default_redirect_uri(app)
})
assert resp = json_response(conn, 400)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
Pleroma.Config.put([:instance, :account_activation_required], true)
password = "testpassword"
{:ok, user} =
- insert(:user, password_hash: Pleroma.Password.hash_pwd_salt(password))
+ insert(:user, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
|> User.confirmation_changeset(need_confirmation: true)
|> User.update_and_set_cache()
refute Pleroma.User.account_status(user) == :active
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 403)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects token exchange for valid credentials belonging to deactivated user" do
password = "testpassword"
user =
insert(:user,
- password_hash: Pleroma.Password.hash_pwd_salt(password),
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
deactivated: true
)
app = insert(:oauth_app)
resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(403)
assert resp == %{
"error" => "Your account is currently disabled",
"identifier" => "account_is_disabled"
}
end
test "rejects token exchange for user with password_reset_pending set to true" do
password = "testpassword"
user =
insert(:user,
- password_hash: Pleroma.Password.hash_pwd_salt(password),
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
password_reset_pending: true
)
app = insert(:oauth_app, scopes: ["read", "write"])
resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(403)
assert resp == %{
"error" => "Password reset is required",
"identifier" => "password_reset_required"
}
end
test "rejects token exchange for user with confirmation_pending set to true" do
Pleroma.Config.put([:instance, :account_activation_required], true)
password = "testpassword"
user =
insert(:user,
- password_hash: Pleroma.Password.hash_pwd_salt(password),
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
confirmation_pending: true
)
app = insert(:oauth_app, scopes: ["read", "write"])
resp =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(403)
assert resp == %{
"error" => "Your login is missing a confirmed e-mail address",
"identifier" => "missing_confirmed_email"
}
end
test "rejects token exchange for valid credentials belonging to an unapproved user" do
password = "testpassword"
user =
insert(:user,
- password_hash: Pleroma.Password.hash_pwd_salt(password),
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password),
approval_pending: true
)
refute Pleroma.User.account_status(user) == :active
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 403)
assert %{"error" => _} = resp
refute Map.has_key?(resp, "access_token")
end
test "rejects an invalid authorization code" do
app = insert(:oauth_app)
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "authorization_code",
"code" => "Imobviouslyinvalid",
"redirect_uri" => OAuthController.default_redirect_uri(app),
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 400)
assert %{"error" => _} = json_response(conn, 400)
refute Map.has_key?(resp, "access_token")
end
end
describe "POST /oauth/token - refresh token" do
setup do: clear_config([:oauth2, :issue_new_refresh_token])
test "issues a new access token with keep fresh token" do
Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true)
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
new_token = Repo.get_by(Token, token: response["access_token"])
assert new_token.refresh_token == token.refresh_token
assert new_token.scopes == auth.scopes
assert new_token.user_id == user.id
assert new_token.app_id == app.id
end
test "issues a new access token with new fresh token" do
Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false)
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
new_token = Repo.get_by(Token, token: response["access_token"])
refute new_token.refresh_token == token.refresh_token
assert new_token.scopes == auth.scopes
assert new_token.user_id == user.id
assert new_token.app_id == app.id
end
test "returns 400 if we try use access token" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => token.token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert %{"error" => "Invalid credentials"} == response
end
test "returns 400 if refresh_token invalid" do
app = insert(:oauth_app, scopes: ["read", "write"])
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => "token.refresh_token",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(400)
assert %{"error" => "Invalid credentials"} == response
end
test "issues a new token if token expired" do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
{:ok, auth} = Authorization.create_authorization(app, user, ["write"])
{:ok, token} = Token.exchange_token(app, auth)
change =
Ecto.Changeset.change(
token,
%{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)}
)
{:ok, access_token} = Repo.update(change)
response =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "refresh_token",
"refresh_token" => access_token.refresh_token,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
|> json_response(200)
ap_id = user.ap_id
assert match?(
%{
"scope" => "write",
"token_type" => "Bearer",
"access_token" => _,
"refresh_token" => _,
"me" => ^ap_id
},
response
)
refute Repo.get_by(Token, token: token.token)
token = Repo.get_by(Token, token: response["access_token"])
assert token
assert token.scopes == auth.scopes
assert token.user_id == user.id
assert token.app_id == app.id
end
end
describe "POST /oauth/token - bad request" do
test "returns 500" do
response =
build_conn()
|> post("/oauth/token", %{})
|> json_response(500)
assert %{"error" => "Bad request"} == response
end
end
describe "POST /oauth/revoke" do
test "when authenticated with request token, revokes it and clears it from session" do
oauth_token = insert(:oauth_token)
conn =
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
|> AuthHelper.put_session_token(oauth_token.token)
|> post("/oauth/revoke", %{"token" => oauth_token.token})
assert json_response(conn, 200)
refute AuthHelper.get_session_token(conn)
assert Token.get_by_token(oauth_token.token) == {:error, :not_found}
end
test "if request is authenticated with a different token, " <>
"revokes requested token but keeps session token" do
user = insert(:user)
oauth_token = insert(:oauth_token, user: user)
other_app_oauth_token = insert(:oauth_token, user: user)
conn =
build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session()
|> AuthHelper.put_session_token(oauth_token.token)
|> post("/oauth/revoke", %{"token" => other_app_oauth_token.token})
assert json_response(conn, 200)
assert AuthHelper.get_session_token(conn) == oauth_token.token
assert Token.get_by_token(other_app_oauth_token.token) == {:error, :not_found}
end
test "returns 500 on bad request" do
response =
build_conn()
|> post("/oauth/revoke", %{})
|> json_response(500)
assert %{"error" => "Bad request"} == response
end
end
end
diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs
index 4a0ff6710..118ab302a 100644
--- a/test/pleroma/web/plugs/authentication_plug_test.exs
+++ b/test/pleroma/web/plugs/authentication_plug_test.exs
@@ -1,128 +1,128 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.User
alias Pleroma.Web.Plugs.AuthenticationPlug
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.PlugHelper
import ExUnit.CaptureLog
import Pleroma.Factory
setup %{conn: conn} do
user = %User{
id: 1,
name: "dude",
- password_hash: Pleroma.Password.hash_pwd_salt("guy")
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("guy")
}
conn =
conn
|> assign(:auth_user, user)
%{user: user, conn: conn}
end
test "it does nothing if a user is assigned", %{conn: conn} do
conn =
conn
|> assign(:user, %User{})
ret_conn =
conn
|> AuthenticationPlug.call(%{})
assert ret_conn == conn
end
test "with a correct password in the credentials, " <>
"it assigns the auth_user and marks OAuthScopesPlug as skipped",
%{conn: conn} do
conn =
conn
|> assign(:auth_credentials, %{password: "guy"})
|> AuthenticationPlug.call(%{})
assert conn.assigns.user == conn.assigns.auth_user
assert conn.assigns.token == nil
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
end
test "with a bcrypt hash, it updates to a pkbdf2 hash", %{conn: conn} do
user = insert(:user, password_hash: Bcrypt.hash_pwd_salt("123"))
assert "$2" <> _ = user.password_hash
conn =
conn
|> assign(:auth_user, user)
|> assign(:auth_credentials, %{password: "123"})
|> AuthenticationPlug.call(%{})
assert conn.assigns.user.id == conn.assigns.auth_user.id
assert conn.assigns.token == nil
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
user = User.get_by_id(user.id)
assert "$pbkdf2" <> _ = user.password_hash
end
@tag :skip_on_mac
test "with a crypt hash, it updates to a pkbdf2 hash", %{conn: conn} do
user =
insert(:user,
password_hash:
"$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
)
conn =
conn
|> assign(:auth_user, user)
|> assign(:auth_credentials, %{password: "password"})
|> AuthenticationPlug.call(%{})
assert conn.assigns.user.id == conn.assigns.auth_user.id
assert conn.assigns.token == nil
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
user = User.get_by_id(user.id)
assert "$pbkdf2" <> _ = user.password_hash
end
describe "checkpw/2" do
test "check pbkdf2 hash" do
hash =
"$pbkdf2-sha512$160000$loXqbp8GYls43F0i6lEfIw$AY.Ep.2pGe57j2hAPY635sI/6w7l9Q9u9Bp02PkPmF3OrClDtJAI8bCiivPr53OKMF7ph6iHhN68Rom5nEfC2A"
assert AuthenticationPlug.checkpw("test-password", hash)
refute AuthenticationPlug.checkpw("test-password1", hash)
end
@tag :skip_on_mac
test "check sha512-crypt hash" do
hash =
"$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
assert AuthenticationPlug.checkpw("password", hash)
end
test "check bcrypt hash" do
hash = "$2a$10$uyhC/R/zoE1ndwwCtMusK.TLVzkQ/Ugsbqp3uXI.CTTz0gBw.24jS"
assert AuthenticationPlug.checkpw("password", hash)
refute AuthenticationPlug.checkpw("password1", hash)
end
test "it returns false when hash invalid" do
hash =
"psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1"
assert capture_log(fn ->
refute AuthenticationPlug.checkpw("password", hash)
end) =~ "[error] Password hash not recognized"
end
end
end
diff --git a/test/pleroma/web/twitter_api/password_controller_test.exs b/test/pleroma/web/twitter_api/password_controller_test.exs
index 880f097cb..cf99e2434 100644
--- a/test/pleroma/web/twitter_api/password_controller_test.exs
+++ b/test/pleroma/web/twitter_api/password_controller_test.exs
@@ -1,119 +1,119 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.PasswordResetToken
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
describe "GET /api/pleroma/password_reset/token" do
test "it returns error when token invalid", %{conn: conn} do
response =
conn
|> get("/api/pleroma/password_reset/token")
|> html_response(:ok)
assert response =~ "<h2>Invalid Token</h2>"
end
test "it shows password reset form", %{conn: conn} do
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
response =
conn
|> get("/api/pleroma/password_reset/#{token.token}")
|> html_response(:ok)
assert response =~ "<h2>Password Reset for #{user.nickname}</h2>"
end
test "it returns an error when the token has expired", %{conn: conn} do
clear_config([:instance, :password_reset_token_validity], 0)
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, token} = time_travel(token, -2)
response =
conn
|> get("/api/pleroma/password_reset/#{token.token}")
|> html_response(:ok)
assert response =~ "<h2>Invalid Token</h2>"
end
end
describe "POST /api/pleroma/password_reset" do
test "it fails for an expired token", %{conn: conn} do
clear_config([:instance, :password_reset_token_validity], 0)
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, token} = time_travel(token, -2)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
response =
conn
|> assign(:user, user)
|> post("/api/pleroma/password_reset", %{data: params})
|> html_response(:ok)
refute response =~ "<h2>Password changed!</h2>"
end
test "it returns HTTP 200", %{conn: conn} do
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
response =
conn
|> assign(:user, user)
|> post("/api/pleroma/password_reset", %{data: params})
|> html_response(:ok)
assert response =~ "<h2>Password changed!</h2>"
user = refresh_record(user)
- assert Pleroma.Password.verify_pass("test", user.password_hash)
+ assert Pleroma.Password.Pbkdf2.verify_pass("test", user.password_hash)
assert Enum.empty?(Token.get_user_tokens(user))
end
test "it sets password_reset_pending to false", %{conn: conn} do
user = insert(:user, password_reset_pending: true)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
conn
|> assign(:user, user)
|> post("/api/pleroma/password_reset", %{data: params})
|> html_response(:ok)
assert User.get_by_id(user.id).password_reset_pending == false
end
end
end
diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs
index 923be8fae..6d007ab66 100644
--- a/test/pleroma/web/twitter_api/util_controller_test.exs
+++ b/test/pleroma/web/twitter_api/util_controller_test.exs
@@ -1,437 +1,437 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Config
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
import Pleroma.Factory
import Mock
setup do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance])
setup do: clear_config([:frontend_configurations, :pleroma_fe])
describe "PUT /api/pleroma/notification_settings" do
setup do: oauth_access(["write:accounts"])
test "it updates notification settings", %{user: user, conn: conn} do
conn
|> put("/api/pleroma/notification_settings", %{
"block_from_strangers" => true,
"bar" => 1
})
|> json_response(:ok)
user = refresh_record(user)
assert %Pleroma.User.NotificationSetting{
block_from_strangers: true,
hide_notification_contents: false
} == user.notification_settings
end
test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do
conn
|> put("/api/pleroma/notification_settings", %{"hide_notification_contents" => "1"})
|> json_response(:ok)
user = refresh_record(user)
assert %Pleroma.User.NotificationSetting{
block_from_strangers: false,
hide_notification_contents: true
} == user.notification_settings
end
end
describe "GET /api/pleroma/frontend_configurations" do
test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do
config = [
frontend_a: %{
x: 1,
y: 2
},
frontend_b: %{
z: 3
}
]
Config.put(:frontend_configurations, config)
response =
conn
|> get("/api/pleroma/frontend_configurations")
|> json_response(:ok)
assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!()
end
end
describe "/api/pleroma/emoji" do
test "returns json with custom emoji with tags", %{conn: conn} do
emoji =
conn
|> get("/api/pleroma/emoji")
|> json_response(200)
assert Enum.all?(emoji, fn
{_key,
%{
"image_url" => url,
"tags" => tags
}} ->
is_binary(url) and is_list(tags)
end)
end
end
describe "GET /api/pleroma/healthcheck" do
setup do: clear_config([:instance, :healthcheck])
test "returns 503 when healthcheck disabled", %{conn: conn} do
Config.put([:instance, :healthcheck], false)
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response(503)
assert response == %{}
end
test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do
Config.put([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response(200)
assert %{
"active" => _,
"healthy" => true,
"idle" => _,
"memory_used" => _,
"pool_size" => _
} = response
end
end
test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do
Config.put([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response(503)
assert %{
"active" => _,
"healthy" => false,
"idle" => _,
"memory_used" => _,
"pool_size" => _
} = response
end
end
end
describe "POST /api/pleroma/disable_account" do
setup do: oauth_access(["write:accounts"])
test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do
response =
conn
|> post("/api/pleroma/disable_account", %{"password" => "test"})
|> json_response(:ok)
assert response == %{"status" => "success"}
ObanHelpers.perform_all()
user = User.get_cached_by_id(user.id)
assert user.deactivated == true
end
test "with valid permissions and invalid password, it returns an error", %{conn: conn} do
user = insert(:user)
response =
conn
|> post("/api/pleroma/disable_account", %{"password" => "test1"})
|> json_response(:ok)
assert response == %{"error" => "Invalid password."}
user = User.get_cached_by_id(user.id)
refute user.deactivated
end
end
describe "POST /main/ostatus - remote_subscribe/2" do
setup do: clear_config([:instance, :federating], true)
test "renders subscribe form", %{conn: conn} do
user = insert(:user)
response =
conn
|> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""})
|> response(:ok)
refute response =~ "Could not find user"
assert response =~ "Remotely follow #{user.nickname}"
end
test "renders subscribe form with error when user not found", %{conn: conn} do
response =
conn
|> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""})
|> response(:ok)
assert response =~ "Could not find user"
refute response =~ "Remotely follow"
end
test "it redirect to webfinger url", %{conn: conn} do
user = insert(:user)
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
conn =
conn
|> post("/main/ostatus", %{
"user" => %{"nickname" => user.nickname, "profile" => user2.ap_id}
})
assert redirected_to(conn) ==
"https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}"
end
test "it renders form with error when user not found", %{conn: conn} do
user2 = insert(:user, ap_id: "shp@social.heldscal.la")
response =
conn
|> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}})
|> response(:ok)
assert response =~ "Something went wrong."
end
end
test "it returns new captcha", %{conn: conn} do
with_mock Pleroma.Captcha,
new: fn -> "test_captcha" end do
resp =
conn
|> get("/api/pleroma/captcha")
|> response(200)
assert resp == "\"test_captcha\""
assert called(Pleroma.Captcha.new())
end
end
describe "POST /api/pleroma/change_email" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> post("/api/pleroma/change_email")
assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."}
end
test "with proper permissions and invalid password", %{conn: conn} do
conn =
post(conn, "/api/pleroma/change_email", %{
"password" => "hi",
"email" => "test@test.com"
})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with proper permissions, valid password and invalid email", %{
conn: conn
} do
conn =
post(conn, "/api/pleroma/change_email", %{
"password" => "test",
"email" => "foobar"
})
assert json_response(conn, 200) == %{"error" => "Email has invalid format."}
end
test "with proper permissions, valid password and no email", %{
conn: conn
} do
conn =
post(conn, "/api/pleroma/change_email", %{
"password" => "test"
})
assert json_response(conn, 200) == %{"error" => "Email can't be blank."}
end
test "with proper permissions, valid password and blank email", %{
conn: conn
} do
conn =
post(conn, "/api/pleroma/change_email", %{
"password" => "test",
"email" => ""
})
assert json_response(conn, 200) == %{"error" => "Email can't be blank."}
end
test "with proper permissions, valid password and non unique email", %{
conn: conn
} do
user = insert(:user)
conn =
post(conn, "/api/pleroma/change_email", %{
"password" => "test",
"email" => user.email
})
assert json_response(conn, 200) == %{"error" => "Email has already been taken."}
end
test "with proper permissions, valid password and valid email", %{
conn: conn
} do
conn =
post(conn, "/api/pleroma/change_email", %{
"password" => "test",
"email" => "cofe@foobar.com"
})
assert json_response(conn, 200) == %{"status" => "success"}
end
end
describe "POST /api/pleroma/change_password" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> post("/api/pleroma/change_password")
assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."}
end
test "with proper permissions and invalid password", %{conn: conn} do
conn =
post(conn, "/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with proper permissions, valid password and new password and confirmation not matching",
%{
conn: conn
} do
conn =
post(conn, "/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "notnewpass"
})
assert json_response(conn, 200) == %{
"error" => "New password does not match confirmation."
}
end
test "with proper permissions, valid password and invalid new password", %{
conn: conn
} do
conn =
post(conn, "/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "",
"new_password_confirmation" => ""
})
assert json_response(conn, 200) == %{
"error" => "New password can't be blank."
}
end
test "with proper permissions, valid password and matching new password and confirmation", %{
conn: conn,
user: user
} do
conn =
post(conn, "/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"status" => "success"}
fetched_user = User.get_cached_by_id(user.id)
- assert Pleroma.Password.verify_pass("newpass", fetched_user.password_hash) == true
+ assert Pleroma.Password.Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true
end
end
describe "POST /api/pleroma/delete_account" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> post("/api/pleroma/delete_account")
assert json_response(conn, 403) ==
%{"error" => "Insufficient permissions: write:accounts."}
end
test "with proper permissions and wrong or missing password", %{conn: conn} do
for params <- [%{"password" => "hi"}, %{}] do
ret_conn = post(conn, "/api/pleroma/delete_account", params)
assert json_response(ret_conn, 200) == %{"error" => "Invalid password."}
end
end
test "with proper permissions and valid password", %{conn: conn, user: user} do
conn = post(conn, "/api/pleroma/delete_account", %{"password" => "test"})
ObanHelpers.perform_all()
assert json_response(conn, 200) == %{"status" => "success"}
user = User.get_by_id(user.id)
assert user.deactivated == true
assert user.name == nil
assert user.bio == ""
assert user.password_hash == nil
end
end
end
diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex
index 27470498d..6bccbb35a 100644
--- a/test/support/builders/user_builder.ex
+++ b/test/support/builders/user_builder.ex
@@ -1,26 +1,26 @@
defmodule Pleroma.Builders.UserBuilder do
alias Pleroma.Repo
alias Pleroma.User
def build(data \\ %{}) do
user = %User{
email: "test@example.org",
name: "Test Name",
nickname: "testname",
- password_hash: Pleroma.Password.hash_pwd_salt("test"),
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"),
bio: "A tester.",
ap_id: "some id",
last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
notification_settings: %Pleroma.User.NotificationSetting{}
}
Map.merge(user, data)
end
def insert(data \\ %{}) do
{:ok, user} = Repo.insert(build(data))
User.invalidate_cache(user)
{:ok, user}
end
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 53b1dfd09..bf9592064 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -1,461 +1,461 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Factory do
use ExMachina.Ecto, repo: Pleroma.Repo
alias Pleroma.Object
alias Pleroma.User
def participation_factory do
conversation = insert(:conversation)
user = insert(:user)
%Pleroma.Conversation.Participation{
conversation: conversation,
user: user,
read: false
}
end
def conversation_factory do
%Pleroma.Conversation{
ap_id: sequence(:ap_id, &"https://some_conversation/#{&1}")
}
end
def user_factory(attrs \\ %{}) do
user = %User{
name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"),
- password_hash: Pleroma.Password.hash_pwd_salt("test"),
+ password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"),
is_discoverable: true,
last_digest_emailed_at: NaiveDateTime.utc_now(),
last_refreshed_at: NaiveDateTime.utc_now(),
notification_settings: %Pleroma.User.NotificationSetting{},
multi_factor_authentication_settings: %Pleroma.MFA.Settings{},
ap_enabled: true
}
urls =
if attrs[:local] == false do
base_domain = Enum.random(["domain1.com", "domain2.com", "domain3.com"])
ap_id = "https://#{base_domain}/users/#{user.nickname}"
%{
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following"
}
else
%{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user)
}
end
user
|> Map.put(:raw_bio, user.bio)
|> Map.merge(urls)
|> merge_attributes(attrs)
end
def user_relationship_factory(attrs \\ %{}) do
source = attrs[:source] || insert(:user)
target = attrs[:target] || insert(:user)
relationship_type = attrs[:relationship_type] || :block
%Pleroma.UserRelationship{
source_id: source.id,
target_id: target.id,
relationship_type: relationship_type
}
end
def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}")
user = attrs[:user] || insert(:user)
data = %{
"type" => "Note",
"content" => text,
"source" => text,
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"likes" => [],
"like_count" => 0,
"context" => "2hu",
"summary" => "2hu",
"tag" => ["2hu"],
"emoji" => %{
"2hu" => "corndog.png"
}
}
%Pleroma.Object{
data: merge_attributes(data, Map.get(attrs, :data, %{}))
}
end
def audio_factory(attrs \\ %{}) do
text = sequence(:text, &"lain radio episode #{&1}")
user = attrs[:user] || insert(:user)
data = %{
"type" => "Audio",
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"artist" => "lain",
"title" => text,
"album" => "lain radio",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"actor" => user.ap_id,
"length" => 180_000
}
%Pleroma.Object{
data: merge_attributes(data, Map.get(attrs, :data, %{}))
}
end
def listen_factory do
audio = insert(:audio)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Listen",
"actor" => audio.data["actor"],
"to" => audio.data["to"],
"object" => audio.data,
"published" => audio.data["published"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def direct_note_factory do
user2 = insert(:user)
%Pleroma.Object{data: data} = note_factory()
%Pleroma.Object{data: Map.merge(data, %{"to" => [user2.ap_id]})}
end
def article_factory do
note_factory()
|> Map.put("type", "Article")
end
def tombstone_factory do
data = %{
"type" => "Tombstone",
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"formerType" => "Note",
"deleted" => DateTime.utc_now() |> DateTime.to_iso8601()
}
%Pleroma.Object{
data: data
}
end
def direct_note_activity_factory do
dm = insert(:direct_note)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => dm.data["actor"],
"to" => dm.data["to"],
"object" => dm.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => dm.data["context"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def note_activity_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)
data_attrs = attrs[:data_attrs] || %{}
attrs = Map.drop(attrs, [:user, :note, :data_attrs])
data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => note.data["actor"],
"to" => note.data["to"],
"object" => note.data["id"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => note.data["context"]
}
|> Map.merge(data_attrs)
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
|> Map.merge(attrs)
end
def article_activity_factory do
article = insert(:article)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => article.data["actor"],
"to" => article.data["to"],
"object" => article.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => article.data["context"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def announce_activity_factory(attrs \\ %{}) do
note_activity = attrs[:note_activity] || insert(:note_activity)
user = attrs[:user] || insert(:user)
data = %{
"type" => "Announce",
"actor" => note_activity.actor,
"object" => note_activity.data["id"],
"to" => [user.follower_address, note_activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => note_activity.data["context"]
}
%Pleroma.Activity{
data: data,
actor: user.ap_id,
recipients: data["to"]
}
end
def like_activity_factory(attrs \\ %{}) do
note_activity = attrs[:note_activity] || insert(:note_activity)
object = Object.normalize(note_activity, fetch: false)
user = insert(:user)
data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => user.ap_id,
"type" => "Like",
"object" => object.data["id"],
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
|> Map.merge(attrs[:data_attrs] || %{})
%Pleroma.Activity{
data: data
}
end
def follow_activity_factory do
follower = insert(:user)
followed = insert(:user)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => follower.ap_id,
"type" => "Follow",
"object" => followed.ap_id,
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
%Pleroma.Activity{
data: data,
actor: follower.ap_id
}
end
def report_activity_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
activity = attrs[:activity] || insert(:note_activity)
state = attrs[:state] || "open"
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => user.ap_id,
"type" => "Flag",
"object" => [activity.actor, activity.data["id"]],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"to" => [],
"cc" => [activity.actor],
"context" => activity.data["context"],
"state" => state
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"] ++ data["cc"]
}
end
def oauth_app_factory do
%Pleroma.Web.OAuth.App{
client_name: sequence(:client_name, &"Some client #{&1}"),
redirect_uris: "https://example.com/callback",
scopes: ["read", "write", "follow", "push", "admin"],
website: "https://example.com",
client_id: Ecto.UUID.generate(),
client_secret: "aaa;/&bbb"
}
end
def instance_factory do
%Pleroma.Instances.Instance{
host: "domain.com",
unreachable_since: nil
}
end
def oauth_token_factory(attrs \\ %{}) do
scopes = Map.get(attrs, :scopes, ["read"])
oauth_app = Map.get_lazy(attrs, :app, fn -> insert(:oauth_app, scopes: scopes) end)
user = Map.get_lazy(attrs, :user, fn -> build(:user) end)
valid_until =
Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10))
%Pleroma.Web.OAuth.Token{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
scopes: scopes,
user: user,
app: oauth_app,
valid_until: valid_until
}
end
def oauth_admin_token_factory(attrs \\ %{}) do
user = Map.get_lazy(attrs, :user, fn -> build(:user, is_admin: true) end)
scopes =
attrs
|> Map.get(:scopes, ["admin"])
|> Kernel.++(["admin"])
|> Enum.uniq()
attrs = Map.merge(attrs, %{user: user, scopes: scopes})
oauth_token_factory(attrs)
end
def oauth_authorization_factory do
%Pleroma.Web.OAuth.Authorization{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
scopes: ["read", "write", "follow", "push"],
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10),
user: build(:user),
app: build(:oauth_app)
}
end
def push_subscription_factory do
%Pleroma.Web.Push.Subscription{
user: build(:user),
token: build(:oauth_token),
endpoint: "https://example.com/example/1234",
key_auth: "8eDyX_uCN0XRhSbY5hs7Hg==",
key_p256dh:
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=",
data: %{}
}
end
def notification_factory do
%Pleroma.Notification{
user: build(:user)
}
end
def scheduled_activity_factory do
%Pleroma.ScheduledActivity{
user: build(:user),
scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond),
params: build(:note) |> Map.from_struct() |> Map.get(:data)
}
end
def registration_factory do
user = insert(:user)
%Pleroma.Registration{
user: user,
provider: "twitter",
uid: "171799000",
info: %{
"name" => "John Doe",
"email" => "john@doe.com",
"nickname" => "johndoe",
"description" => "My bio"
}
}
end
def config_factory(attrs \\ %{}) do
%Pleroma.ConfigDB{
key: sequence(:key, &String.to_atom("some_key_#{&1}")),
group: :pleroma,
value:
sequence(
:value,
&%{another_key: "#{&1}somevalue", another: "#{&1}somevalue"}
)
}
|> merge_attributes(attrs)
end
def marker_factory do
%Pleroma.Marker{
user: build(:user),
timeline: "notifications",
lock_version: 0,
last_read_id: "1"
}
end
def mfa_token_factory do
%Pleroma.MFA.Token{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
authorization: build(:oauth_authorization),
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10),
user: build(:user)
}
end
def filter_factory do
%Pleroma.Filter{
user: build(:user),
filter_id: sequence(:filter_id, & &1),
phrase: "cofe"
}
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 4:15 AM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768838
Default Alt Text
(194 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment