From c2c7c23aabc7085808a4576dc4eddc5d148b5ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= Date: Tue, 7 Nov 2023 00:07:18 +0100 Subject: [PATCH 01/20] Allow using multiple domains for WebFinger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- lib/pleroma/domain.ex | 53 +++++++++ lib/pleroma/user.ex | 49 +++++++- .../activity_pub/activity_pub_controller.ex | 3 +- .../web/activity_pub/views/user_view.ex | 17 ++- .../controllers/domain_controller.ex | 62 ++++++++++ .../admin_api/controllers/user_controller.ex | 10 +- .../web/admin_api/views/domain_view.ex | 21 ++++ .../api_spec/operations/account_operation.ex | 3 +- .../operations/admin/domain_operation.ex | 111 ++++++++++++++++++ .../operations/admin/user_operation.ex | 3 +- .../web/mastodon_api/views/account_view.ex | 3 +- .../web/mastodon_api/views/instance_view.ex | 23 +++- lib/pleroma/web/router.ex | 5 + lib/pleroma/web/twitter_api/twitter_api.ex | 1 + lib/pleroma/web/web_finger.ex | 26 ++-- .../20230618190919_create_domains.exs | 18 +++ 16 files changed, 381 insertions(+), 27 deletions(-) create mode 100644 lib/pleroma/domain.ex create mode 100644 lib/pleroma/web/admin_api/controllers/domain_controller.ex create mode 100644 lib/pleroma/web/admin_api/views/domain_view.ex create mode 100644 lib/pleroma/web/api_spec/operations/admin/domain_operation.ex create mode 100644 priv/repo/migrations/20230618190919_create_domains.exs diff --git a/lib/pleroma/domain.ex b/lib/pleroma/domain.ex new file mode 100644 index 0000000000..d708188eed --- /dev/null +++ b/lib/pleroma/domain.ex @@ -0,0 +1,53 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Domain do + use Ecto.Schema + + import Ecto.Changeset + + alias Pleroma.Repo + + schema "domains" do + field(:domain, :string, default: "") + field(:public, :boolean, default: false) + + timestamps() + end + + def changeset(%__MODULE__{} = domain, params \\ %{}) do + domain + |> cast(params, [:domain, :public]) + |> validate_required([:domain]) + end + + def update_changeset(%__MODULE__{} = domain, params \\ %{}) do + domain + |> cast(params, [:domain]) + end + + def get(id), do: Repo.get(__MODULE__, id) + + def create(params) do + {:ok, domain} = + %__MODULE__{} + |> changeset(params) + |> Repo.insert() + + domain + end + + def update(params, id) do + {:ok, domain} = + get(id) + |> update_changeset(params) + |> Repo.update() + + domain + end + + def delete(id) do + get(id) + |> Repo.delete() + end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ce125d6081..bb180517e1 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -14,6 +14,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 @@ -157,6 +158,8 @@ defmodule Pleroma.User do field(:show_birthday, :boolean, default: false) field(:language, :string) + belongs_to(:domain, Domain) + embeds_one( :notification_settings, Pleroma.User.NotificationSetting, @@ -788,16 +791,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) @@ -811,6 +816,26 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do |> put_private_key() end + defp fix_nickname(changeset, domain_id, from_admin) when is_binary(domain_id) do + with {:domain, domain} <- {:domain, Pleroma.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? = @@ -871,7 +896,16 @@ 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}) + + ap_id = + if String.contains?(nickname, ".") do + ap_id <> ".json" + else + ap_id + end + put_change(changeset, :ap_id, ap_id) end @@ -1278,6 +1312,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 diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 1357c379c2..52668776ff 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -66,7 +66,8 @@ defp relay_active?(conn, _) do end def user(conn, %{"nickname" => nickname}) do - with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do + with %User{local: true} = user <- + nickname |> URI.decode() |> User.get_cached_by_nickname_or_id() do conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index f69fca0759..1afa11e546 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -95,14 +95,21 @@ def render("user.json", %{user: user}) do do: Date.to_iso8601(user.birthday), else: nil + ap_id = + if String.ends_with?(user.ap_id, ".json") do + String.slice(user.ap_id, 0..-6) + else + user.ap_id + end + %{ "id" => user.ap_id, "type" => user.actor_type, - "following" => "#{user.ap_id}/following", - "followers" => "#{user.ap_id}/followers", - "inbox" => "#{user.ap_id}/inbox", - "outbox" => "#{user.ap_id}/outbox", - "featured" => "#{user.ap_id}/collections/featured", + "following" => "#{ap_id}/following", + "followers" => "#{ap_id}/followers", + "inbox" => "#{ap_id}/inbox", + "outbox" => "#{ap_id}/outbox", + "featured" => "#{ap_id}/collections/featured", "preferredUsername" => user.nickname, "name" => user.name, "summary" => user.bio, diff --git a/lib/pleroma/web/admin_api/controllers/domain_controller.ex b/lib/pleroma/web/admin_api/controllers/domain_controller.ex new file mode 100644 index 0000000000..46b0394601 --- /dev/null +++ b/lib/pleroma/web/admin_api/controllers/domain_controller.ex @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.DomainController do + use Pleroma.Web, :controller + + alias Pleroma.Repo + alias Pleroma.Domain + 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 + |> Repo.all() + + render(conn, "index.json", domains: domains) + end + + def create(%{body_params: params} = conn, _) do + domain = + params + |> Domain.create() + + render(conn, "show.json", domain: domain) + end + + def update(%{body_params: params} = conn, %{id: id}) do + domain = + params + |> Domain.update(id) + + render(conn, "show.json", domain: domain) + end + + def delete(conn, %{id: id}) do + with {:ok, _} <- Domain.delete(id) do + json(conn, %{}) + else + _ -> json_response(conn, :bad_request, "") + end + end +end diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex index 7b4ee46a4e..e08831d5a4 100644 --- a/lib/pleroma/web/admin_api/controllers/user_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex @@ -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 @@ -127,17 +128,20 @@ def unfollow( def create(%{assigns: %{user: admin}, body_params: %{users: users}} = conn, _) do changesets = users - |> Enum.map(fn %{nickname: nickname, email: email, password: password} -> + |> Enum.map(fn %{nickname: nickname, email: email, password: password, domain: domain} -> + domain = Domain.get(domain) + 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) diff --git a/lib/pleroma/web/admin_api/views/domain_view.ex b/lib/pleroma/web/admin_api/views/domain_view.ex new file mode 100644 index 0000000000..0f91d44900 --- /dev/null +++ b/lib/pleroma/web/admin_api/views/domain_view.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.DomainView do + use Pleroma.Web, :view + + alias Pleroma.Domain + + def render("index.json", %{domains: domains}) do + render_many(domains, __MODULE__, "show.json") + end + + def render("show.json", %{domain: %Domain{id: id, domain: domain, public: public}}) do + %{ + id: id |> to_string(), + domain: domain, + public: public + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index f2897a3a36..3855d19889 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -585,7 +585,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", diff --git a/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex b/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex new file mode 100644 index 0000000000..d73c67b3d5 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex @@ -0,0 +1,111 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# 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}, + 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}, + public: %Schema{type: :boolean} + } + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex index a5179ac398..d2e76ff151 100644 --- a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex @@ -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} } } } diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index cc3e3582f6..c668ff1b70 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -297,7 +297,8 @@ defp do_render("show.json", %{user: user} = opts) do skip_thread_containment: user.skip_thread_containment, background_image: image_url(user.background) |> MediaProxy.url(), accepts_chat_messages: user.accepts_chat_messages, - favicon: favicon + favicon: favicon, + is_local: user.local } } |> maybe_put_role(user, opts[:for]) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 1b01d73711..4ab4ccb74c 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -6,7 +6,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do use Pleroma.Web, :view alias Pleroma.Config + alias Pleroma.Domain + alias Pleroma.Repo alias Pleroma.Web.ActivityPub.MRF + alias Pleroma.Web.AdminAPI.DomainView @mastodon_api_level "2.7.2" @@ -49,7 +52,8 @@ def render("show.json", _) do fields_limits: fields_limits(), post_formats: Config.get([:instance, :allowed_post_formats]), birthday_required: Config.get([:instance, :birthday_required]), - birthday_min_age: Config.get([:instance, :birthday_min_age]) + birthday_min_age: Config.get([:instance, :birthday_min_age]), + multitenancy: multitenancy() }, stats: %{mau: Pleroma.User.active_user_count()}, vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) @@ -141,4 +145,21 @@ def fields_limits do value_length: Config.get([:instance, :account_field_value_length]) } end + + defp multitenancy do + enabled = Config.get([:multitenancy, :enabled]) + + if enabled do + domains = + [%Domain{id: "", domain: Pleroma.Web.WebFinger.domain(), public: true}] ++ + Repo.all(Domain) + + %{ + enabled: true, + domains: DomainView.render("index.json", domains: domains) + } + else + nil + end + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6b9e158a3f..0dbdd2c901 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -286,6 +286,11 @@ defmodule Pleroma.Web.Router do post("/frontends/install", FrontendController, :install) post("/backups", AdminAPIController, :create_backup) + + 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) diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index ef2eb75f4c..485a105e64 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -27,6 +27,7 @@ def register_user(params, opts \\ []) do :language, Pleroma.Web.Gettext.normalize_locale(params[:language]) || fallback_language ) + |> Map.put(:domain_id, params[:domain]) if Pleroma.Config.get([:instance, :registrations_open]) do create_user(params, opts) diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index f95dc24584..49c17e2c32 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -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:)?(?[a-z0-9A-Z_\.-]+)@(#{host}|#{webfinger_domain})/ - else - ~r/(acct:)?(?[a-z0-9A-Z_\.-]+)@#{host}/ - end + regex = ~r/(acct:)?(?[a-z0-9A-Z_\.-]+)@(?[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}@#{domain()}", + "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}@#{domain()}"} + {: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}@#{domain()}" + end + end + defp domain do Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host() end diff --git a/priv/repo/migrations/20230618190919_create_domains.exs b/priv/repo/migrations/20230618190919_create_domains.exs new file mode 100644 index 0000000000..a306c80ee0 --- /dev/null +++ b/priv/repo/migrations/20230618190919_create_domains.exs @@ -0,0 +1,18 @@ +defmodule Pleroma.Repo.Migrations.CreateDomains do + use Ecto.Migration + + def change do + create_if_not_exists table(:domains) do + add(:domain, :string) + add(:public, :boolean) + + timestamps() + end + + create_if_not_exists(unique_index(:domains, [:domain])) + + alter table(:users) do + add(:domain_id, references(:domains)) + end + end +end From 182e0b58daa44e33c1a8d26ffb3c9db5597ba99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= Date: Fri, 10 Nov 2023 00:53:17 +0100 Subject: [PATCH 02/20] Add initial docs page, improve adding domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- config/description.exs | 14 ++++++++- ...to_serve_multiple_domains_for_webfinger.md | 30 +++++++++++++++++++ lib/pleroma/domain.ex | 4 ++- .../controllers/domain_controller.ex | 12 ++++---- .../web/mastodon_api/views/instance_view.ex | 2 +- lib/pleroma/web/web_finger.ex | 2 +- .../20230618190919_create_domains.exs | 2 +- 7 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 docs/configuration/how_to_serve_multiple_domains_for_webfinger.md diff --git a/config/description.exs b/config/description.exs index d18649ae8a..4d80dd846f 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1061,6 +1061,18 @@ suggestions: [ "en" ] + }, + %{ + key: :multitenancy, + type: :map, + description: "Multitenancy support", + children: [ + %{ + key: :enabled, + type: :boolean, + description: "Enables allowing multiple Webfinger domains" + }, + ] } ] }, @@ -3466,5 +3478,5 @@ ] } ] - } + }, ] diff --git a/docs/configuration/how_to_serve_multiple_domains_for_webfinger.md b/docs/configuration/how_to_serve_multiple_domains_for_webfinger.md new file mode 100644 index 0000000000..5a7a3733ff --- /dev/null +++ b/docs/configuration/how_to_serve_multiple_domains_for_webfinger.md @@ -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. diff --git a/lib/pleroma/domain.ex b/lib/pleroma/domain.ex index d708188eed..cf7efc350a 100644 --- a/lib/pleroma/domain.ex +++ b/lib/pleroma/domain.ex @@ -19,11 +19,13 @@ def changeset(%__MODULE__{} = domain, params \\ %{}) do domain |> cast(params, [:domain, :public]) |> validate_required([:domain]) + |> update_change(:domain, &String.downcase/1) + |> unique_constraint(:domain) end def update_changeset(%__MODULE__{} = domain, params \\ %{}) do domain - |> cast(params, [:domain]) + |> cast(params, [:public]) end def get(id), do: Repo.get(__MODULE__, id) diff --git a/lib/pleroma/web/admin_api/controllers/domain_controller.ex b/lib/pleroma/web/admin_api/controllers/domain_controller.ex index 46b0394601..cb2b7dd3f6 100644 --- a/lib/pleroma/web/admin_api/controllers/domain_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/domain_controller.ex @@ -37,11 +37,13 @@ def index(conn, _) do end def create(%{body_params: params} = conn, _) do - domain = - params - |> Domain.create() - - render(conn, "show.json", domain: domain) + with {:domain_not_used, true} <- + {:domain_not_used, params[:domain] !== Pleroma.Web.WebFinger.domain()}, + {:domain, domain} <- Domain.create(params) do + render(conn, "show.json", domain: domain) + else + {:domain_not_used, false} -> {:error, :invalid_domain} + end end def update(%{body_params: params} = conn, %{id: id}) do diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 4ab4ccb74c..fa2b467eca 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -147,7 +147,7 @@ def fields_limits do end defp multitenancy do - enabled = Config.get([:multitenancy, :enabled]) + enabled = Config.get([:instance, :multitenancy, :enabled]) if enabled do domains = diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex index 49c17e2c32..39e38d25cc 100644 --- a/lib/pleroma/web/web_finger.ex +++ b/lib/pleroma/web/web_finger.ex @@ -102,7 +102,7 @@ defp get_subject(%User{nickname: nickname}) do end end - defp domain do + def domain do Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host() end diff --git a/priv/repo/migrations/20230618190919_create_domains.exs b/priv/repo/migrations/20230618190919_create_domains.exs index a306c80ee0..c97e073261 100644 --- a/priv/repo/migrations/20230618190919_create_domains.exs +++ b/priv/repo/migrations/20230618190919_create_domains.exs @@ -3,7 +3,7 @@ defmodule Pleroma.Repo.Migrations.CreateDomains do def change do create_if_not_exists table(:domains) do - add(:domain, :string) + add(:domain, :citext) add(:public, :boolean) timestamps() From 5a30fefb3203c7f9afeff35ae8af734568b4f409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= Date: Fri, 10 Nov 2023 19:00:45 +0100 Subject: [PATCH 03/20] Add tests for domain controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- lib/pleroma/domain.ex | 25 +-- .../controllers/domain_controller.ex | 18 +- .../operations/admin/domain_operation.ex | 2 +- .../controllers/domain_controller_test.exs | 166 ++++++++++++++++++ 4 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 test/pleroma/web/admin_api/controllers/domain_controller_test.exs diff --git a/lib/pleroma/domain.ex b/lib/pleroma/domain.ex index cf7efc350a..a7e390e62d 100644 --- a/lib/pleroma/domain.ex +++ b/lib/pleroma/domain.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Domain do use Ecto.Schema import Ecto.Changeset + import Ecto.Query alias Pleroma.Repo @@ -28,24 +29,24 @@ def update_changeset(%__MODULE__{} = domain, params \\ %{}) do |> cast(params, [:public]) end + def list do + __MODULE__ + |> order_by(asc: :id) + |> Repo.all() + end + def get(id), do: Repo.get(__MODULE__, id) def create(params) do - {:ok, domain} = - %__MODULE__{} - |> changeset(params) - |> Repo.insert() - - domain + %__MODULE__{} + |> changeset(params) + |> Repo.insert() end def update(params, id) do - {:ok, domain} = - get(id) - |> update_changeset(params) - |> Repo.update() - - domain + get(id) + |> update_changeset(params) + |> Repo.update() end def delete(id) do diff --git a/lib/pleroma/web/admin_api/controllers/domain_controller.ex b/lib/pleroma/web/admin_api/controllers/domain_controller.ex index cb2b7dd3f6..d913db8a11 100644 --- a/lib/pleroma/web/admin_api/controllers/domain_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/domain_controller.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.AdminAPI.DomainController do use Pleroma.Web, :controller - alias Pleroma.Repo alias Pleroma.Domain + alias Pleroma.Web.AdminAPI alias Pleroma.Web.Plugs.OAuthScopesPlug import Pleroma.Web.ControllerHelper, @@ -29,9 +29,7 @@ defmodule Pleroma.Web.AdminAPI.DomainController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.DomainOperation def index(conn, _) do - domains = - Domain - |> Repo.all() + domains = Domain.list() render(conn, "index.json", domains: domains) end @@ -39,15 +37,21 @@ def index(conn, _) do def create(%{body_params: params} = conn, _) do with {:domain_not_used, true} <- {:domain_not_used, params[:domain] !== Pleroma.Web.WebFinger.domain()}, - {:domain, domain} <- Domain.create(params) do + {:ok, domain} <- Domain.create(params) do render(conn, "show.json", domain: domain) else - {:domain_not_used, false} -> {:error, :invalid_domain} + {: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 - domain = + {:ok, domain} = params |> Domain.update(id) diff --git a/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex b/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex index d73c67b3d5..37c663b649 100644 --- a/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex @@ -42,7 +42,7 @@ def create_operation do responses: %{ 200 => Operation.response("Response", "application/json", domain()), 400 => Operation.response("Bad Request", "application/json", ApiError), - 403 => Operation.response("Forbidden", "application/json", ApiError) + 403 => Operation.response("Forbidden", "application/json", ApiError), } } end diff --git a/test/pleroma/web/admin_api/controllers/domain_controller_test.exs b/test/pleroma/web/admin_api/controllers/domain_controller_test.exs new file mode 100644 index 0000000000..fc86a09258 --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/domain_controller_test.exs @@ -0,0 +1,166 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# 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 From 9f95368da27c93d1ff6ff9583facf15d0c8fcbb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= Date: Sat, 11 Nov 2023 00:24:54 +0100 Subject: [PATCH 04/20] Add more tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- lib/pleroma/user.ex | 18 +++---- lib/pleroma/web/twitter_api/twitter_api.ex | 10 +++- test/pleroma/user_test.exs | 49 +++++++++++++++++++ .../controllers/instance_controller_test.exs | 49 +++++++++++++++++++ uploads/.gitignore | 3 -- 5 files changed, 116 insertions(+), 13 deletions(-) delete mode 100644 uploads/.gitignore diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index bb180517e1..106569b4d7 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -816,7 +816,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do |> put_private_key() end - defp fix_nickname(changeset, domain_id, from_admin) when is_binary(domain_id) do + defp fix_nickname(changeset, domain_id, from_admin) when not is_nil(domain_id) do with {:domain, domain} <- {:domain, Pleroma.Domain.get(domain_id)}, {:domain_allowed, true} <- {:domain_allowed, from_admin || domain.public} do nickname = get_field(changeset, :nickname) @@ -896,15 +896,15 @@ defp validate_min_age(changeset) do end defp put_ap_id(changeset) do - nickname = get_field(changeset, :nickname) - ap_id = ap_id(%User{nickname: nickname}) + nickname = get_field(changeset, :nickname) + ap_id = ap_id(%User{nickname: nickname}) - ap_id = - if String.contains?(nickname, ".") do - ap_id <> ".json" - else - ap_id - end + ap_id = + if String.contains?(nickname, ".") do + ap_id <> ".json" + else + ap_id + end put_change(changeset, :ap_id, ap_id) end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 485a105e64..818eaa00cd 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -27,7 +27,7 @@ def register_user(params, opts \\ []) do :language, Pleroma.Web.Gettext.normalize_locale(params[:language]) || fallback_language ) - |> Map.put(:domain_id, params[:domain]) + |> maybe_put_domain_id(params[:domain]) if Pleroma.Config.get([:instance, :registrations_open]) do create_user(params, opts) @@ -65,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) <- diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 7f60b959af..611d625b0f 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -836,6 +836,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) diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs index a556ef6a8d..03be9fcdec 100644 --- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -106,4 +106,53 @@ test "instance languages", %{conn: conn} do |> get("/api/v1/instance") |> json_response_and_validate_schema(200) 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 diff --git a/uploads/.gitignore b/uploads/.gitignore deleted file mode 100644 index 523e584a70..0000000000 --- a/uploads/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Git will ignore everything in this directory except this file. -* -!.gitignore From af63670901716ef5a112feee80bfb65054a28094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= Date: Sat, 11 Nov 2023 00:25:01 +0100 Subject: [PATCH 05/20] Add changelog entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- changelog.d/multitenancy.add | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/multitenancy.add diff --git a/changelog.d/multitenancy.add b/changelog.d/multitenancy.add new file mode 100644 index 0000000000..8e95e87385 --- /dev/null +++ b/changelog.d/multitenancy.add @@ -0,0 +1 @@ +Allow using multiple domains for WebFinger \ No newline at end of file From 34c142e672da15697db0b46b29febcf5429c4ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= Date: Sat, 11 Nov 2023 19:13:37 +0100 Subject: [PATCH 06/20] lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- config/description.exs | 4 ++-- lib/pleroma/web/api_spec.ex | 3 ++- lib/pleroma/web/api_spec/operations/admin/domain_operation.ex | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/config/description.exs b/config/description.exs index 4d80dd846f..4aaa939dab 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1071,7 +1071,7 @@ key: :enabled, type: :boolean, description: "Enables allowing multiple Webfinger domains" - }, + } ] } ] @@ -3478,5 +3478,5 @@ ] } ] - }, + } ] diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 163226ce57..9862401cd0 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -104,7 +104,8 @@ def spec(opts \\ []) do "Report managment", "Status administration", "User administration", - "Announcement management" + "Announcement management", + "Domain managment" ] }, %{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]}, diff --git a/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex b/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex index 37c663b649..d73c67b3d5 100644 --- a/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex @@ -42,7 +42,7 @@ def create_operation do responses: %{ 200 => Operation.response("Response", "application/json", domain()), 400 => Operation.response("Bad Request", "application/json", ApiError), - 403 => Operation.response("Forbidden", "application/json", ApiError), + 403 => Operation.response("Forbidden", "application/json", ApiError) } } end From c3b989951adb658dbe7bb2f2979d2552ff1eaa5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 14 Nov 2023 13:51:55 +0100 Subject: [PATCH 07/20] Check if domains resolve correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- config/config.exs | 4 +- lib/pleroma/domain.ex | 13 +++++++ .../controllers/domain_controller.ex | 4 ++ .../web/admin_api/views/domain_view.ex | 26 ++++++++++--- .../web/mastodon_api/views/instance_view.ex | 2 +- .../workers/check_domain_resolve_worker.ex | 38 +++++++++++++++++++ .../cron/check_domains_resolve_worker.ex | 34 +++++++++++++++++ .../20230618190919_create_domains.exs | 2 + 8 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 lib/pleroma/workers/check_domain_resolve_worker.ex create mode 100644 lib/pleroma/workers/cron/check_domains_resolve_worker.ex diff --git a/config/config.exs b/config/config.exs index e8ae31542c..4eac1cb6e8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -591,10 +591,12 @@ remote_fetcher: 2, attachments_cleanup: 1, new_users_digest: 1, - mute_expire: 5 + mute_expire: 5, + 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} ] diff --git a/lib/pleroma/domain.ex b/lib/pleroma/domain.ex index a7e390e62d..aa78b35fe8 100644 --- a/lib/pleroma/domain.ex +++ b/lib/pleroma/domain.ex @@ -12,6 +12,8 @@ defmodule Pleroma.Domain do schema "domains" do field(:domain, :string, default: "") field(:public, :boolean, default: false) + field(:resolves, :boolean, default: false) + field(:last_checked_at, :naive_datetime) timestamps() end @@ -29,6 +31,17 @@ def update_changeset(%__MODULE__{} = domain, params \\ %{}) do |> 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 + def list do __MODULE__ |> order_by(asc: :id) diff --git a/lib/pleroma/web/admin_api/controllers/domain_controller.ex b/lib/pleroma/web/admin_api/controllers/domain_controller.ex index d913db8a11..832f6288c0 100644 --- a/lib/pleroma/web/admin_api/controllers/domain_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/domain_controller.ex @@ -38,6 +38,10 @@ def create(%{body_params: params} = conn, _) do with {:domain_not_used, true} <- {:domain_not_used, params[:domain] !== Pleroma.Web.WebFinger.domain()}, {:ok, domain} <- Domain.create(params) do + Pleroma.Workers.CheckDomainResolveWorker.enqueue("check_domain_resolve", %{ + "id" => domain.id + }) + render(conn, "show.json", domain: domain) else {:domain_not_used, false} -> diff --git a/lib/pleroma/web/admin_api/views/domain_view.ex b/lib/pleroma/web/admin_api/views/domain_view.ex index 0f91d44900..e1057b94f4 100644 --- a/lib/pleroma/web/admin_api/views/domain_view.ex +++ b/lib/pleroma/web/admin_api/views/domain_view.ex @@ -6,16 +6,30 @@ defmodule Pleroma.Web.AdminAPI.DomainView do use Pleroma.Web, :view alias Pleroma.Domain + alias Pleroma.Web.CommonAPI.Utils - def render("index.json", %{domains: domains}) do - render_many(domains, __MODULE__, "show.json") + 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{id: id, domain: domain, public: public}}) do + def render("show.json", %{domain: %Domain{} = domain} = assigns) do %{ - id: id |> to_string(), - domain: domain, - public: public + 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 diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index fa2b467eca..9f78c279c0 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -156,7 +156,7 @@ defp multitenancy do %{ enabled: true, - domains: DomainView.render("index.json", domains: domains) + domains: DomainView.render("index.json", domains: domains, admin: false) } else nil diff --git a/lib/pleroma/workers/check_domain_resolve_worker.ex b/lib/pleroma/workers/check_domain_resolve_worker.ex new file mode 100644 index 0000000000..5312d593c6 --- /dev/null +++ b/lib/pleroma/workers/check_domain_resolve_worker.ex @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# 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: %{"op" => "check_domain_resolve", "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!() + + :ok + end + + @impl Oban.Worker + def timeout(_job), do: :timer.seconds(5) +end diff --git a/lib/pleroma/workers/cron/check_domains_resolve_worker.ex b/lib/pleroma/workers/cron/check_domains_resolve_worker.ex new file mode 100644 index 0000000000..7bb2899495 --- /dev/null +++ b/lib/pleroma/workers/cron/check_domains_resolve_worker.ex @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# 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 diff --git a/priv/repo/migrations/20230618190919_create_domains.exs b/priv/repo/migrations/20230618190919_create_domains.exs index c97e073261..91bf423f87 100644 --- a/priv/repo/migrations/20230618190919_create_domains.exs +++ b/priv/repo/migrations/20230618190919_create_domains.exs @@ -5,6 +5,8 @@ def change do create_if_not_exists table(:domains) do add(:domain, :citext) add(:public, :boolean) + add(:resolves, :boolean) + add(:last_checked_at, :naive_datetime) timestamps() end From 3075b8c923fd073fcda9c567544cd0eec836486d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 14 Nov 2023 13:55:28 +0100 Subject: [PATCH 08/20] Set domain timestamps type to utc_datetime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/domain.ex | 2 +- priv/repo/migrations/20230618190919_create_domains.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/domain.ex b/lib/pleroma/domain.ex index aa78b35fe8..d1c633a930 100644 --- a/lib/pleroma/domain.ex +++ b/lib/pleroma/domain.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Domain do field(:resolves, :boolean, default: false) field(:last_checked_at, :naive_datetime) - timestamps() + timestamps(type: :utc_datetime) end def changeset(%__MODULE__{} = domain, params \\ %{}) do diff --git a/priv/repo/migrations/20230618190919_create_domains.exs b/priv/repo/migrations/20230618190919_create_domains.exs index 91bf423f87..78a0ade589 100644 --- a/priv/repo/migrations/20230618190919_create_domains.exs +++ b/priv/repo/migrations/20230618190919_create_domains.exs @@ -8,7 +8,7 @@ def change do add(:resolves, :boolean) add(:last_checked_at, :naive_datetime) - timestamps() + timestamps(type: :utc_datetime) end create_if_not_exists(unique_index(:domains, [:domain])) From db533b032b5b1a9c95f5757850e825ae0bb2e30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 14 Nov 2023 23:46:24 +0100 Subject: [PATCH 09/20] Add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../workers/check_domain_resolve_worker.ex | 6 +- .../activity_pub_controller_test.exs | 17 +++ test/pleroma/web/web_finger_test.exs | 16 +++ .../check_domains_resolve_worker_test.exs | 118 ++++++++++++++++++ 4 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 test/pleroma/workers/check_domains_resolve_worker_test.exs diff --git a/lib/pleroma/workers/check_domain_resolve_worker.ex b/lib/pleroma/workers/check_domain_resolve_worker.ex index 5312d593c6..efe5a69dda 100644 --- a/lib/pleroma/workers/check_domain_resolve_worker.ex +++ b/lib/pleroma/workers/check_domain_resolve_worker.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Workers.CheckDomainResolveWorker do alias Pleroma.Web.WebFinger @impl Oban.Worker - def perform(%Job{args: %{"op" => "check_domain_resolve", "id" => domain_id}}) do + def perform(%Job{args: %{"id" => domain_id}}) do domain = Domain.get(domain_id) resolves = @@ -28,9 +28,7 @@ def perform(%Job{args: %{"op" => "check_domain_resolve", "id" => domain_id}}) do domain |> Domain.update_state_changeset(resolves) - |> Repo.update!() - - :ok + |> Repo.update() end @impl Oban.Worker diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 0dc61c2e56..b1a32e6c18 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -138,6 +138,23 @@ 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 diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index be5e08776b..42af421a3a 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -37,6 +37,22 @@ 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 diff --git a/test/pleroma/workers/check_domains_resolve_worker_test.exs b/test/pleroma/workers/check_domains_resolve_worker_test.exs new file mode 100644 index 0000000000..615c934cc3 --- /dev/null +++ b/test/pleroma/workers/check_domains_resolve_worker_test.exs @@ -0,0 +1,118 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.ScheduledActivityWorkerTest 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 From 1ee3ba5fd3d553658aef38ae290b0f711f75148d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 16 Nov 2023 01:18:09 +0100 Subject: [PATCH 10/20] Optionally filter local timelines according to domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- config/description.exs | 5 +++ lib/pleroma/web/activity_pub/activity_pub.ex | 19 +++++++++ .../controllers/timeline_controller.ex | 18 ++++++++ .../20230618190919_create_domains.exs | 2 + .../controllers/timeline_controller_test.exs | 42 +++++++++++++++++++ 5 files changed, 86 insertions(+) diff --git a/config/description.exs b/config/description.exs index 1cd4d2d24f..cea74a9b3b 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1071,6 +1071,11 @@ 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" } ] } diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 32d1a1037a..2b04d2dcfb 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -926,6 +926,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 @@ -1404,6 +1422,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) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 293c61b41c..599f2742ba 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -50,6 +50,7 @@ def home(%{assigns: %{user: user}} = conn, params) do |> Map.put(:user, user) |> Map.put(:local_only, params[:local]) |> Map.delete(:local) + |> ActivityPub.fetch_public_activities() activities = [user.ap_id | User.following(user)] @@ -114,6 +115,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(user) |> ActivityPub.fetch_public_activities() conn @@ -150,6 +152,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(user) |> ActivityPub.fetch_public_activities() end @@ -183,6 +186,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]) + |> ActivityPub.fetch_public_activities() # 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 +211,18 @@ 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, user) do + separate_timelines = Config.get([:instance, :multitenancy, :separate_timelines]) + domain_id = if(user, do: user.domain_id || 0, else: 0) + + if separate_timelines do + params + |> Map.put(:domain_id, domain_id) + else + params + end + end + + defp maybe_put_domain_id(params, _user), do: params end diff --git a/priv/repo/migrations/20230618190919_create_domains.exs b/priv/repo/migrations/20230618190919_create_domains.exs index 78a0ade589..9994a46829 100644 --- a/priv/repo/migrations/20230618190919_create_domains.exs +++ b/priv/repo/migrations/20230618190919_create_domains.exs @@ -16,5 +16,7 @@ def change do alter table(:users) do add(:domain_id, references(:domains)) end + + create_if_not_exists(index(:users, [:domain_id])) end end diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index c120dd53c2..8cb0543475 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -408,6 +408,48 @@ 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], %{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 + |> assign(:user, user1) + |> assign(:token, insert(:oauth_token, user: user1, scopes: ["read:statuses"])) + |> get("/api/v1/timelines/public?local=true") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^note2}] = + conn + |> assign(:user, user2) + |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"])) + |> get("/api/v1/timelines/public?local=true") + |> json_response_and_validate_schema(200) + end end defp local_and_remote_activities do From 6d7cd696b53e5a6114c6dbf42897b0bad76c071b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 17 Nov 2023 14:47:40 +0100 Subject: [PATCH 11/20] Update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- docs/development/API/admin_api.md | 67 +++++++++++++++++++ .../API/differences_in_mastoapi_responses.md | 1 + uploads/.gitignore | 3 + 3 files changed, 71 insertions(+) create mode 100644 uploads/.gitignore diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index 7d31ee262f..b55892e99b 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -1751,3 +1751,70 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns ```json {} ``` + +## `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 +{} +``` diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 48a9c104c3..8228f67acd 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -298,6 +298,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 diff --git a/uploads/.gitignore b/uploads/.gitignore new file mode 100644 index 0000000000..523e584a70 --- /dev/null +++ b/uploads/.gitignore @@ -0,0 +1,3 @@ +# Git will ignore everything in this directory except this file. +* +!.gitignore From 45e70ca6ab8fbc649f520eb370aa25c31b1cc966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 17 Nov 2023 14:47:40 +0100 Subject: [PATCH 12/20] Update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- docs/development/API/admin_api.md | 67 +++++++++++++++++++ .../API/differences_in_mastoapi_responses.md | 1 + .../activity_pub_controller_test.exs | 7 +- test/pleroma/web/web_finger_test.exs | 6 +- uploads/.gitignore | 3 + 5 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 uploads/.gitignore diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md index 7d31ee262f..b55892e99b 100644 --- a/docs/development/API/admin_api.md +++ b/docs/development/API/admin_api.md @@ -1751,3 +1751,70 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns ```json {} ``` + +## `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 +{} +``` diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 48a9c104c3..8228f67acd 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -298,6 +298,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 diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index b1a32e6c18..f6d4319617 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -141,9 +141,10 @@ test "it returns a json representation of the user with accept application/ld+js test "it returns a json representation of a local user domain different from host", %{ conn: conn } do - user = insert(:user, %{ - nickname: "nick@example.org" - }) + user = + insert(:user, %{ + nickname: "nick@example.org" + }) conn = conn diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index 42af421a3a..86d3e9d1d3 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -41,8 +41,7 @@ test "works for ap_ids" do test "works for fqns with domains other than host" do user = insert(:user, %{nickname: "nick@example.org"}) - {:ok, result} = - WebFinger.webfinger("#{user.nickname})}", "XML") + {:ok, result} = WebFinger.webfinger("#{user.nickname})}", "XML") assert is_binary(result) end @@ -50,8 +49,7 @@ test "works for fqns with domains other than host" do test "doesn't work for remote users" do user = insert(:user, %{local: false}) - assert {:error, _} = - WebFinger.webfinger("#{user.nickname})}", "XML") + assert {:error, _} = WebFinger.webfinger("#{user.nickname})}", "XML") end end diff --git a/uploads/.gitignore b/uploads/.gitignore new file mode 100644 index 0000000000..523e584a70 --- /dev/null +++ b/uploads/.gitignore @@ -0,0 +1,3 @@ +# Git will ignore everything in this directory except this file. +* +!.gitignore From f5b4aead2819beaac980b35840b68905ed2af6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 21 Nov 2023 23:25:52 +0100 Subject: [PATCH 13/20] update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- test/pleroma/web/mastodon_api/views/account_view_test.exs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 3bb4970caa..b8b927366e 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -93,7 +93,8 @@ test "Represent a user account" do hide_follows_count: false, relationship: %{}, skip_thread_containment: false, - accepts_chat_messages: nil + accepts_chat_messages: nil, + is_local: true } } @@ -337,7 +338,8 @@ test "Represent a Service(bot) account" do hide_follows_count: false, relationship: %{}, skip_thread_containment: false, - accepts_chat_messages: nil + accepts_chat_messages: nil, + is_local: true } } From cee4e2b6a18e4a8bef3ac3f7517442bcfe9bad64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 28 Nov 2023 00:22:47 +0100 Subject: [PATCH 14/20] Cache domains list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/application.ex | 3 ++- lib/pleroma/domain.ex | 15 +++++++++++++++ .../admin_api/controllers/domain_controller.ex | 10 ++++++++-- .../api_spec/operations/admin/domain_operation.ex | 6 +++++- .../web/mastodon_api/views/instance_view.ex | 2 +- .../migrations/20230618190919_create_domains.exs | 2 ++ 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 7bbc132f12..00103abf25 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -210,7 +210,8 @@ defp cachex_children do expiration: chat_message_id_idempotency_key_expiration(), limit: 500_000 ), - build_cachex("rel_me", limit: 2500) + build_cachex("rel_me", limit: 2500), + build_cachex("domain", limit: 2500) ] end diff --git a/lib/pleroma/domain.ex b/lib/pleroma/domain.ex index d1c633a930..6d518fe00a 100644 --- a/lib/pleroma/domain.ex +++ b/lib/pleroma/domain.ex @@ -2,6 +2,8 @@ # Copyright © 2017-2023 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Domain do + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + use Ecto.Schema import Ecto.Changeset @@ -11,6 +13,7 @@ defmodule Pleroma.Domain do 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) @@ -22,8 +25,10 @@ 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 @@ -42,6 +47,12 @@ def update_state_changeset(%__MODULE__{} = domain, resolves) do ) 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) @@ -66,4 +77,8 @@ def delete(id) do get(id) |> Repo.delete() end + + def cached_list do + @cachex.fetch!(:domain_cache, "domains_list", fn _ -> list() end) + end end diff --git a/lib/pleroma/web/admin_api/controllers/domain_controller.ex b/lib/pleroma/web/admin_api/controllers/domain_controller.ex index 832f6288c0..3dd42c59a0 100644 --- a/lib/pleroma/web/admin_api/controllers/domain_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/domain_controller.ex @@ -3,6 +3,8 @@ # 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 @@ -37,7 +39,8 @@ def index(conn, _) do def create(%{body_params: params} = conn, _) do with {:domain_not_used, true} <- {:domain_not_used, params[:domain] !== Pleroma.Web.WebFinger.domain()}, - {:ok, domain} <- Domain.create(params) do + {:ok, domain} <- Domain.create(params), + _ <- @cachex.del(:domain, :domains_list) do Pleroma.Workers.CheckDomainResolveWorker.enqueue("check_domain_resolve", %{ "id" => domain.id }) @@ -59,11 +62,14 @@ def update(%{body_params: params} = conn, %{id: id}) do 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) do + with {:ok, _} <- Domain.delete(id), + _ <- @cachex.del(:domain, :domains_list) do json(conn, %{}) else _ -> json_response(conn, :bad_request, "") diff --git a/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex b/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex index d73c67b3d5..a91872f976 100644 --- a/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/domain_operation.ex @@ -84,6 +84,7 @@ defp create_request do required: [:domain], properties: %{ domain: %Schema{type: :string}, + service_domain: %Schema{type: :string, nullable: true}, public: %Schema{type: :boolean, nullable: true} } } @@ -104,7 +105,10 @@ defp domain do properties: %{ id: %Schema{type: :integer}, domain: %Schema{type: :string}, - public: %Schema{type: :boolean} + 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 diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 9f78c279c0..ecc546ce73 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -152,7 +152,7 @@ defp multitenancy do if enabled do domains = [%Domain{id: "", domain: Pleroma.Web.WebFinger.domain(), public: true}] ++ - Repo.all(Domain) + Domain.cached_list() %{ enabled: true, diff --git a/priv/repo/migrations/20230618190919_create_domains.exs b/priv/repo/migrations/20230618190919_create_domains.exs index 9994a46829..cfb1498949 100644 --- a/priv/repo/migrations/20230618190919_create_domains.exs +++ b/priv/repo/migrations/20230618190919_create_domains.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Repo.Migrations.CreateDomains do 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) @@ -12,6 +13,7 @@ def change do 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)) From c4a89058cacf7d6c39c016dddc106e4da8cc1e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 29 Nov 2023 00:10:05 +0100 Subject: [PATCH 15/20] Use service domains like Takahe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- config/config.exs | 5 ++- lib/pleroma/domain.ex | 6 ++++ lib/pleroma/user.ex | 7 ---- .../activity_pub/activity_pub_controller.ex | 11 +++++-- lib/pleroma/web/feed/user_controller.ex | 1 + .../controllers/timeline_controller.ex | 1 - .../plugs/assign_nickname_with_domain_plug.ex | 33 +++++++++++++++++++ 7 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 lib/pleroma/web/plugs/assign_nickname_with_domain_plug.ex diff --git a/config/config.exs b/config/config.exs index 70a586cee5..61a6fd3a1a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -260,7 +260,10 @@ max_endorsed_users: 20, birthday_required: false, birthday_min_age: 0, - max_media_attachments: 1_000 + max_media_attachments: 1_000, + multitenancy: %{ + enabled: false + } config :pleroma, :welcome, direct_message: [ diff --git a/lib/pleroma/domain.ex b/lib/pleroma/domain.ex index 6d518fe00a..a07fcc06f9 100644 --- a/lib/pleroma/domain.ex +++ b/lib/pleroma/domain.ex @@ -61,6 +61,12 @@ def list do 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) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 106569b4d7..97ebc2f224 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -899,13 +899,6 @@ defp put_ap_id(changeset) do nickname = get_field(changeset, :nickname) ap_id = ap_id(%User{nickname: nickname}) - ap_id = - if String.contains?(nickname, ".") do - ap_id <> ".json" - else - ap_id - end - put_change(changeset, :ap_id, ap_id) end diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 52668776ff..ad150b98f4 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -52,6 +52,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do when action in [:activity, :object] ) + plug( + Pleroma.Web.Plugs.AssignNicknameWithDomainPlug + 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]) @@ -66,8 +71,10 @@ defp relay_active?(conn, _) do end def user(conn, %{"nickname" => nickname}) do - with %User{local: true} = user <- - nickname |> URI.decode() |> User.get_cached_by_nickname_or_id() do + with nickname( + %User{local: true} = user <- + nickname |> URI.decode() |> User.get_cached_by_nickname_or_id() + ) do conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 6657c2b3ed..9ab7250571 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -12,6 +12,7 @@ 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.AssignNicknameWithDomainPlug when action in [:feed_redirect, :feed]) action_fallback(:errors) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 599f2742ba..89e295f2db 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -50,7 +50,6 @@ def home(%{assigns: %{user: user}} = conn, params) do |> Map.put(:user, user) |> Map.put(:local_only, params[:local]) |> Map.delete(:local) - |> ActivityPub.fetch_public_activities() activities = [user.ap_id | User.following(user)] diff --git a/lib/pleroma/web/plugs/assign_nickname_with_domain_plug.ex b/lib/pleroma/web/plugs/assign_nickname_with_domain_plug.ex new file mode 100644 index 0000000000..b140da5355 --- /dev/null +++ b/lib/pleroma/web/plugs/assign_nickname_with_domain_plug.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.AssignNicknameWithDomainPlug do + alias Pleroma.Domain + + use Pleroma.Web, :plug + + def init(opts), do: opts + + @impl true + def perform(%{host: domain, params: params} = conn, opts) do + with key <- Keyword.get(opts, :key, "nickname"), + nickname <- Map.get(params, key), + false <- String.contains?(nickname, "@"), + true <- Pleroma.Config.get([:instance, :multitenancy, :enabled], false), + false <- + domain in [ + Pleroma.Config.get([__MODULE__, :domain]), + Pleroma.Web.Endpoint.host() + ], + %Domain{domain: domain} <- Domain.get_by_service_domain(domain), + nickname <- nickname <> "@" <> domain do + conn + |> Map.put(:params, %{"nickname" => nickname}) + else + _ -> conn + end + end + + def perform(conn, _), do: conn +end From 71075b901bb8360ab1e01a8201276fa39ff15245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 29 Nov 2023 10:58:15 +0100 Subject: [PATCH 16/20] Separate nickname/domain plugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../activity_pub/activity_pub_controller.ex | 13 ++++++---- lib/pleroma/web/feed/user_controller.ex | 3 ++- ...with_domain_plug.ex => set_domain_plug.ex} | 15 ++++------- .../plugs/set_nickname_with_domain_plug.ex | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+), 16 deletions(-) rename lib/pleroma/web/plugs/{assign_nickname_with_domain_plug.ex => set_domain_plug.ex} (55%) create mode 100644 lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index ad150b98f4..67c66bdb0f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -53,7 +53,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do ) plug( - Pleroma.Web.Plugs.AssignNicknameWithDomainPlug + 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] ) @@ -71,10 +76,8 @@ defp relay_active?(conn, _) do end def user(conn, %{"nickname" => nickname}) do - with nickname( - %User{local: true} = user <- - nickname |> URI.decode() |> User.get_cached_by_nickname_or_id() - ) do + with %User{local: true} = user <- + nickname |> URI.decode() |> User.get_cached_by_nickname_or_id() do conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index 9ab7250571..5c1466d20a 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -12,7 +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.AssignNicknameWithDomainPlug when action in [:feed_redirect, :feed]) + 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) diff --git a/lib/pleroma/web/plugs/assign_nickname_with_domain_plug.ex b/lib/pleroma/web/plugs/set_domain_plug.ex similarity index 55% rename from lib/pleroma/web/plugs/assign_nickname_with_domain_plug.ex rename to lib/pleroma/web/plugs/set_domain_plug.ex index b140da5355..0a3673ddba 100644 --- a/lib/pleroma/web/plugs/assign_nickname_with_domain_plug.ex +++ b/lib/pleroma/web/plugs/set_domain_plug.ex @@ -2,7 +2,7 @@ # Copyright © 2017-2023 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Web.Plugs.AssignNicknameWithDomainPlug do +defmodule Pleroma.Web.Plugs.SetDomainPlug do alias Pleroma.Domain use Pleroma.Web, :plug @@ -10,20 +10,15 @@ defmodule Pleroma.Web.Plugs.AssignNicknameWithDomainPlug do def init(opts), do: opts @impl true - def perform(%{host: domain, params: params} = conn, opts) do - with key <- Keyword.get(opts, :key, "nickname"), - nickname <- Map.get(params, key), - false <- String.contains?(nickname, "@"), - true <- Pleroma.Config.get([:instance, :multitenancy, :enabled], false), + 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} <- Domain.get_by_service_domain(domain), - nickname <- nickname <> "@" <> domain do - conn - |> Map.put(:params, %{"nickname" => nickname}) + %Domain{domain: domain} <- Domain.get_by_service_domain(domain) do + Map.put(conn, :domain, domain) else _ -> conn end diff --git a/lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex b/lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex new file mode 100644 index 0000000000..ba00f62511 --- /dev/null +++ b/lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.SetNicknameWithDomainPlug do + alias Pleroma.Domain + + 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 do + Map.put(conn, :params, %{"nickname" => nickname}) + else + _ -> conn + end + end + + def perform(conn, _), do: conn +end From 7e57c5d0b1c2aebbafe527a29c0195b73fa959cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 24 Jan 2024 19:17:50 +0100 Subject: [PATCH 17/20] Handle trailing formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../web/activity_pub/views/user_view.ex | 17 +-- lib/pleroma/web/endpoint.ex | 5 +- lib/pleroma/web/plugs/trailing_format_plug.ex | 103 +++++++++++++++++- mix.exs | 1 - mix.lock | 1 - 5 files changed, 108 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index fe54f5e026..24ee683ae3 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -96,21 +96,14 @@ def render("user.json", %{user: user}) do do: Date.to_iso8601(user.birthday), else: nil - ap_id = - if String.ends_with?(user.ap_id, ".json") do - String.slice(user.ap_id, 0..-6) - else - user.ap_id - end - %{ "id" => user.ap_id, "type" => user.actor_type, - "following" => "#{ap_id}/following", - "followers" => "#{ap_id}/followers", - "inbox" => "#{ap_id}/inbox", - "outbox" => "#{ap_id}/outbox", - "featured" => "#{ap_id}/collections/featured", + "following" => "#{user.ap_id}/following", + "followers" => "#{user.ap_id}/followers", + "inbox" => "#{user.ap_id}/inbox", + "outbox" => "#{user.ap_id}/outbox", + "featured" => "#{user.ap_id}/collections/featured", "preferredUsername" => user.nickname, "name" => user.name, "summary" => user.bio, diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 307fa069e1..2f4620eddc 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -109,7 +109,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) diff --git a/lib/pleroma/web/plugs/trailing_format_plug.ex b/lib/pleroma/web/plugs/trailing_format_plug.ex index a883ba5419..8b14500afa 100644 --- a/lib/pleroma/web/plugs/trailing_format_plug.ex +++ b/lib/pleroma/web/plugs/trailing_format_plug.ex @@ -1,9 +1,65 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors +# Copyright © 2017-2024 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only +defmodule TrailingFormatPlug do + @behaviour Plug + + def init(options), do: options + + def call(conn, opts \\ []) + + def call(%{path_info: []} = conn, _opts), do: conn + + def call(conn, opts) do + 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 + + 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 + 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 +85,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 diff --git a/mix.exs b/mix.exs index a13c89c04b..890273ba99 100644 --- a/mix.exs +++ b/mix.exs @@ -131,7 +131,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"}, diff --git a/mix.lock b/mix.lock index ba1ac2c9d2..a854165ac4 100644 --- a/mix.lock +++ b/mix.lock @@ -135,7 +135,6 @@ "tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"}, "timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, - "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"}, "tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"}, "ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, From 7c36a9165749ee122ee32199699c7264b411e7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 24 Jan 2024 19:45:07 +0100 Subject: [PATCH 18/20] Fixes, update tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/user.ex | 2 +- .../activity_pub/activity_pub_controller.ex | 5 +- .../admin_api/controllers/user_controller.ex | 9 +++- .../controllers/media_controller.ex | 5 +- .../controllers/timeline_controller.ex | 21 ++++---- .../web/mastodon_api/views/instance_view.ex | 2 +- lib/pleroma/web/plugs/set_domain_plug.ex | 8 +-- .../plugs/set_nickname_with_domain_plug.ex | 7 ++- lib/pleroma/web/plugs/trailing_format_plug.ex | 53 ------------------- .../controllers/timeline_controller_test.exs | 11 ++-- .../check_domains_resolve_worker_test.exs | 2 +- 11 files changed, 36 insertions(+), 89 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 47ef6c1f02..76d754db7d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -818,7 +818,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do end defp fix_nickname(changeset, domain_id, from_admin) when not is_nil(domain_id) do - with {:domain, domain} <- {:domain, Pleroma.Domain.get(domain_id)}, + with {:domain, domain} <- {:domain, Domain.get(domain_id)}, {:domain_allowed, true} <- {:domain_allowed, from_admin || domain.public} do nickname = get_field(changeset, :nickname) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index d2c678c617..5865119311 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -76,8 +76,7 @@ defp relay_active?(conn, _) do end def user(conn, %{"nickname" => nickname}) do - with %User{local: true} = user <- - nickname |> URI.decode() |> User.get_cached_by_nickname_or_id() do + with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do conn |> put_resp_content_type("application/activity+json") |> put_view(UserView) @@ -536,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)) diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex index e08831d5a4..f44abdf0d0 100644 --- a/lib/pleroma/web/admin_api/controllers/user_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex @@ -128,8 +128,13 @@ def unfollow( def create(%{assigns: %{user: admin}, body_params: %{users: users}} = conn, _) do changesets = users - |> Enum.map(fn %{nickname: nickname, email: email, password: password, domain: domain} -> - domain = Domain.get(domain) + |> 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, diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 7d9a63cf4a..e416042945 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -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 @@ -24,7 +23,7 @@ def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, 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) @@ -40,7 +39,7 @@ def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, 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) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 89e295f2db..d1ea612b11 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -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,7 +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(user) + |> maybe_put_domain_id(conn) |> ActivityPub.fetch_public_activities() conn @@ -132,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]] @@ -151,7 +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(user) + |> maybe_put_domain_id(conn) |> ActivityPub.fetch_public_activities() end @@ -162,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}) @@ -185,7 +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]) - |> ActivityPub.fetch_public_activities() + |> 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). @@ -211,17 +213,18 @@ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do end end - defp maybe_put_domain_id(%{local_only: true} = params, user) do + defp maybe_put_domain_id(%{local_only: true} = params, conn) do separate_timelines = Config.get([:instance, :multitenancy, :separate_timelines]) - domain_id = if(user, do: user.domain_id || 0, else: 0) if separate_timelines do + domain = Map.get(conn, :domain, %{id: 0}) + params - |> Map.put(:domain_id, domain_id) + |> Map.put(:domain_id, domain.id) else params end end - defp maybe_put_domain_id(params, _user), do: params + defp maybe_put_domain_id(params, _conn), do: params end diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 9978daeeb5..e058c0183a 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -207,7 +207,7 @@ defp pleroma_configuration(instance) do birthday_required: Config.get([:instance, :birthday_required]), birthday_min_age: Config.get([:instance, :birthday_min_age]), multitenancy: multitenancy() - }, + }, stats: %{mau: Pleroma.User.active_user_count()}, vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) } diff --git a/lib/pleroma/web/plugs/set_domain_plug.ex b/lib/pleroma/web/plugs/set_domain_plug.ex index 0a3673ddba..fc58987f7c 100644 --- a/lib/pleroma/web/plugs/set_domain_plug.ex +++ b/lib/pleroma/web/plugs/set_domain_plug.ex @@ -3,21 +3,21 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Plugs.SetDomainPlug do - alias Pleroma.Domain - use Pleroma.Web, :plug + alias Pleroma.Domain + def init(opts), do: opts @impl true - def perform(%{host: domain} = conn, opts) do + 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} <- Domain.get_by_service_domain(domain) do + %Domain{} = domain <- Domain.get_by_service_domain(domain) do Map.put(conn, :domain, domain) else _ -> conn diff --git a/lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex b/lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex index ba00f62511..ca0c59af12 100644 --- a/lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex +++ b/lib/pleroma/web/plugs/set_nickname_with_domain_plug.ex @@ -3,8 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Plugs.SetNicknameWithDomainPlug do - alias Pleroma.Domain - use Pleroma.Web, :plug def init(opts), do: opts @@ -14,8 +12,9 @@ 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 do - Map.put(conn, :params, %{"nickname" => nickname}) + nickname <- nickname <> "@" <> domain.domain, + params <- Map.put(params, "nickname", nickname) do + Map.put(conn, :params, params) else _ -> conn end diff --git a/lib/pleroma/web/plugs/trailing_format_plug.ex b/lib/pleroma/web/plugs/trailing_format_plug.ex index 8b14500afa..652ee45876 100644 --- a/lib/pleroma/web/plugs/trailing_format_plug.ex +++ b/lib/pleroma/web/plugs/trailing_format_plug.ex @@ -2,59 +2,6 @@ # Copyright © 2017-2024 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule TrailingFormatPlug do - @behaviour Plug - - def init(options), do: options - - def call(conn, opts \\ []) - - def call(%{path_info: []} = conn, _opts), do: conn - - def call(conn, opts) do - 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 - - 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 - defmodule Pleroma.Web.Plugs.TrailingFormatPlug do @moduledoc """ This plug is adapted from [`TrailingFormatPlug`](https://github.com/mschae/trailing_format_plug/blob/master/lib/trailing_format_plug.ex). diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index 8cb0543475..b104c9bfbe 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -410,12 +410,11 @@ test "should not return local-only posts for anonymous users" do end test "filtering local posts basing on domain", %{conn: conn} do - clear_config([:instance, :multitenancy], %{separate_timelines: false}) + 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) @@ -429,7 +428,7 @@ test "filtering local posts basing on domain", %{conn: conn} do |> get("/api/v1/timelines/public?local=true") |> json_response_and_validate_schema(200) - clear_config([:instance, :multitenancy], %{separate_timelines: true}) + clear_config([:instance, :multitenancy, :separate_timelines], true) assert [%{"id" => ^note1}] = conn @@ -438,16 +437,12 @@ test "filtering local posts basing on domain", %{conn: conn} do assert [%{"id" => ^note1}] = conn - |> assign(:user, user1) - |> assign(:token, insert(:oauth_token, user: user1, scopes: ["read:statuses"])) |> get("/api/v1/timelines/public?local=true") |> json_response_and_validate_schema(200) assert [%{"id" => ^note2}] = conn - |> assign(:user, user2) - |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"])) - |> get("/api/v1/timelines/public?local=true") + |> get("http://pleroma.example.org/api/v1/timelines/public?local=true") |> json_response_and_validate_schema(200) end end diff --git a/test/pleroma/workers/check_domains_resolve_worker_test.exs b/test/pleroma/workers/check_domains_resolve_worker_test.exs index 615c934cc3..4d448dd89f 100644 --- a/test/pleroma/workers/check_domains_resolve_worker_test.exs +++ b/test/pleroma/workers/check_domains_resolve_worker_test.exs @@ -2,7 +2,7 @@ # Copyright © 2017-2022 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Workers.ScheduledActivityWorkerTest do +defmodule Pleroma.Workers.CheckDomainsResolveWorkerTest do use Pleroma.DataCase alias Pleroma.Domain From 33eea71f5f5e52dda72dece35ecc04ed83a143ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 7 Feb 2024 17:52:48 +0100 Subject: [PATCH 19/20] Use full_nickname in OAuth html template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/web/templates/o_auth/o_auth/show.html.eex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 5b38f71427..052dd7d3b3 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -13,7 +13,7 @@ <% end %> From c773ea1341503224f06c7ef3219659ce2ee185db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 9 Feb 2024 16:41:18 +0100 Subject: [PATCH 20/20] Add 'multitenancy' to features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 3 ++- lib/pleroma/web/templates/o_auth/o_auth/show.html.eex | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index e058c0183a..0ba47dfccc 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -129,7 +129,8 @@ def features do "profile_directory" end, "pleroma:get:main/ostatus", - "pleroma:group_actors" + "pleroma:group_actors", + "multitenancy" ] |> Enum.filter(& &1) end diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 052dd7d3b3..6bc8eb6023 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -13,7 +13,7 @@ <% end %>