diff --git a/CHANGELOG.md b/CHANGELOG.md index 33252ad3d..182f5e579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** attachments are removed along with statuses when there are no other references to it - **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default +- **Breaking:** OAuth: defaulted `[:auth, :enforce_oauth_admin_scope_usage]` setting to `true` which demands `admin` OAuth scope to perform admin actions (in addition to `is_admin` flag on User); make sure to use bundled or newer versions of AdminFE & PleromaFE to access admin / moderator features. - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings) - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler - Enabled `:instance, extended_nickname_format` in the default config diff --git a/config/config.exs b/config/config.exs index d41abf090..b0036fff0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -561,7 +561,7 @@ config :pleroma, :auth, - enforce_oauth_admin_scope_usage: false, + enforce_oauth_admin_scope_usage: true, oauth_consumer_strategies: oauth_consumer_strategies config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index 582fb1f92..3190163d3 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -23,6 +23,7 @@ def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do token && OAuth.Scopes.contains_admin_scopes?(token.scopes) -> # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. # Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. + # Admin might opt out of admin scope for some apps to block any admin actions from them. conn true -> diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 2e225415c..430f04ae9 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1874,22 +1874,13 @@ defp truncate_field(%{"name" => name, "value" => value}) do end def admin_api_update(user, params) do - changeset = - cast(user, params, [ - :is_moderator, - :is_admin, - :show_role - ]) - - with {:ok, updated_user} <- update_and_set_cache(changeset) do - if user.is_admin != updated_user.is_admin do - # Admin status change results in change of accessible OAuth scopes, and instead of changing - # already issued tokens we revoke them, requiring user to sign in again - global_sign_out(user) - end - - {:ok, updated_user} - end + user + |> cast(params, [ + :is_moderator, + :is_admin, + :show_role + ]) + |> update_and_set_cache() end @doc "Signs user out of all applications" diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index c8abeff06..529169c1b 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -32,19 +32,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["read:accounts"], admin: true} - when action in [:list_users, :user_show, :right_get, :invites] + when action in [:list_users, :user_show, :right_get] ) plug( OAuthScopesPlug, %{scopes: ["write:accounts"], admin: true} when action in [ - :get_invite_token, - :revoke_invite, - :email_invite, :get_password_reset, - :user_follow, - :user_unfollow, :user_delete, :users_create, :user_toggle_activation, @@ -57,6 +52,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do ] ) + plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites) + + plug( + OAuthScopesPlug, + %{scopes: ["write:invites"], admin: true} + when action in [:create_invite_token, :revoke_invite, :email_invite] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["write:follows"], admin: true} + when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow] + ) + plug( OAuthScopesPlug, %{scopes: ["read:reports"], admin: true} @@ -90,7 +99,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["write"], admin: true} - when action in [:relay_follow, :relay_unfollow, :config_update] + when action == :config_update ) @users_page_size 50 diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 87acdec97..d31a3d91c 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -222,7 +222,7 @@ def token_exchange( {:user_active, true} <- {:user_active, !user.deactivated}, {:password_reset_pending, false} <- {:password_reset_pending, user.password_reset_pending}, - {:ok, scopes} <- validate_scopes(app, params, user), + {:ok, scopes} <- validate_scopes(app, params), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do json(conn, Token.Response.build(user, token)) @@ -471,7 +471,7 @@ defp do_create_authorization( {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, %App{} = app <- Repo.get_by(App, client_id: client_id), true <- redirect_uri in String.split(app.redirect_uris), - {:ok, scopes} <- validate_scopes(app, auth_attrs, user), + {:ok, scopes} <- validate_scopes(app, auth_attrs), {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do Authorization.create_authorization(app, user, scopes) end @@ -487,12 +487,12 @@ defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :re defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), do: put_session(conn, :registration_id, registration_id) - @spec validate_scopes(App.t(), map(), User.t()) :: + @spec validate_scopes(App.t(), map()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - defp validate_scopes(%App{} = app, params, %User{} = user) do + defp validate_scopes(%App{} = app, params) do params |> Scopes.fetch_scopes(app.scopes) - |> Scopes.validate(app.scopes, user) + |> Scopes.validate(app.scopes) end def default_redirect_uri(%App{} = app) do diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 00da225b9..151467494 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.OAuth.Scopes do """ alias Pleroma.Plugs.OAuthScopesPlug - alias Pleroma.User @doc """ Fetch scopes from request params. @@ -56,35 +55,18 @@ def to_string(scopes), do: Enum.join(scopes, " ") @doc """ Validates scopes. """ - @spec validate(list() | nil, list(), User.t()) :: + @spec validate(list() | nil, list()) :: {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} - def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []], + def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []], do: {:error, :missing_scopes} - def validate(scopes, app_scopes, %User{} = user) do - with {:ok, _} <- ensure_scopes_support(scopes, app_scopes), - {:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do - {:ok, scopes} - end - end - - defp ensure_scopes_support(scopes, app_scopes) do + def validate(scopes, app_scopes) do case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do ^scopes -> {:ok, scopes} _ -> {:error, :unsupported_scopes} end end - defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do - if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do - {:ok, scopes} - else - # Gracefully dropping admin scopes from requested scopes if user isn't an admin (not raising) - scopes = scopes -- OAuthScopesPlug.filter_descendants(scopes, ["admin"]) - validate(scopes, app_scopes, user) - end - end - def contains_admin_scopes?(scopes) do scopes |> OAuthScopesPlug.filter_descendants(["admin"]) diff --git a/priv/repo/migrations/20191220174645_add_scopes_to_pleroma_feo_auth_records.exs b/priv/repo/migrations/20191220174645_add_scopes_to_pleroma_feo_auth_records.exs new file mode 100644 index 000000000..6b160ad16 --- /dev/null +++ b/priv/repo/migrations/20191220174645_add_scopes_to_pleroma_feo_auth_records.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Repo.Migrations.AddScopesToPleromaFEOAuthRecords do + use Ecto.Migration + + def up do + update_scopes_clause = "SET scopes = '{read,write,follow,push,admin}'" + apps_where = "WHERE apps.client_name like 'PleromaFE_%' or apps.client_name like 'AdminFE_%'" + app_id_subquery_where = "WHERE app_id IN (SELECT apps.id FROM apps #{apps_where})" + + execute("UPDATE apps #{update_scopes_clause} #{apps_where}") + + for table <- ["oauth_authorizations", "oauth_tokens"] do + execute("UPDATE #{table} #{update_scopes_clause} #{app_id_subquery_where}") + end + end + + def down, do: :noop +end diff --git a/priv/static/index.html b/priv/static/index.html index 2467aa22a..b0aadb1a1 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -Pleroma
\ No newline at end of file +Pleroma
\ No newline at end of file diff --git a/priv/static/static/font/fontello.1576166651574.woff b/priv/static/static/font/fontello.1576166651574.woff deleted file mode 100644 index bbffd6413..000000000 Binary files a/priv/static/static/font/fontello.1576166651574.woff and /dev/null differ diff --git a/priv/static/static/font/fontello.1576166651574.woff2 b/priv/static/static/font/fontello.1576166651574.woff2 deleted file mode 100644 index d35dce862..000000000 Binary files a/priv/static/static/font/fontello.1576166651574.woff2 and /dev/null differ diff --git a/priv/static/static/font/fontello.1576166651574.eot b/priv/static/static/font/fontello.1579102213354.eot similarity index 80% rename from priv/static/static/font/fontello.1576166651574.eot rename to priv/static/static/font/fontello.1579102213354.eot index fb27d4037..160cfa9f6 100644 Binary files a/priv/static/static/font/fontello.1576166651574.eot and b/priv/static/static/font/fontello.1579102213354.eot differ diff --git a/priv/static/static/font/fontello.1576166651574.svg b/priv/static/static/font/fontello.1579102213354.svg old mode 100755 new mode 100644 similarity index 89% rename from priv/static/static/font/fontello.1576166651574.svg rename to priv/static/static/font/fontello.1579102213354.svg index f5e497ce4..44beba9a2 --- a/priv/static/static/font/fontello.1576166651574.svg +++ b/priv/static/static/font/fontello.1579102213354.svg @@ -1,7 +1,7 @@ -Copyright (C) 2019 by original authors @ fontello.com +Copyright (C) 2020 by original authors @ fontello.com @@ -64,6 +64,18 @@ + + + + + + + + + + + + diff --git a/priv/static/static/font/fontello.1576166651574.ttf b/priv/static/static/font/fontello.1579102213354.ttf similarity index 80% rename from priv/static/static/font/fontello.1576166651574.ttf rename to priv/static/static/font/fontello.1579102213354.ttf index c49743ec6..44753f8c1 100644 Binary files a/priv/static/static/font/fontello.1576166651574.ttf and b/priv/static/static/font/fontello.1579102213354.ttf differ diff --git a/priv/static/static/font/fontello.1579102213354.woff b/priv/static/static/font/fontello.1579102213354.woff new file mode 100644 index 000000000..23351a090 Binary files /dev/null and b/priv/static/static/font/fontello.1579102213354.woff differ diff --git a/priv/static/static/font/fontello.1579102213354.woff2 b/priv/static/static/font/fontello.1579102213354.woff2 new file mode 100644 index 000000000..9c354e7f6 Binary files /dev/null and b/priv/static/static/font/fontello.1579102213354.woff2 differ diff --git a/priv/static/static/fontello.1576166651574.css b/priv/static/static/fontello.1579102213354.css similarity index 80% rename from priv/static/static/fontello.1576166651574.css rename to priv/static/static/fontello.1579102213354.css index 54f9fe05f..0f81954a5 100644 Binary files a/priv/static/static/fontello.1576166651574.css and b/priv/static/static/fontello.1579102213354.css differ diff --git a/priv/static/static/fontello.json b/priv/static/static/fontello.json index c0cf17271..829241b55 100755 --- a/priv/static/static/fontello.json +++ b/priv/static/static/fontello.json @@ -303,6 +303,42 @@ "css": "gauge", "code": 61668, "src": "fontawesome" + }, + { + "uid": "31972e4e9d080eaa796290349ae6c1fd", + "css": "users", + "code": 59421, + "src": "fontawesome" + }, + { + "uid": "e82cedfa1d5f15b00c5a81c9bd731ea2", + "css": "info-circled", + "code": 59423, + "src": "fontawesome" + }, + { + "uid": "w3nzesrlbezu6f30q7ytyq919p6gdlb6", + "css": "home-2", + "code": 59425, + "src": "typicons" + }, + { + "uid": "dcedf50ab1ede3283d7a6c70e2fe32f3", + "css": "chat", + "code": 59422, + "src": "fontawesome" + }, + { + "uid": "3a00327e61b997b58518bd43ed83c3df", + "css": "login", + "code": 59424, + "src": "fontawesome" + }, + { + "uid": "f3ebd6751c15a280af5cc5f4a764187d", + "css": "arrow-curved", + "code": 59426, + "src": "iconic" } ] } \ No newline at end of file diff --git a/priv/static/static/js/2.8896ea39a0ea8016391a.js b/priv/static/static/js/2.8896ea39a0ea8016391a.js new file mode 100644 index 000000000..ece883546 Binary files /dev/null and b/priv/static/static/js/2.8896ea39a0ea8016391a.js differ diff --git a/priv/static/static/js/2.8896ea39a0ea8016391a.js.map b/priv/static/static/js/2.8896ea39a0ea8016391a.js.map new file mode 100644 index 000000000..4a5dc5be7 Binary files /dev/null and b/priv/static/static/js/2.8896ea39a0ea8016391a.js.map differ diff --git a/priv/static/static/js/2.c96b30ae9f2d3f46f0ad.js b/priv/static/static/js/2.c96b30ae9f2d3f46f0ad.js deleted file mode 100644 index 910d304d3..000000000 Binary files a/priv/static/static/js/2.c96b30ae9f2d3f46f0ad.js and /dev/null differ diff --git a/priv/static/static/js/app.a43640742dacfb13b6b0.js b/priv/static/static/js/app.a43640742dacfb13b6b0.js new file mode 100644 index 000000000..82265996f Binary files /dev/null and b/priv/static/static/js/app.a43640742dacfb13b6b0.js differ diff --git a/priv/static/static/js/app.a43640742dacfb13b6b0.js.map b/priv/static/static/js/app.a43640742dacfb13b6b0.js.map new file mode 100644 index 000000000..b30f1ac4c Binary files /dev/null and b/priv/static/static/js/app.a43640742dacfb13b6b0.js.map differ diff --git a/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js b/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js deleted file mode 100644 index 124f284be..000000000 Binary files a/priv/static/static/js/app.a9b3f4c3e79baf3fa8b7.js and /dev/null differ diff --git a/priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js b/priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js deleted file mode 100644 index a64eee9a9..000000000 Binary files a/priv/static/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js and /dev/null differ diff --git a/priv/static/static/js/vendors~app.86bc6d5e06d2e17976c5.js b/priv/static/static/js/vendors~app.86bc6d5e06d2e17976c5.js new file mode 100644 index 000000000..0b8705ae8 Binary files /dev/null and b/priv/static/static/js/vendors~app.86bc6d5e06d2e17976c5.js differ diff --git a/priv/static/static/js/vendors~app.86bc6d5e06d2e17976c5.js.map b/priv/static/static/js/vendors~app.86bc6d5e06d2e17976c5.js.map new file mode 100644 index 000000000..98d62c3b1 Binary files /dev/null and b/priv/static/static/js/vendors~app.86bc6d5e06d2e17976c5.js.map differ diff --git a/priv/static/static/styles.json b/priv/static/static/styles.json index 842092c44..23508970d 100644 --- a/priv/static/static/styles.json +++ b/priv/static/static/styles.json @@ -1,6 +1,7 @@ { "pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ], "pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ], + "pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"], "classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ], "bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"], "ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ], diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js index 4738f3391..ae01a067e 100644 Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ diff --git a/priv/static/sw-pleroma.js.map b/priv/static/sw-pleroma.js.map index 1ac8d8676..5d9874693 100644 Binary files a/priv/static/sw-pleroma.js.map and b/priv/static/sw-pleroma.js.map differ diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 9cc534f57..59f4674eb 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -568,29 +568,34 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with describe "POST /oauth/authorize" do test "redirects with oauth authorization, " <> - "keeping only non-admin scopes for non-admin user" do - app = insert(:oauth_app, scopes: ["read", "write", "admin"]) + "granting requested app-supported scopes to both admin- and non-admin users" do + app_scopes = ["read", "write", "admin", "secret_scope"] + app = insert(:oauth_app, scopes: app_scopes) redirect_uri = OAuthController.default_redirect_uri(app) non_admin = insert(:user, is_admin: false) admin = insert(:user, is_admin: true) + scopes_subset = ["read:subscope", "write", "admin"] - for {user, expected_scopes} <- %{ - non_admin => ["read:subscope", "write"], - admin => ["read:subscope", "write", "admin"] - } do + # In case scope param is missing, expecting _all_ app-supported scopes to be granted + for user <- [non_admin, admin], + {requested_scopes, expected_scopes} <- + %{scopes_subset => scopes_subset, nil => app_scopes} do conn = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "scope" => "read:subscope write admin", - "state" => "statepassed" + post( + build_conn(), + "/oauth/authorize", + %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "scope" => requested_scopes, + "state" => "statepassed" + } } - }) + ) target = redirected_to(conn) assert target =~ redirect_uri @@ -631,34 +636,31 @@ test "returns 401 for wrong credentials", %{conn: conn} do assert result =~ "Invalid Username/Password" end - test "returns 401 for missing scopes " <> - "(including all admin-only scopes for non-admin user)" do + test "returns 401 for missing scopes" do user = insert(:user, is_admin: false) app = insert(:oauth_app, scopes: ["read", "write", "admin"]) redirect_uri = OAuthController.default_redirect_uri(app) - for scope_param <- ["", "admin:read admin:write"] do - result = - build_conn() - |> post("/oauth/authorize", %{ - "authorization" => %{ - "name" => user.nickname, - "password" => "test", - "client_id" => app.client_id, - "redirect_uri" => redirect_uri, - "state" => "statepassed", - "scope" => scope_param - } - }) - |> html_response(:unauthorized) + result = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => redirect_uri, + "state" => "statepassed", + "scope" => "" + } + }) + |> html_response(:unauthorized) - # Keep the details - assert result =~ app.client_id - assert result =~ redirect_uri + # Keep the details + assert result =~ app.client_id + assert result =~ redirect_uri - # Error message - assert result =~ "This action is outside the authorized scopes" - end + # Error message + assert result =~ "This action is outside the authorized scopes" end test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs index e1b484dae..8e76f2f3d 100644 --- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs @@ -14,6 +14,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do "emoji" ) + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + end + test "shared & non-shared pack information in list_packs is ok" do conn = build_conn() resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)