Merge branch '1234-mastodon-2-4-3-oauth-scopes' into 'develop'

[#1234] Mastodon 2.4.3 hierarchical OAuth scopes

Closes #1234

See merge request pleroma/pleroma!1643
This commit is contained in:
Haelwenn 2019-10-07 02:14:45 +00:00
commit b4f3c16885
36 changed files with 721 additions and 267 deletions

View file

@ -5,24 +5,48 @@
defmodule Pleroma.Plugs.OAuthScopesPlugTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo
import Mock
import Pleroma.Factory
test "proceeds with no op if `assigns[:token]` is nil", %{conn: conn} do
conn =
conn
|> assign(:user, insert(:user))
|> OAuthScopesPlug.call(%{scopes: ["read"]})
refute conn.halted
assert conn.assigns[:user]
setup_with_mocks([{EnsurePublicOrAuthenticatedPlug, [], [call: fn conn, _ -> conn end]}]) do
:ok
end
test "proceeds with no op if `token.scopes` fulfill specified 'any of' conditions", %{
conn: conn
} do
describe "when `assigns[:token]` is nil, " do
test "with :skip_instance_privacy_check option, proceeds with no op", %{conn: conn} do
conn =
conn
|> assign(:user, insert(:user))
|> OAuthScopesPlug.call(%{scopes: ["read"], skip_instance_privacy_check: true})
refute conn.halted
assert conn.assigns[:user]
refute called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
end
test "without :skip_instance_privacy_check option, calls EnsurePublicOrAuthenticatedPlug", %{
conn: conn
} do
conn =
conn
|> assign(:user, insert(:user))
|> OAuthScopesPlug.call(%{scopes: ["read"]})
refute conn.halted
assert conn.assigns[:user]
assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
end
end
test "if `token.scopes` fulfills specified 'any of' conditions, " <>
"proceeds with no op",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
conn =
@ -35,9 +59,9 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
assert conn.assigns[:user]
end
test "proceeds with no op if `token.scopes` fulfill specified 'all of' conditions", %{
conn: conn
} do
test "if `token.scopes` fulfills specified 'all of' conditions, " <>
"proceeds with no op",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
conn =
@ -50,73 +74,154 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do
assert conn.assigns[:user]
end
test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill specified 'any of' conditions " <>
"and `fallback: :proceed_unauthenticated` option is specified",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
describe "with `fallback: :proceed_unauthenticated` option, " do
test "if `token.scopes` doesn't fulfill specified 'any of' conditions, " <>
"clears `assigns[:user]` and calls EnsurePublicOrAuthenticatedPlug",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: ["follow"], fallback: :proceed_unauthenticated})
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: ["follow"], fallback: :proceed_unauthenticated})
refute conn.halted
refute conn.assigns[:user]
refute conn.halted
refute conn.assigns[:user]
assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
end
test "if `token.scopes` doesn't fulfill specified 'all of' conditions, " <>
"clears `assigns[:user] and calls EnsurePublicOrAuthenticatedPlug",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{
scopes: ["read", "follow"],
op: :&,
fallback: :proceed_unauthenticated
})
refute conn.halted
refute conn.assigns[:user]
assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
end
test "with :skip_instance_privacy_check option, " <>
"if `token.scopes` doesn't fulfill specified conditions, " <>
"clears `assigns[:user]` and does not call EnsurePublicOrAuthenticatedPlug",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read:statuses", "write"]) |> Repo.preload(:user)
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{
scopes: ["read"],
fallback: :proceed_unauthenticated,
skip_instance_privacy_check: true
})
refute conn.halted
refute conn.assigns[:user]
refute called(EnsurePublicOrAuthenticatedPlug.call(conn, :_))
end
end
test "proceeds with cleared `assigns[:user]` if `token.scopes` doesn't fulfill specified 'all of' conditions " <>
"and `fallback: :proceed_unauthenticated` option is specified",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
describe "without :fallback option, " do
test "if `token.scopes` does not fulfill specified 'any of' conditions, " <>
"returns 403 and halts",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"])
any_of_scopes = ["follow"]
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{
scopes: ["read", "follow"],
op: :&,
fallback: :proceed_unauthenticated
})
conn =
conn
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: any_of_scopes})
refute conn.halted
refute conn.assigns[:user]
assert conn.halted
assert 403 == conn.status
expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}."
assert Jason.encode!(%{error: expected_error}) == conn.resp_body
end
test "if `token.scopes` does not fulfill specified 'all of' conditions, " <>
"returns 403 and halts",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"])
all_of_scopes = ["write", "follow"]
conn =
conn
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&})
assert conn.halted
assert 403 == conn.status
expected_error =
"Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}."
assert Jason.encode!(%{error: expected_error}) == conn.resp_body
end
end
test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'any of' conditions",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"])
any_of_scopes = ["follow"]
describe "with hierarchical scopes, " do
test "if `token.scopes` fulfills specified 'any of' conditions, " <>
"proceeds with no op",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
conn =
conn
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: any_of_scopes})
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: ["read:something"]})
assert conn.halted
assert 403 == conn.status
refute conn.halted
assert conn.assigns[:user]
end
expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}."
assert Jason.encode!(%{error: expected_error}) == conn.resp_body
test "if `token.scopes` fulfills specified 'all of' conditions, " <>
"proceeds with no op",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
conn =
conn
|> assign(:user, token.user)
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&})
refute conn.halted
assert conn.assigns[:user]
end
end
test "returns 403 and halts in case of no :fallback option and `token.scopes` not fulfilling specified 'all of' conditions",
%{conn: conn} do
token = insert(:oauth_token, scopes: ["read", "write"])
all_of_scopes = ["write", "follow"]
describe "filter_descendants/2" do
test "filters scopes which directly match or are ancestors of supported scopes" do
f = fn scopes, supported_scopes ->
OAuthScopesPlug.filter_descendants(scopes, supported_scopes)
end
conn =
conn
|> assign(:token, token)
|> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&})
assert f.(["read", "follow"], ["write", "read"]) == ["read"]
assert conn.halted
assert 403 == conn.status
assert f.(["read", "write:something", "follow"], ["write", "read"]) ==
["read", "write:something"]
expected_error =
"Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}."
assert f.(["admin:read"], ["write", "read"]) == []
assert Jason.encode!(%{error: expected_error}) == conn.resp_body
assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"]
end
end
end

View file

@ -324,6 +324,7 @@ defmodule Pleroma.Factory do
%Pleroma.Web.OAuth.Token{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
scopes: ["read"],
refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
user: build(:user),
app_id: oauth_app.id,

View file

@ -272,7 +272,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
assert user_response["pleroma"]["background_image"]
end
test "requires 'write' permission", %{conn: conn} do
test "requires 'write:accounts' permission", %{conn: conn} do
token1 = insert(:oauth_token, scopes: ["read"])
token2 = insert(:oauth_token, scopes: ["write", "follow"])
@ -283,7 +283,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
|> patch("/api/v1/accounts/update_credentials", %{})
if token == token1 do
assert %{"error" => "Insufficient permissions: write."} == json_response(conn, 403)
assert %{"error" => "Insufficient permissions: write:accounts."} ==
json_response(conn, 403)
else
assert json_response(conn, 200)
end

View file

@ -557,7 +557,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"scope" => "read write",
"scope" => "read:subscope write",
"state" => "statepassed"
}
})
@ -570,7 +570,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
assert %{"state" => "statepassed", "code" => code} = query
auth = Repo.get_by(Authorization, token: code)
assert auth
assert auth.scopes == ["read", "write"]
assert auth.scopes == ["read:subscope", "write"]
end
test "returns 401 for wrong credentials", %{conn: conn} do
@ -627,7 +627,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
assert result =~ "This action is outside the authorized scopes"
end
test "returns 401 for scopes beyond app scopes", %{conn: conn} do
test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app, scopes: ["read", "write"])
redirect_uri = OAuthController.default_redirect_uri(app)

View file

@ -81,19 +81,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
assert response == "job started"
end
test "requires 'follow' permission", %{conn: conn} do
test "requires 'follow' or 'write:follows' permissions", %{conn: conn} do
token1 = insert(:oauth_token, scopes: ["read", "write"])
token2 = insert(:oauth_token, scopes: ["follow"])
token3 = insert(:oauth_token, scopes: ["something"])
another_user = insert(:user)
for token <- [token1, token2] do
for token <- [token1, token2, token3] do
conn =
conn
|> put_req_header("authorization", "Bearer #{token.token}")
|> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"})
if token == token1 do
assert %{"error" => "Insufficient permissions: follow."} == json_response(conn, 403)
if token == token3 do
assert %{"error" => "Insufficient permissions: follow | write:follows."} ==
json_response(conn, 403)
else
assert json_response(conn, 200)
end