diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a3733eebe..f61e22dad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -290,7 +290,7 @@ stop_review_app: amd64: stage: release image: - name: hexpm/elixir-amd64:1.17.3-erlang-26.2.5.6-ubuntu-focal-20241011 + name: hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 only: &release-only - stable@pleroma/pleroma - develop@pleroma/pleroma @@ -317,7 +317,7 @@ amd64: VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS DEBIAN_FRONTEND: noninteractive before_script: &before-release - - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git + - apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential - echo "import Config" > config/prod.secret.exs - mix local.hex --force - mix local.rebar --force @@ -333,7 +333,7 @@ amd64-musl: artifacts: *release-artifacts only: *release-only image: - name: hexpm/elixir-amd64:1.17.3-erlang-26.2.5.6-alpine-3.17.9 + name: hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 tags: - amd64 cache: *release-cache @@ -377,7 +377,7 @@ arm64: tags: - arm image: - name: hexpm/elixir-arm64:1.17.3-erlang-26.2.5.6-ubuntu-focal-20241011 + name: hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716 cache: *release-cache variables: *release-variables before_script: *before-release @@ -390,7 +390,7 @@ arm64-musl: tags: - arm image: - name: hexpm/elixir-arm64:1.17.3-erlang-26.2.5.6-alpine-3.17.9 + name: hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1 cache: *release-cache variables: *release-variables before_script: *before-release-musl diff --git a/Dockerfile b/Dockerfile index fff58154e..11a17b7ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ # https://hub.docker.com/r/hexpm/elixir/tags ARG ELIXIR_IMG=hexpm/elixir -ARG ELIXIR_VER=1.14.5 -ARG ERLANG_VER=25.3.2.14 +ARG ELIXIR_VER=1.17.3 +ARG ERLANG_VER=26.2.5.6 ARG ALPINE_VER=3.17.9 -FROM ${ELIXIR_IMG}:${ELIXIR_VER}-erlang-${ERLANG_VER}-alpine-${ALPINE_VER} as build +FROM ${ELIXIR_IMG}:${ELIXIR_VER}-erlang-${ERLANG_VER}-alpine-${ALPINE_VER} AS build COPY . . @@ -15,6 +15,7 @@ RUN apk add git gcc g++ musl-dev make cmake file-dev vips-dev &&\ echo "import Config" > config/prod.secret.exs &&\ mix local.hex --force &&\ mix local.rebar --force &&\ + mix deps.clean --all &&\ mix deps.get --only prod &&\ mkdir release &&\ mix release --path release diff --git a/changelog.d/admin-self-revocation.security b/changelog.d/admin-self-revocation.security new file mode 100644 index 000000000..a311ca1ed --- /dev/null +++ b/changelog.d/admin-self-revocation.security @@ -0,0 +1 @@ +Admin API: Fixed self-revocation vulnerability where admins could accidentally revoke their own admin status via the single-user permission endpoint \ No newline at end of file diff --git a/changelog.d/dockerfile-versions.change b/changelog.d/dockerfile-versions.change new file mode 100644 index 000000000..54b3df93d --- /dev/null +++ b/changelog.d/dockerfile-versions.change @@ -0,0 +1 @@ +Update Dockerfile to use Elixir 1.17.3, Erlang 26.2.5.6, and Alpine 3.17.9 to match CI release builds \ No newline at end of file diff --git a/changelog.d/moderation-log-unknown-actions.fix b/changelog.d/moderation-log-unknown-actions.fix new file mode 100644 index 000000000..8940e8d34 --- /dev/null +++ b/changelog.d/moderation-log-unknown-actions.fix @@ -0,0 +1 @@ +Fix ModerationLog FunctionClauseError for unknown actions \ No newline at end of file diff --git a/changelog.d/repost-repeat-filtering-3391.add b/changelog.d/repost-repeat-filtering-3391.add new file mode 100644 index 000000000..b4dce4397 --- /dev/null +++ b/changelog.d/repost-repeat-filtering-3391.add @@ -0,0 +1 @@ +Add only_reblogs parameter to account statuses API for filtering to show only reblogs/reposts \ No newline at end of file diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index cbd0d6bce..4326c4255 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -88,6 +88,7 @@ The `id` parameter can also be the `nickname` of the user. This only works in th - `only_media`: include only statuses with media attached - `with_muted`: include statuses/reactions from muted accounts - `exclude_reblogs`: exclude reblogs +- `only_reblogs`: include only reblogs - `exclude_replies`: exclude replies - `exclude_visibilities`: exclude visibilities diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 5c3ca58b0..52a71bc2d 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -575,6 +575,12 @@ defmodule Pleroma.ModerationLog do "@#{actor_nickname} requested account backup for @#{user_nickname}" end + def get_log_entry_message(%ModerationLog{data: data}) do + actor_name = get_in(data, ["actor", "nickname"]) || "unknown" + action = data["action"] || "unknown" + "@#{actor_name} performed action #{action}" + end + defp nicknames_to_string(nicknames) do nicknames |> Enum.map(&"@#{&1}") diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 62c7a7b31..99dcd9603 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1065,6 +1065,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data)) end + defp restrict_reblogs(query, %{only_reblogs: true}) do + from(activity in query, where: fragment("?->>'type' = 'Announce'", activity.data)) + end + defp restrict_reblogs(query, _), do: query defp restrict_muted(query, %{with_muted: true}), do: query diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex index b35f5cdcd..ea852748e 100644 --- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -240,6 +240,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do render_error(conn, :not_found, "No such permission_group") end + def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do + render_error(conn, :forbidden, "You can't revoke your own admin status.") + end + def right_delete( %{assigns: %{user: admin}} = conn, %{ @@ -265,10 +269,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do json(conn, fields) end - def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do - render_error(conn, :forbidden, "You can't revoke your own admin status.") - end - @doc "Get a password reset token (base64 string) for given nickname" def get_password_reset(conn, %{"nickname" => nickname}) do (%User{local: true} = user) = User.get_cached_by_nickname(nickname) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index d63e92d16..5a19e0fbb 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -143,6 +143,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do "Include statuses from muted accounts." ), Operation.parameter(:exclude_reblogs, :query, BooleanLike.schema(), "Exclude reblogs"), + Operation.parameter( + :only_reblogs, + :query, + BooleanLike.schema(), + "Include only reblogs" + ), Operation.parameter(:exclude_replies, :query, BooleanLike.schema(), "Exclude replies"), Operation.parameter( :exclude_visibilities, diff --git a/test/pleroma/moderation_log_test.exs b/test/pleroma/moderation_log_test.exs index 584b8708b..45fb3b8e1 100644 --- a/test/pleroma/moderation_log_test.exs +++ b/test/pleroma/moderation_log_test.exs @@ -308,4 +308,37 @@ defmodule Pleroma.ModerationLogTest do assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}" end end + + describe "get_log_entry_message/1" do + setup do + moderator = insert(:user, is_moderator: true) + [moderator: moderator] + end + + test "handles unknown action types gracefully", %{moderator: moderator} do + log_entry = %ModerationLog{ + data: %{ + "actor" => %{"nickname" => moderator.nickname}, + "action" => "unknown_action", + "some_data" => "test_value" + } + } + + assert ModerationLog.get_log_entry_message(log_entry) =~ moderator.nickname + assert ModerationLog.get_log_entry_message(log_entry) =~ "unknown_action" + end + + test "handles malformed log entries gracefully" do + log_entry = %ModerationLog{ + data: %{ + "action" => "force_password_reset" + # Missing "actor" and "subject" fields + } + } + + message = ModerationLog.get_log_entry_message(log_entry) + assert is_binary(message) + assert message =~ "force_password_reset" + end + end end diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index c16f081f6..a5e9d3879 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -1270,6 +1270,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert activity == expected_activity end + test "includes only reblogs on request" do + user = insert(:user) + {:ok, _} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) + {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) + + [activity] = ActivityPub.fetch_user_activities(user, nil, %{only_reblogs: true}) + + assert activity == expected_activity + end + describe "irreversible filters" do setup do user = insert(:user) diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs index 6614d1409..fc578d828 100644 --- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs @@ -321,6 +321,36 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert ModerationLog.get_log_entry_message(log_entry) == "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{user_two.nickname}" end + + test "/:right DELETE, admin cannot revoke their own admin status (single)", %{ + admin: admin, + conn: conn + } do + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users/#{admin.nickname}/permission_group/admin") + + assert json_response(conn, 403) == %{"error" => "You can't revoke your own admin status."} + end + + test "/:right DELETE, admin cannot revoke their own admin status (multiple)", %{ + admin: admin, + conn: conn + } do + user = insert(:user, is_admin: true) + + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users/permission_group/admin", %{ + nicknames: [admin.nickname, user.nickname] + }) + + assert json_response(conn, 403) == %{ + "error" => "You can't revoke your own admin/moderator status." + } + end end describe "/api/pleroma/admin/users/:nickname/password_reset" do diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index cd3107f32..2d91964da 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -469,6 +469,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200) end + test "gets only a user's reblogs", %{user: user, conn: conn} do + {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"}) + {:ok, %{id: reblog_id}} = CommonAPI.repeat(post_id, user) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_reblogs=true") + assert [%{"id" => ^reblog_id}] = json_response_and_validate_schema(conn, 200) + + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_reblogs=1") + assert [%{"id" => ^reblog_id}] = json_response_and_validate_schema(conn, 200) + end + test "filters user's statuses by a hashtag", %{user: user, conn: conn} do {:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "#hashtag"}) {:ok, _post} = CommonAPI.post(user, %{status: "hashtag"})