Merge branch 'multitenancy' into fork
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
53a73aada2
43 changed files with 1263 additions and 49 deletions
1
changelog.d/multitenancy.add
Normal file
1
changelog.d/multitenancy.add
Normal file
|
@ -0,0 +1 @@
|
|||
Allow using multiple domains for WebFinger
|
|
@ -252,7 +252,10 @@
|
|||
birthday_required: false,
|
||||
birthday_min_age: 0,
|
||||
max_media_attachments: 1_000,
|
||||
migration_cooldown_period: 30
|
||||
migration_cooldown_period: 30,
|
||||
multitenancy: %{
|
||||
enabled: false
|
||||
}
|
||||
|
||||
config :pleroma, :welcome,
|
||||
direct_message: [
|
||||
|
@ -587,10 +590,12 @@
|
|||
attachments_cleanup: 1,
|
||||
new_users_digest: 1,
|
||||
mute_expire: 5,
|
||||
search_indexing: 10
|
||||
search_indexing: 10,
|
||||
check_domain_resolve: 1
|
||||
],
|
||||
plugins: [Oban.Plugins.Pruner],
|
||||
crontab: [
|
||||
{"0 0 * * 0", Pleroma.Workers.Cron.CheckDomainsResolveWorker},
|
||||
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
|
||||
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
|
||||
]
|
||||
|
|
|
@ -1096,6 +1096,23 @@
|
|||
suggestions: [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
%{
|
||||
key: :multitenancy,
|
||||
type: :map,
|
||||
description: "Multitenancy support",
|
||||
children: [
|
||||
%{
|
||||
key: :enabled,
|
||||
type: :boolean,
|
||||
description: "Enables allowing multiple Webfinger domains"
|
||||
},
|
||||
%{
|
||||
key: :separate_timelines,
|
||||
type: :boolean,
|
||||
description: "Only display posts from own domain on local timeline"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# How to serve multiple domains for Pleroma user identifiers
|
||||
|
||||
It is possible to use multiple domains for WebFinger identifiers. If configured, users can select from the available domains during registration. Domains can be set by instance administrator and can be marked as either public (everyone can choose it) or private (only available when admin creates a user)
|
||||
|
||||
## Configuring
|
||||
|
||||
### Configuring Pleroma
|
||||
|
||||
To enable using multiple domains, append the following to your `prod.secret.exs` or `dev.secret.exs`:
|
||||
```elixir
|
||||
config :pleroma, :instance, :multitenancy, enabled: true
|
||||
```
|
||||
|
||||
Creating, updating and deleting domains is available from the admin API.
|
||||
|
||||
### Configuring WebFinger domains
|
||||
|
||||
If you recall how webfinger queries work, the first step is to query `https://example.org/.well-known/host-meta`, which will contain an URL template.
|
||||
|
||||
Therefore, the easiest way to configure the additional domains is to redirect `/.well-known/host-meta` to the domain used by Pleroma.
|
||||
|
||||
With nginx, it would be as simple as adding:
|
||||
|
||||
```nginx
|
||||
location = /.well-known/host-meta {
|
||||
return 301 https://pleroma.example.org$request_uri;
|
||||
}
|
||||
```
|
||||
|
||||
in the additional domain's server block.
|
|
@ -1840,3 +1840,70 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns
|
|||
- Params:
|
||||
- `id`: **string** Webhook ID
|
||||
- Response: A webhook
|
||||
|
||||
## `GET /api/v1/pleroma/admin/domains`
|
||||
|
||||
### List of domains
|
||||
|
||||
- Response: JSON, list of domains
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"domain": "example.org",
|
||||
"public": false,
|
||||
"resolves": true,
|
||||
"last_checked_at": "2023-11-17T12:13:05"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## `POST /api/v1/pleroma/admin/domains`
|
||||
|
||||
### Create a domain
|
||||
|
||||
- Params:
|
||||
- `domain`: string, required, domain name
|
||||
- `public`: boolean, optional, defaults to false, whether it is possible to register an account under the domain by everyone
|
||||
|
||||
- Response: JSON, created announcement
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"domain": "example.org",
|
||||
"public": true,
|
||||
"resolves": false,
|
||||
"last_checked_at": null
|
||||
}
|
||||
```
|
||||
|
||||
## `POST /api/v1/pleroma/admin/domains/:id`
|
||||
|
||||
### Change domain publicity
|
||||
|
||||
- Params:
|
||||
- `public`: boolean, whether it is possible to register an account under the domain by everyone
|
||||
|
||||
- Response: JSON, updated domain
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"domain": "example.org",
|
||||
"public": false,
|
||||
"resolves": true,
|
||||
"last_checked_at": "2023-11-17T12:13:05"
|
||||
}
|
||||
```
|
||||
|
||||
## `DELETE /api/v1/pleroma/admin/domains/:id`
|
||||
|
||||
### Delete a domain
|
||||
|
||||
- Response: JSON, empty object
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
|
|
@ -352,6 +352,7 @@ Has these additional parameters (which are the same as in Pleroma-API):
|
|||
- `captcha_answer_data`: optional, contains provider-specific captcha data
|
||||
- `token`: invite token required when the registrations aren't public.
|
||||
- `language`: optional, user's preferred language for receiving emails (digest, confirmation, etc.), default to the language set in the `userLanguage` cookies or `Accept-Language` header.
|
||||
- `domain`: optional, domain id, if multitenancy is enabled.
|
||||
|
||||
## Instance
|
||||
|
||||
|
|
|
@ -188,7 +188,8 @@ defp cachex_children do
|
|||
build_cachex("anti_duplication_mrf", limit: 5_000),
|
||||
build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000),
|
||||
build_cachex("rel_me", default_ttl: :timer.minutes(30), limit: 2_500),
|
||||
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000)
|
||||
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5000),
|
||||
build_cachex("domain", limit: 2500)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
90
lib/pleroma/domain.ex
Normal file
90
lib/pleroma/domain.ex
Normal file
|
@ -0,0 +1,90 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
defmodule Pleroma.Domain do
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Pleroma.Repo
|
||||
|
||||
schema "domains" do
|
||||
field(:domain, :string, default: "")
|
||||
field(:service_domain, :string, default: "")
|
||||
field(:public, :boolean, default: false)
|
||||
field(:resolves, :boolean, default: false)
|
||||
field(:last_checked_at, :naive_datetime)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
def changeset(%__MODULE__{} = domain, params \\ %{}) do
|
||||
domain
|
||||
|> cast(params, [:domain, :public])
|
||||
|> validate_required([:domain])
|
||||
|> maybe_add_service_domain()
|
||||
|> update_change(:domain, &String.downcase/1)
|
||||
|> unique_constraint(:domain)
|
||||
|> unique_constraint(:service_domain)
|
||||
end
|
||||
|
||||
def update_changeset(%__MODULE__{} = domain, params \\ %{}) do
|
||||
domain
|
||||
|> cast(params, [:public])
|
||||
end
|
||||
|
||||
def update_state_changeset(%__MODULE__{} = domain, resolves) do
|
||||
domain
|
||||
|> cast(
|
||||
%{
|
||||
resolves: resolves,
|
||||
last_checked_at: NaiveDateTime.utc_now()
|
||||
},
|
||||
[:resolves, :last_checked_at]
|
||||
)
|
||||
end
|
||||
|
||||
defp maybe_add_service_domain(%{changes: %{service_domain: _}} = changeset), do: changeset
|
||||
|
||||
defp maybe_add_service_domain(%{changes: %{domain: domain}} = changeset) do
|
||||
change(changeset, service_domain: domain)
|
||||
end
|
||||
|
||||
def list do
|
||||
__MODULE__
|
||||
|> order_by(asc: :id)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def get(id), do: Repo.get(__MODULE__, id)
|
||||
|
||||
def get_by_service_domain(domain) do
|
||||
__MODULE__
|
||||
|> where(service_domain: ^domain)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def create(params) do
|
||||
%__MODULE__{}
|
||||
|> changeset(params)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def update(params, id) do
|
||||
get(id)
|
||||
|> update_changeset(params)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def delete(id) do
|
||||
get(id)
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
def cached_list do
|
||||
@cachex.fetch!(:domain_cache, "domains_list", fn _ -> list() end)
|
||||
end
|
||||
end
|
|
@ -15,6 +15,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Delivery
|
||||
alias Pleroma.Domain
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.FollowingRelationship
|
||||
|
@ -162,6 +163,8 @@ defmodule Pleroma.User do
|
|||
field(:language, :string)
|
||||
field(:last_move_at, :naive_datetime)
|
||||
|
||||
belongs_to(:domain, Domain)
|
||||
|
||||
embeds_one(
|
||||
:notification_settings,
|
||||
Pleroma.User.NotificationSetting,
|
||||
|
@ -813,16 +816,18 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
|||
:accepts_chat_messages,
|
||||
:registration_reason,
|
||||
:birthday,
|
||||
:language
|
||||
:language,
|
||||
:domain_id
|
||||
])
|
||||
|> validate_required([:name, :nickname, :password, :password_confirmation])
|
||||
|> validate_confirmation(:password)
|
||||
|> unique_constraint(:email)
|
||||
|> validate_format(:email, @email_regex)
|
||||
|> validate_email_not_in_blacklisted_domain(:email)
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_not_restricted_nickname(:nickname)
|
||||
|> validate_format(:nickname, local_nickname_regex())
|
||||
|> fix_nickname(Map.get(params, :domain_id), opts[:from_admin])
|
||||
|> validate_not_restricted_nickname(:nickname)
|
||||
|> unique_constraint(:nickname)
|
||||
|> validate_length(:bio, max: bio_limit)
|
||||
|> validate_length(:name, min: 1, max: name_limit)
|
||||
|> validate_length(:registration_reason, max: reason_limit)
|
||||
|
@ -836,6 +841,26 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
|
|||
|> put_private_key()
|
||||
end
|
||||
|
||||
defp fix_nickname(changeset, domain_id, from_admin) when not is_nil(domain_id) do
|
||||
with {:domain, domain} <- {:domain, Domain.get(domain_id)},
|
||||
{:domain_allowed, true} <- {:domain_allowed, from_admin || domain.public} do
|
||||
nickname = get_field(changeset, :nickname)
|
||||
|
||||
changeset
|
||||
|> put_change(:nickname, nickname <> "@" <> domain.domain)
|
||||
|> put_change(:domain, domain)
|
||||
else
|
||||
{:domain_allowed, false} ->
|
||||
changeset
|
||||
|> add_error(:domain, "not allowed to use this domain")
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp fix_nickname(changeset, _, _), do: changeset
|
||||
|
||||
def validate_not_restricted_nickname(changeset, field) do
|
||||
validate_change(changeset, field, fn _, value ->
|
||||
valid? =
|
||||
|
@ -896,7 +921,9 @@ defp validate_min_age(changeset) do
|
|||
end
|
||||
|
||||
defp put_ap_id(changeset) do
|
||||
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
|
||||
nickname = get_field(changeset, :nickname)
|
||||
ap_id = ap_id(%User{nickname: nickname})
|
||||
|
||||
put_change(changeset, :ap_id, ap_id)
|
||||
end
|
||||
|
||||
|
@ -1309,6 +1336,13 @@ def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
|
|||
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
|
||||
get_cached_by_nickname(nickname_or_id)
|
||||
|
||||
String.contains?(nickname_or_id, "@") ->
|
||||
with %User{local: true} = user <- get_cached_by_nickname(nickname_or_id) do
|
||||
user
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -952,6 +952,24 @@ defp restrict_local(query, %{local_only: true}) do
|
|||
|
||||
defp restrict_local(query, _), do: query
|
||||
|
||||
defp restrict_domain(query, %{domain_id: 0}) do
|
||||
query
|
||||
|> join(:inner, [activity], u in User,
|
||||
as: :domain_user,
|
||||
on: activity.actor == u.ap_id and is_nil(u.domain_id)
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_domain(query, %{domain_id: domain_id}) do
|
||||
query
|
||||
|> join(:inner, [activity], u in User,
|
||||
as: :domain_user,
|
||||
on: activity.actor == u.ap_id and u.domain_id == ^domain_id
|
||||
)
|
||||
end
|
||||
|
||||
defp restrict_domain(query, _), do: query
|
||||
|
||||
defp restrict_remote(query, %{remote: true}) do
|
||||
from(activity in query, where: activity.local == false)
|
||||
end
|
||||
|
@ -1468,6 +1486,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> restrict_replies(opts)
|
||||
|> restrict_since(opts)
|
||||
|> restrict_local(opts)
|
||||
|> restrict_domain(opts)
|
||||
|> restrict_remote(opts)
|
||||
|> restrict_actor(opts)
|
||||
|> restrict_type(opts)
|
||||
|
|
|
@ -52,6 +52,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|||
when action in [:activity, :object]
|
||||
)
|
||||
|
||||
plug(
|
||||
Pleroma.Web.Plugs.SetDomainPlug
|
||||
when action in [:following, :followers, :pinned, :inbox, :outbox, :update_outbox]
|
||||
)
|
||||
|
||||
plug(
|
||||
Pleroma.Web.Plugs.SetNicknameWithDomainPlug
|
||||
when action in [:following, :followers, :pinned, :inbox, :outbox, :update_outbox]
|
||||
)
|
||||
|
||||
plug(:set_requester_reachable when action in [:inbox])
|
||||
plug(:relay_active? when action in [:relay])
|
||||
|
||||
|
@ -525,7 +535,7 @@ def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} =
|
|||
with {:ok, object} <-
|
||||
ActivityPub.upload(
|
||||
file,
|
||||
actor: User.ap_id(user),
|
||||
actor: user.ap_id,
|
||||
description: Map.get(data, "description")
|
||||
) do
|
||||
Logger.debug(inspect(object))
|
||||
|
|
78
lib/pleroma/web/admin_api/controllers/domain_controller.ex
Normal file
78
lib/pleroma/web/admin_api/controllers/domain_controller.ex
Normal file
|
@ -0,0 +1,78 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.DomainController do
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Domain
|
||||
alias Pleroma.Web.AdminAPI
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
import Pleroma.Web.ControllerHelper,
|
||||
only: [
|
||||
json_response: 3
|
||||
]
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["admin:write"]}
|
||||
when action in [:create, :update, :delete]
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
|
||||
|
||||
action_fallback(AdminAPI.FallbackController)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.DomainOperation
|
||||
|
||||
def index(conn, _) do
|
||||
domains = Domain.list()
|
||||
|
||||
render(conn, "index.json", domains: domains)
|
||||
end
|
||||
|
||||
def create(%{body_params: params} = conn, _) do
|
||||
with {:domain_not_used, true} <-
|
||||
{:domain_not_used, params[:domain] !== Pleroma.Web.WebFinger.host()},
|
||||
{:ok, domain} <- Domain.create(params),
|
||||
_ <- @cachex.del(:domain, :domains_list) do
|
||||
Pleroma.Workers.CheckDomainResolveWorker.enqueue("check_domain_resolve", %{
|
||||
"id" => domain.id
|
||||
})
|
||||
|
||||
render(conn, "show.json", domain: domain)
|
||||
else
|
||||
{:domain_not_used, false} ->
|
||||
{:error, :invalid_domain}
|
||||
|
||||
{:error, changeset} ->
|
||||
errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
|
||||
|
||||
{:errors, errors}
|
||||
end
|
||||
end
|
||||
|
||||
def update(%{body_params: params} = conn, %{id: id}) do
|
||||
{:ok, domain} =
|
||||
params
|
||||
|> Domain.update(id)
|
||||
|
||||
@cachex.del(:domain, :domains_list)
|
||||
|
||||
render(conn, "show.json", domain: domain)
|
||||
end
|
||||
|
||||
def delete(conn, %{id: id}) do
|
||||
with {:ok, _} <- Domain.delete(id),
|
||||
_ <- @cachex.del(:domain, :domains_list) do
|
||||
json(conn, %{})
|
||||
else
|
||||
_ -> json_response(conn, :bad_request, "")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
|
|||
import Pleroma.Web.ControllerHelper,
|
||||
only: [fetch_integer_param: 3]
|
||||
|
||||
alias Pleroma.Domain
|
||||
alias Pleroma.ModerationLog
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
|
@ -165,17 +166,25 @@ def create(
|
|||
) do
|
||||
changesets =
|
||||
users
|
||||
|> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
|
||||
|> Enum.map(fn %{nickname: nickname, email: email, password: password} = user ->
|
||||
domain_id = Map.get(user, :domain)
|
||||
|
||||
domain =
|
||||
if domain_id do
|
||||
Domain.get(domain_id)
|
||||
end
|
||||
|
||||
user_data = %{
|
||||
nickname: nickname,
|
||||
name: nickname,
|
||||
email: email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
bio: "."
|
||||
bio: ".",
|
||||
domain: domain
|
||||
}
|
||||
|
||||
User.register_changeset(%User{}, user_data, need_confirmation: false)
|
||||
User.register_changeset(%User{}, user_data, need_confirmation: false, from_admin: true)
|
||||
end)
|
||||
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
|
||||
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
|
||||
|
|
35
lib/pleroma/web/admin_api/views/domain_view.ex
Normal file
35
lib/pleroma/web/admin_api/views/domain_view.ex
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.DomainView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Domain
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
def render("index.json", %{domains: domains} = assigns) do
|
||||
render_many(domains, __MODULE__, "show.json", assigns |> Map.delete("domains"))
|
||||
end
|
||||
|
||||
def render("show.json", %{domain: %Domain{} = domain} = assigns) do
|
||||
%{
|
||||
id: domain.id |> to_string(),
|
||||
domain: domain.domain,
|
||||
public: domain.public
|
||||
}
|
||||
|> maybe_put_resolve_information(domain, assigns)
|
||||
end
|
||||
|
||||
defp maybe_put_resolve_information(map, _domain, %{admin: false}) do
|
||||
map
|
||||
end
|
||||
|
||||
defp maybe_put_resolve_information(map, domain, _assigns) do
|
||||
map
|
||||
|> Map.merge(%{
|
||||
resolves: domain.resolves,
|
||||
last_checked_at: Utils.to_masto_date(domain.last_checked_at)
|
||||
})
|
||||
end
|
||||
end
|
|
@ -106,14 +106,8 @@ def spec(opts \\ []) do
|
|||
"User administration",
|
||||
"Announcement management",
|
||||
"Instance rule managment",
|
||||
"Webhooks"
|
||||
]
|
||||
},
|
||||
%{
|
||||
"name" => "Administration (MastoAPI)",
|
||||
"tags" => [
|
||||
"User administration",
|
||||
"Report methods"
|
||||
"Webhooks",
|
||||
"Domain managment"
|
||||
]
|
||||
},
|
||||
%{
|
||||
|
|
|
@ -639,7 +639,8 @@ defp create_request do
|
|||
type: :string,
|
||||
nullable: true,
|
||||
description: "User's preferred language for emails"
|
||||
}
|
||||
},
|
||||
domain: %Schema{type: :string, nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"username" => "cofe",
|
||||
|
|
115
lib/pleroma/web/api_spec/operations/admin/domain_operation.ex
Normal file
115
lib/pleroma/web/api_spec/operations/admin/domain_operation.ex
Normal file
|
@ -0,0 +1,115 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Admin.DomainOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Domain managment"],
|
||||
summary: "Retrieve list of domains",
|
||||
operationId: "AdminAPI.DomainController.index",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Response", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: domain()
|
||||
}),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Domain managment"],
|
||||
summary: "Create new domain",
|
||||
operationId: "AdminAPI.DomainController.create",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: admin_api_params(),
|
||||
requestBody: request_body("Parameters", create_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Response", "application/json", domain()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Domain managment"],
|
||||
summary: "Modify existing domain",
|
||||
operationId: "AdminAPI.DomainController.update",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
parameters: [Operation.parameter(:id, :path, :string, "Domain ID")],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Response", "application/json", domain()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Domain managment"],
|
||||
summary: "Delete domain",
|
||||
operationId: "AdminAPI.DomainController.delete",
|
||||
parameters: [Operation.parameter(:id, :path, :string, "Domain ID")],
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
responses: %{
|
||||
200 => empty_object_response(),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:domain],
|
||||
properties: %{
|
||||
domain: %Schema{type: :string},
|
||||
service_domain: %Schema{type: :string, nullable: true},
|
||||
public: %Schema{type: :boolean, nullable: true}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
public: %Schema{type: :boolean, nullable: true}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp domain do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :integer},
|
||||
domain: %Schema{type: :string},
|
||||
service_domain: %Schema{type: :string},
|
||||
public: %Schema{type: :boolean},
|
||||
resolves: %Schema{type: :boolean},
|
||||
last_checked_at: %Schema{type: :string, format: "date-time", nullable: true}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -82,7 +82,8 @@ def create_operation do
|
|||
properties: %{
|
||||
nickname: %Schema{type: :string},
|
||||
email: %Schema{type: :string},
|
||||
password: %Schema{type: :string}
|
||||
password: %Schema{type: :string},
|
||||
domain: %Schema{type: :string, nullable: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ def reopen_operation do
|
|||
|
||||
def assign_to_self_operation do
|
||||
%Operation{
|
||||
tags: ["Report methods"],
|
||||
tags: ["Report management (Mastodon API)"],
|
||||
summary: "Assign report to self",
|
||||
operationId: "MastodonAdmin.ReportController.assign_to_self",
|
||||
description: "Claim the handling of this report to yourself.",
|
||||
|
@ -123,7 +123,7 @@ def assign_to_self_operation do
|
|||
|
||||
def unassign_operation do
|
||||
%Operation{
|
||||
tags: ["Report methods"],
|
||||
tags: ["Report management (Mastodon API)"],
|
||||
summary: "Unassign report",
|
||||
operationId: "MastodonAdmin.ReportController.unassign",
|
||||
description: "Unassign a report so that someone else can claim it.",
|
||||
|
|
|
@ -116,7 +116,10 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(Phoenix.CodeReloader)
|
||||
end
|
||||
|
||||
plug(Pleroma.Web.Plugs.TrailingFormatPlug)
|
||||
plug(Pleroma.Web.Plugs.TrailingFormatPlug,
|
||||
supported_formats: ["html", "xml", "rss", "atom", "activity+json", "json"]
|
||||
)
|
||||
|
||||
plug(Plug.RequestId)
|
||||
plug(Plug.Logger, log: :debug)
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ defmodule Pleroma.Web.Feed.UserController do
|
|||
alias Pleroma.Web.Feed.FeedView
|
||||
|
||||
plug(Pleroma.Web.Plugs.SetFormatPlug when action in [:feed_redirect])
|
||||
plug(Pleroma.Web.Plugs.SetDomainPlug when action in [:feed_redirect, :feed])
|
||||
plug(Pleroma.Web.Plugs.SetNicknameWithDomainPlug when action in [:feed_redirect, :feed])
|
||||
|
||||
action_fallback(:errors)
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
|
|||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||
|
||||
|
@ -28,7 +27,7 @@ def create(
|
|||
with {:ok, object} <-
|
||||
ActivityPub.upload(
|
||||
file,
|
||||
actor: User.ap_id(user),
|
||||
actor: user.ap_id,
|
||||
description: Map.get(data, :description)
|
||||
) do
|
||||
attachment_data = Map.put(object.data, "id", object.id)
|
||||
|
@ -48,7 +47,7 @@ def create2(
|
|||
with {:ok, object} <-
|
||||
ActivityPub.upload(
|
||||
file,
|
||||
actor: User.ap_id(user),
|
||||
actor: user.ap_id,
|
||||
description: Map.get(data, :description)
|
||||
) do
|
||||
attachment_data = Map.put(object.data, "id", object.id)
|
||||
|
|
|
@ -36,6 +36,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
|||
when action in [:public, :hashtag]
|
||||
)
|
||||
|
||||
plug(Pleroma.Web.Plugs.SetDomainPlug when action in [:public, :hashtag])
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
|
||||
|
||||
# GET /api/v1/timelines/home
|
||||
|
@ -114,6 +116,7 @@ def public(%{assigns: %{user: user}} = conn, params) do
|
|||
|> Map.put(:instance, params[:instance])
|
||||
# Restricts unfederated content to authenticated users
|
||||
|> Map.put(:includes_local_public, not is_nil(user))
|
||||
|> maybe_put_domain_id(conn)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
|
||||
conn
|
||||
|
@ -131,7 +134,7 @@ defp fail_on_bad_auth(conn) do
|
|||
render_error(conn, :unauthorized, "authorization required for timeline view")
|
||||
end
|
||||
|
||||
defp hashtag_fetching(params, user, local_only) do
|
||||
defp hashtag_fetching(conn, params, user, local_only) do
|
||||
# Note: not sanitizing tag options at this stage (may be mix-cased, have duplicates etc.)
|
||||
tags_any =
|
||||
[params[:tag], params[:any]]
|
||||
|
@ -150,6 +153,7 @@ defp hashtag_fetching(params, user, local_only) do
|
|||
|> Map.put(:tag, tags_any)
|
||||
|> Map.put(:tag_all, tag_all)
|
||||
|> Map.put(:tag_reject, tag_reject)
|
||||
|> maybe_put_domain_id(conn)
|
||||
|> ActivityPub.fetch_public_activities()
|
||||
end
|
||||
|
||||
|
@ -160,7 +164,7 @@ def hashtag(%{assigns: %{user: user}} = conn, params) do
|
|||
if is_nil(user) and restrict_unauthenticated?(local_only) do
|
||||
fail_on_bad_auth(conn)
|
||||
else
|
||||
activities = hashtag_fetching(params, user, local_only)
|
||||
activities = hashtag_fetching(conn, params, user, local_only)
|
||||
|
||||
conn
|
||||
|> add_link_headers(activities, %{"local" => local_only})
|
||||
|
@ -183,6 +187,7 @@ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do
|
|||
|> Map.put(:user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:local_only, params[:local])
|
||||
|> maybe_put_domain_id(conn)
|
||||
|
||||
# we must filter the following list for the user to avoid leaking statuses the user
|
||||
# does not actually have permission to see (for more info, peruse security issue #270).
|
||||
|
@ -207,4 +212,19 @@ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do
|
|||
_e -> render_error(conn, :forbidden, "Error.")
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_domain_id(%{local_only: true} = params, conn) do
|
||||
separate_timelines = Config.get([:instance, :multitenancy, :separate_timelines])
|
||||
|
||||
if separate_timelines do
|
||||
domain = Map.get(conn, :domain, %{id: 0})
|
||||
|
||||
params
|
||||
|> Map.put(:domain_id, domain.id)
|
||||
else
|
||||
params
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_put_domain_id(params, _conn), do: params
|
||||
end
|
||||
|
|
|
@ -323,7 +323,8 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
background_image: image_url(user.background) |> MediaProxy.url(),
|
||||
accepts_chat_messages: user.accepts_chat_messages,
|
||||
favicon: favicon,
|
||||
location: user.location
|
||||
location: user.location,
|
||||
is_local: user.local
|
||||
}
|
||||
}
|
||||
|> maybe_put_role(user, opts[:for])
|
||||
|
|
|
@ -6,8 +6,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Domain
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
alias Pleroma.Web.AdminAPI.DomainView
|
||||
alias Pleroma.Web.MastodonAPI
|
||||
|
||||
@mastodon_api_level "2.7.2"
|
||||
|
@ -175,7 +177,8 @@ def features do
|
|||
if Pleroma.Language.Translation.configured?() do
|
||||
"translation"
|
||||
end,
|
||||
"events"
|
||||
"events",
|
||||
"multitenancy"
|
||||
]
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
@ -310,7 +313,8 @@ defp pleroma_configuration(instance) do
|
|||
migration_cooldown_period: Config.get([:instance, :migration_cooldown_period]),
|
||||
restrict_unauthenticated: restrict_unauthenticated(),
|
||||
translation: translation_configuration(),
|
||||
markup: markup()
|
||||
markup: markup(),
|
||||
multitenancy: multitenancy()
|
||||
},
|
||||
stats: %{mau: Pleroma.User.active_user_count()},
|
||||
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key),
|
||||
|
@ -374,4 +378,21 @@ defp markup do
|
|||
allow_tables: Config.get([:markup, :allow_tables])
|
||||
}
|
||||
end
|
||||
|
||||
defp multitenancy do
|
||||
enabled = Config.get([:instance, :multitenancy, :enabled])
|
||||
|
||||
if enabled do
|
||||
domains =
|
||||
[%Domain{id: "", domain: Pleroma.Web.WebFinger.host(), public: true}] ++
|
||||
Domain.cached_list()
|
||||
|
||||
%{
|
||||
enabled: true,
|
||||
domains: DomainView.render("index.json", domains: domains, admin: false)
|
||||
}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
28
lib/pleroma/web/plugs/set_domain_plug.ex
Normal file
28
lib/pleroma/web/plugs/set_domain_plug.ex
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.SetDomainPlug do
|
||||
use Pleroma.Web, :plug
|
||||
|
||||
alias Pleroma.Domain
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
@impl true
|
||||
def perform(%{host: domain} = conn, _opts) do
|
||||
with true <- Pleroma.Config.get([:instance, :multitenancy, :enabled], false),
|
||||
false <-
|
||||
domain in [
|
||||
Pleroma.Config.get([__MODULE__, :domain]),
|
||||
Pleroma.Web.Endpoint.host()
|
||||
],
|
||||
%Domain{} = domain <- Domain.get_by_service_domain(domain) do
|
||||
Map.put(conn, :domain, domain)
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
|
||||
def perform(conn, _), do: conn
|
||||
end
|
24
lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex
Normal file
24
lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.SetNicknameWithDomainPlug do
|
||||
use Pleroma.Web, :plug
|
||||
|
||||
def init(opts), do: opts
|
||||
|
||||
@impl true
|
||||
def perform(%{domain: domain, params: params} = conn, opts) do
|
||||
with key <- Keyword.get(opts, :key, "nickname"),
|
||||
nickname <- Map.get(params, key),
|
||||
false <- String.contains?(nickname, "@"),
|
||||
nickname <- nickname <> "@" <> domain.domain,
|
||||
params <- Map.put(params, "nickname", nickname) do
|
||||
Map.put(conn, :params, params)
|
||||
else
|
||||
_ -> conn
|
||||
end
|
||||
end
|
||||
|
||||
def perform(conn, _), do: conn
|
||||
end
|
|
@ -1,9 +1,12 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.TrailingFormatPlug do
|
||||
@moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers."
|
||||
@moduledoc """
|
||||
This plug is adapted from [`TrailingFormatPlug`](https://github.com/mschae/trailing_format_plug/blob/master/lib/trailing_format_plug.ex).
|
||||
Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers."
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
@paths [
|
||||
|
@ -29,14 +32,53 @@ defmodule Pleroma.Web.Plugs.TrailingFormatPlug do
|
|||
]
|
||||
|
||||
def init(opts) do
|
||||
TrailingFormatPlug.init(opts)
|
||||
opts
|
||||
end
|
||||
|
||||
for path <- @paths do
|
||||
def call(%{request_path: unquote(path) <> _} = conn, opts) do
|
||||
TrailingFormatPlug.call(conn, opts)
|
||||
path = conn.path_info |> List.last() |> String.split(".") |> Enum.reverse()
|
||||
|
||||
supported_formats = Keyword.get(opts, :supported_formats, nil)
|
||||
|
||||
case path do
|
||||
[_] ->
|
||||
conn
|
||||
|
||||
[format | fragments] ->
|
||||
if supported_formats == nil || format in supported_formats do
|
||||
new_path = fragments |> Enum.reverse() |> Enum.join(".")
|
||||
path_fragments = List.replace_at(conn.path_info, -1, new_path)
|
||||
|
||||
params =
|
||||
Plug.Conn.fetch_query_params(conn).params
|
||||
|> update_params(new_path, format)
|
||||
|> Map.put("_format", format)
|
||||
|
||||
%{
|
||||
conn
|
||||
| path_info: path_fragments,
|
||||
query_params: params,
|
||||
params: params
|
||||
}
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _opts), do: conn
|
||||
|
||||
defp update_params(params, new_path, format) do
|
||||
wildcard = Enum.find(params, fn {_, v} -> v == "#{new_path}.#{format}" end)
|
||||
|
||||
case wildcard do
|
||||
{key, _} ->
|
||||
Map.put(params, key, new_path)
|
||||
|
||||
_ ->
|
||||
params
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -306,6 +306,11 @@ defmodule Pleroma.Web.Router do
|
|||
post("/webhooks/:id/enable", WebhookController, :enable)
|
||||
post("/webhooks/:id/disable", WebhookController, :disable)
|
||||
post("/webhooks/:id/rotate_secret", WebhookController, :rotate_secret)
|
||||
|
||||
get("/domains", DomainController, :index)
|
||||
post("/domains", DomainController, :create)
|
||||
patch("/domains/:id", DomainController, :update)
|
||||
delete("/domains/:id", DomainController, :delete)
|
||||
end
|
||||
|
||||
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
|
||||
|
|
|
@ -27,6 +27,7 @@ def register_user(params, opts \\ []) do
|
|||
:language,
|
||||
Pleroma.Web.Gettext.normalize_locale(params[:language]) || fallback_language
|
||||
)
|
||||
|> maybe_put_domain_id(params[:domain])
|
||||
|
||||
if Pleroma.Config.get([:instance, :registrations_open]) do
|
||||
create_user(params, opts)
|
||||
|
@ -64,6 +65,14 @@ defp create_user(params, opts) do
|
|||
end
|
||||
end
|
||||
|
||||
defp maybe_put_domain_id(params, domain) do
|
||||
if Pleroma.Config.get([:instance, :multitenancy, :enabled]) do
|
||||
Map.put(params, :domain_id, domain)
|
||||
else
|
||||
params
|
||||
end
|
||||
end
|
||||
|
||||
def password_reset(nickname_or_email) do
|
||||
with true <- is_binary(nickname_or_email),
|
||||
%User{local: true, email: email, is_active: true} = user when is_binary(email) <-
|
||||
|
|
|
@ -33,15 +33,13 @@ def host_meta do
|
|||
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
|
||||
host = Pleroma.Web.Endpoint.host()
|
||||
|
||||
regex =
|
||||
if webfinger_domain = Pleroma.Config.get([__MODULE__, :domain]) do
|
||||
~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})/
|
||||
else
|
||||
~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/
|
||||
end
|
||||
regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@(?<domain>[a-z0-9A-Z_\.-]+)/
|
||||
webfinger_domain = Pleroma.Config.get([__MODULE__, :domain])
|
||||
|
||||
with %{"username" => username} <- Regex.named_captures(regex, resource),
|
||||
%User{} = user <- User.get_cached_by_nickname(username) do
|
||||
with %{"username" => username, "domain" => domain} <- Regex.named_captures(regex, resource),
|
||||
nickname <-
|
||||
if(domain in [host, webfinger_domain], do: username, else: username <> "@" <> domain),
|
||||
%User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||
{:ok, represent_user(user, fmt)}
|
||||
else
|
||||
_e ->
|
||||
|
@ -70,7 +68,7 @@ defp gather_aliases(%User{} = user) do
|
|||
|
||||
def represent_user(user, "JSON") do
|
||||
%{
|
||||
"subject" => "acct:#{user.nickname}@#{host()}",
|
||||
"subject" => get_subject(user),
|
||||
"aliases" => gather_aliases(user),
|
||||
"links" => gather_links(user)
|
||||
}
|
||||
|
@ -90,12 +88,20 @@ def represent_user(user, "XML") do
|
|||
:XRD,
|
||||
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
|
||||
[
|
||||
{:Subject, "acct:#{user.nickname}@#{host()}"}
|
||||
{:Subject, get_subject(user)}
|
||||
] ++ aliases ++ links
|
||||
}
|
||||
|> XmlBuilder.to_doc()
|
||||
end
|
||||
|
||||
defp get_subject(%User{nickname: nickname}) do
|
||||
if String.contains?(nickname, "@") do
|
||||
"acct:#{nickname}"
|
||||
else
|
||||
"acct:#{nickname}@#{host()}"
|
||||
end
|
||||
end
|
||||
|
||||
def host do
|
||||
Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
|
||||
end
|
||||
|
|
36
lib/pleroma/workers/check_domain_resolve_worker.ex
Normal file
36
lib/pleroma/workers/check_domain_resolve_worker.ex
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.CheckDomainResolveWorker do
|
||||
use Pleroma.Workers.WorkerHelper, queue: "check_domain_resolve"
|
||||
|
||||
alias Pleroma.Domain
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.WebFinger
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{args: %{"id" => domain_id}}) do
|
||||
domain = Domain.get(domain_id)
|
||||
|
||||
resolves =
|
||||
with {:ok, %Tesla.Env{status: status, body: hostmeta_body}} when status in 200..299 <-
|
||||
HTTP.get("https://" <> domain.domain <> "/.well-known/host-meta"),
|
||||
{:ok, template} <- WebFinger.get_template_from_xml(hostmeta_body),
|
||||
base_url <- Endpoint.url(),
|
||||
true <- template == "#{base_url}/.well-known/webfinger?resource={uri}" do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
|
||||
domain
|
||||
|> Domain.update_state_changeset(resolves)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@impl Oban.Worker
|
||||
def timeout(_job), do: :timer.seconds(5)
|
||||
end
|
34
lib/pleroma/workers/cron/check_domains_resolve_worker.ex
Normal file
34
lib/pleroma/workers/cron/check_domains_resolve_worker.ex
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.Cron.CheckDomainsResolveWorker do
|
||||
@moduledoc """
|
||||
The worker to check if alternative domains resolve correctly.
|
||||
"""
|
||||
|
||||
use Oban.Worker, queue: "check_domain_resolve"
|
||||
|
||||
alias Pleroma.Domain
|
||||
alias Pleroma.Repo
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
require Logger
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(_job) do
|
||||
domains =
|
||||
Domain
|
||||
|> select([d], d.id)
|
||||
|> Repo.all()
|
||||
|
||||
Enum.each(domains, fn domain_id ->
|
||||
Pleroma.Workers.CheckDomainResolveWorker.enqueue("check_domain_resolve", %{
|
||||
"id" => domain_id
|
||||
})
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
1
mix.exs
1
mix.exs
|
@ -134,7 +134,6 @@ defp deps do
|
|||
{:oban, "~> 2.13.4"},
|
||||
{:gettext, "~> 0.20"},
|
||||
{:bcrypt_elixir, "~> 2.2"},
|
||||
{:trailing_format_plug, "~> 0.0.7"},
|
||||
{:fast_sanitize, "~> 0.2.0"},
|
||||
{:html_entities, "~> 0.5", override: true},
|
||||
{:calendar, "~> 1.0"},
|
||||
|
|
24
priv/repo/migrations/20230618190919_create_domains.exs
Normal file
24
priv/repo/migrations/20230618190919_create_domains.exs
Normal file
|
@ -0,0 +1,24 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateDomains do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_if_not_exists table(:domains) do
|
||||
add(:domain, :citext)
|
||||
add(:service_domain, :citext)
|
||||
add(:public, :boolean)
|
||||
add(:resolves, :boolean)
|
||||
add(:last_checked_at, :naive_datetime)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
create_if_not_exists(unique_index(:domains, [:domain]))
|
||||
create_if_not_exists(unique_index(:domains, [:service_domain]))
|
||||
|
||||
alter table(:users) do
|
||||
add(:domain_id, references(:domains))
|
||||
end
|
||||
|
||||
create_if_not_exists(index(:users, [:domain_id]))
|
||||
end
|
||||
end
|
|
@ -851,6 +851,55 @@ test "it fails when provided invalid birth date" do
|
|||
end
|
||||
end
|
||||
|
||||
describe "user registration, with custom domain" do
|
||||
@full_user_data %{
|
||||
bio: "A guy",
|
||||
name: "my name",
|
||||
nickname: "nick",
|
||||
password: "test",
|
||||
password_confirmation: "test",
|
||||
email: "email@example.com"
|
||||
}
|
||||
|
||||
setup do
|
||||
clear_config([:instance, :multitenancy], %{enabled: true})
|
||||
end
|
||||
|
||||
test "it registers on a given domain" do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Pleroma.Domain.create(%{
|
||||
domain: "pleroma.example.org",
|
||||
public: true
|
||||
})
|
||||
|
||||
params =
|
||||
@full_user_data
|
||||
|> Map.put(:domain_id, to_string(domain_id))
|
||||
|
||||
changeset = User.register_changeset(%User{}, params)
|
||||
assert changeset.valid?
|
||||
|
||||
{:ok, user} = Repo.insert(changeset)
|
||||
|
||||
assert user.nickname == "nick@pleroma.example.org"
|
||||
end
|
||||
|
||||
test "it fails when domain is private" do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Pleroma.Domain.create(%{
|
||||
domain: "private.example.org",
|
||||
public: false
|
||||
})
|
||||
|
||||
params =
|
||||
@full_user_data
|
||||
|> Map.put(:domain_id, to_string(domain_id))
|
||||
|
||||
changeset = User.register_changeset(%User{}, params)
|
||||
refute changeset.valid?
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_or_fetch/1" do
|
||||
test "gets an existing user by nickname" do
|
||||
user = insert(:user)
|
||||
|
|
|
@ -138,6 +138,24 @@ test "it returns a json representation of the user with accept application/ld+js
|
|||
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
|
||||
end
|
||||
|
||||
test "it returns a json representation of a local user domain different from host", %{
|
||||
conn: conn
|
||||
} do
|
||||
user =
|
||||
insert(:user, %{
|
||||
nickname: "nick@example.org"
|
||||
})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> get("/users/#{user.nickname}.json")
|
||||
|
||||
user = User.get_cached_by_id(user.id)
|
||||
|
||||
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
|
||||
end
|
||||
|
||||
test "it returns 404 for remote users", %{
|
||||
conn: conn
|
||||
} do
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.DomainControllerTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.Domain
|
||||
|
||||
setup do
|
||||
clear_config([Pleroma.Web.WebFinger, :domain], "example.com")
|
||||
|
||||
admin = insert(:user, is_admin: true)
|
||||
token = insert(:oauth_admin_token, user: admin)
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> assign(:user, admin)
|
||||
|> assign(:token, token)
|
||||
|
||||
{:ok, %{admin: admin, token: token, conn: conn}}
|
||||
end
|
||||
|
||||
describe "GET /api/pleroma/admin/domains" do
|
||||
test "list created domains", %{conn: conn} do
|
||||
_domain =
|
||||
Domain.create(%{
|
||||
domain: "pleroma.mkljczk.pl",
|
||||
public: true
|
||||
})
|
||||
|
||||
_domain =
|
||||
Domain.create(%{
|
||||
domain: "pleroma2.mkljczk.pl"
|
||||
})
|
||||
|
||||
conn = get(conn, "/api/pleroma/admin/domains")
|
||||
|
||||
[
|
||||
%{
|
||||
"id" => _id,
|
||||
"domain" => "pleroma.mkljczk.pl",
|
||||
"public" => true
|
||||
},
|
||||
%{
|
||||
"id" => _id2,
|
||||
"domain" => "pleroma2.mkljczk.pl",
|
||||
"public" => false
|
||||
}
|
||||
] = json_response_and_validate_schema(conn, 200)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/pleroma/admin/domains" do
|
||||
test "create a valid domain", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/domains", %{
|
||||
domain: "pleroma.mkljczk.pl",
|
||||
public: true
|
||||
})
|
||||
|
||||
%{
|
||||
"id" => _id,
|
||||
"domain" => "pleroma.mkljczk.pl",
|
||||
"public" => true
|
||||
} = json_response_and_validate_schema(conn, 200)
|
||||
end
|
||||
|
||||
test "create a domain the same as host", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/domains", %{
|
||||
domain: "example.com",
|
||||
public: false
|
||||
})
|
||||
|
||||
%{"error" => "invalid_domain"} = json_response_and_validate_schema(conn, 400)
|
||||
end
|
||||
|
||||
test "create duplicate domains", %{conn: conn} do
|
||||
Domain.create(%{
|
||||
domain: "pleroma.mkljczk.pl",
|
||||
public: true
|
||||
})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/domains", %{
|
||||
domain: "pleroma.mkljczk.pl",
|
||||
public: false
|
||||
})
|
||||
|
||||
assert json_response_and_validate_schema(conn, 400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "PATCH /api/pleroma/admin/domains/:id" do
|
||||
test "update domain privacy", %{conn: conn} do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Domain.create(%{
|
||||
domain: "pleroma.mkljczk.pl",
|
||||
public: true
|
||||
})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> patch("/api/pleroma/admin/domains/#{domain_id}", %{
|
||||
public: false
|
||||
})
|
||||
|
||||
%{
|
||||
"id" => _id,
|
||||
"domain" => "pleroma.mkljczk.pl",
|
||||
"public" => false
|
||||
} = json_response_and_validate_schema(conn, 200)
|
||||
end
|
||||
|
||||
test "doesn't update domain name", %{conn: conn} do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Domain.create(%{
|
||||
domain: "plemora.mkljczk.pl",
|
||||
public: true
|
||||
})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> patch("/api/pleroma/admin/domains/#{domain_id}", %{
|
||||
domain: "pleroma.mkljczk.pl"
|
||||
})
|
||||
|
||||
%{
|
||||
"id" => _id,
|
||||
"domain" => "plemora.mkljczk.pl",
|
||||
"public" => true
|
||||
} = json_response_and_validate_schema(conn, 200)
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/pleroma/admin/domains/:id" do
|
||||
test "delete a domain", %{conn: conn} do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Domain.create(%{
|
||||
domain: "pleroma.mkljczk.pl",
|
||||
public: true
|
||||
})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> delete("/api/pleroma/admin/domains/#{domain_id}")
|
||||
|
||||
%{} = json_response_and_validate_schema(conn, 200)
|
||||
|
||||
domains = Domain.list()
|
||||
|
||||
assert length(domains) == 0
|
||||
end
|
||||
end
|
||||
end
|
|
@ -216,4 +216,53 @@ test "restrict_unauthenticated", %{conn: conn} do
|
|||
assert result["pleroma"]["metadata"]["restrict_unauthenticated"]["timelines"]["local"] ==
|
||||
false
|
||||
end
|
||||
|
||||
test "instance domains", %{conn: conn} do
|
||||
clear_config([:instance, :multitenancy], %{enabled: true})
|
||||
|
||||
{:ok, %{id: domain_id}} =
|
||||
Pleroma.Domain.create(%{
|
||||
domain: "pleroma.example.org"
|
||||
})
|
||||
|
||||
domain_id = to_string(domain_id)
|
||||
|
||||
assert %{
|
||||
"pleroma" => %{
|
||||
"metadata" => %{
|
||||
"multitenancy" => %{
|
||||
"enabled" => true,
|
||||
"domains" => [
|
||||
%{
|
||||
"id" => "",
|
||||
"domain" => _,
|
||||
"public" => true
|
||||
},
|
||||
%{
|
||||
"id" => ^domain_id,
|
||||
"domain" => "pleroma.example.org",
|
||||
"public" => false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
} =
|
||||
conn
|
||||
|> get("/api/v1/instance")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
clear_config([:instance, :multitenancy, :enabled], false)
|
||||
|
||||
assert %{
|
||||
"pleroma" => %{
|
||||
"metadata" => %{
|
||||
"multitenancy" => nil
|
||||
}
|
||||
}
|
||||
} =
|
||||
conn
|
||||
|> get("/api/v1/instance")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -408,6 +408,43 @@ test "should not return local-only posts for anonymous users" do
|
|||
|
||||
assert [] = result
|
||||
end
|
||||
|
||||
test "filtering local posts basing on domain", %{conn: conn} do
|
||||
clear_config([:instance, :multitenancy], %{enabled: true, separate_timelines: false})
|
||||
|
||||
{:ok, domain} = Pleroma.Domain.create(%{domain: "pleroma.example.org"})
|
||||
|
||||
user1 = insert(:user)
|
||||
user2 = insert(:user, %{domain_id: domain.id})
|
||||
|
||||
%{id: note1} = insert(:note_activity, user: user1)
|
||||
%{id: note2} = insert(:note_activity, user: user2)
|
||||
|
||||
assert [
|
||||
%{"id" => ^note2},
|
||||
%{"id" => ^note1}
|
||||
] =
|
||||
conn
|
||||
|> get("/api/v1/timelines/public?local=true")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
clear_config([:instance, :multitenancy, :separate_timelines], true)
|
||||
|
||||
assert [%{"id" => ^note1}] =
|
||||
conn
|
||||
|> get("/api/v1/timelines/public?local=true")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [%{"id" => ^note1}] =
|
||||
conn
|
||||
|> get("/api/v1/timelines/public?local=true")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [%{"id" => ^note2}] =
|
||||
conn
|
||||
|> get("http://pleroma.example.org/api/v1/timelines/public?local=true")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
end
|
||||
|
||||
defp local_and_remote_activities do
|
||||
|
|
|
@ -96,7 +96,8 @@ test "Represent a user account" do
|
|||
relationship: %{},
|
||||
skip_thread_containment: false,
|
||||
accepts_chat_messages: nil,
|
||||
location: nil
|
||||
location: nil,
|
||||
is_local: true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,7 +305,8 @@ test "Represent a Service(bot) account" do
|
|||
relationship: %{},
|
||||
skip_thread_containment: false,
|
||||
accepts_chat_messages: nil,
|
||||
location: nil
|
||||
location: nil,
|
||||
is_local: true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,20 @@ test "works for ap_ids" do
|
|||
{:ok, result} = WebFinger.webfinger(user.ap_id, "XML")
|
||||
assert is_binary(result)
|
||||
end
|
||||
|
||||
test "works for fqns with domains other than host" do
|
||||
user = insert(:user, %{nickname: "nick@example.org"})
|
||||
|
||||
{:ok, result} = WebFinger.webfinger("#{user.nickname})}", "XML")
|
||||
|
||||
assert is_binary(result)
|
||||
end
|
||||
|
||||
test "doesn't work for remote users" do
|
||||
user = insert(:user, %{local: false})
|
||||
|
||||
assert {:error, _} = WebFinger.webfinger("#{user.nickname})}", "XML")
|
||||
end
|
||||
end
|
||||
|
||||
describe "fingering" do
|
||||
|
|
118
test/pleroma/workers/check_domains_resolve_worker_test.exs
Normal file
118
test/pleroma/workers/check_domains_resolve_worker_test.exs
Normal file
|
@ -0,0 +1,118 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Workers.CheckDomainsResolveWorkerTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Domain
|
||||
alias Pleroma.Workers.CheckDomainResolveWorker
|
||||
|
||||
setup do
|
||||
Pleroma.Web.Endpoint.config_change(
|
||||
[{Pleroma.Web.Endpoint, url: [host: "pleroma.example.org", scheme: "https", port: 443]}],
|
||||
[]
|
||||
)
|
||||
|
||||
clear_config([Pleroma.Web.Endpoint, :url, :host], "pleroma.example.org")
|
||||
|
||||
Tesla.Mock.mock_global(fn
|
||||
%{url: "https://pleroma.example.org/.well-known/host-meta"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
"test/fixtures/webfinger/pleroma-host-meta.xml"
|
||||
|> File.read!()
|
||||
|> String.replace("{{domain}}", "pleroma.example.org")
|
||||
}
|
||||
|
||||
%{url: "https://example.org/.well-known/host-meta"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
"test/fixtures/webfinger/pleroma-host-meta.xml"
|
||||
|> File.read!()
|
||||
|> String.replace("{{domain}}", "pleroma.example.org")
|
||||
}
|
||||
|
||||
%{url: "https://social.example.org/.well-known/host-meta"} ->
|
||||
%Tesla.Env{
|
||||
status: 302,
|
||||
headers: [{"location", "https://pleroma.example.org/.well-known/host-meta"}]
|
||||
}
|
||||
|
||||
%{url: "https://notpleroma.example.org/.well-known/host-meta"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body:
|
||||
"test/fixtures/webfinger/pleroma-host-meta.xml"
|
||||
|> File.read!()
|
||||
|> String.replace("{{domain}}", "notpleroma.example.org")
|
||||
}
|
||||
|
||||
%{url: "https://wrong.example.org/.well-known/host-meta"} ->
|
||||
%Tesla.Env{
|
||||
status: 302,
|
||||
headers: [{"location", "https://notpleroma.example.org/.well-known/host-meta"}]
|
||||
}
|
||||
|
||||
%{url: "https://bad.example.org/.well-known/host-meta"} ->
|
||||
%Tesla.Env{status: 404}
|
||||
end)
|
||||
|
||||
on_exit(fn ->
|
||||
Pleroma.Web.Endpoint.config_change(
|
||||
[{Pleroma.Web.Endpoint, url: [host: "localhost"]}],
|
||||
[]
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
test "verifies domain state" do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Domain.create(%{
|
||||
domain: "example.org"
|
||||
})
|
||||
|
||||
{:ok, domain} = CheckDomainResolveWorker.perform(%Oban.Job{args: %{"id" => domain_id}})
|
||||
|
||||
assert domain.resolves == true
|
||||
assert domain.last_checked_at != nil
|
||||
end
|
||||
|
||||
test "verifies domain state for a redirect" do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Domain.create(%{
|
||||
domain: "social.example.org"
|
||||
})
|
||||
|
||||
{:ok, domain} = CheckDomainResolveWorker.perform(%Oban.Job{args: %{"id" => domain_id}})
|
||||
|
||||
assert domain.resolves == true
|
||||
assert domain.last_checked_at != nil
|
||||
end
|
||||
|
||||
test "doesn't verify state for an incorrect redirect" do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Domain.create(%{
|
||||
domain: "wrong.example.org"
|
||||
})
|
||||
|
||||
{:ok, domain} = CheckDomainResolveWorker.perform(%Oban.Job{args: %{"id" => domain_id}})
|
||||
|
||||
assert domain.resolves == false
|
||||
assert domain.last_checked_at != nil
|
||||
end
|
||||
|
||||
test "doesn't verify state for unimplemented redirect" do
|
||||
{:ok, %{id: domain_id}} =
|
||||
Domain.create(%{
|
||||
domain: "bad.example.org"
|
||||
})
|
||||
|
||||
{:ok, domain} = CheckDomainResolveWorker.perform(%Oban.Job{args: %{"id" => domain_id}})
|
||||
|
||||
assert domain.resolves == false
|
||||
assert domain.last_checked_at != nil
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue