[#1682] Fixed Basic Auth permissions issue by disabling OAuth scopes checks when...
[akkoma] / test / plugs / oauth_scopes_plug_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Plugs.OAuthScopesPlugTest do
6 use Pleroma.Web.ConnCase, async: true
7
8 alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
9 alias Pleroma.Plugs.OAuthScopesPlug
10 alias Pleroma.Repo
11
12 import Mock
13 import Pleroma.Factory
14
15 setup_with_mocks([{EnsurePublicOrAuthenticatedPlug, [], [call: fn conn, _ -> conn end]}]) do
16 :ok
17 end
18
19 test "is not performed if marked as skipped", %{conn: conn} do
20 with_mock OAuthScopesPlug, [:passthrough], perform: &passthrough([&1, &2]) do
21 conn =
22 conn
23 |> OAuthScopesPlug.skip_plug()
24 |> OAuthScopesPlug.call(%{scopes: ["random_scope"]})
25
26 refute called(OAuthScopesPlug.perform(:_, :_))
27 refute conn.halted
28 end
29 end
30
31 test "if `token.scopes` fulfills specified 'any of' conditions, " <>
32 "proceeds with no op",
33 %{conn: conn} do
34 token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
35
36 conn =
37 conn
38 |> assign(:user, token.user)
39 |> assign(:token, token)
40 |> OAuthScopesPlug.call(%{scopes: ["read"]})
41
42 refute conn.halted
43 assert conn.assigns[:user]
44 end
45
46 test "if `token.scopes` fulfills specified 'all of' conditions, " <>
47 "proceeds with no op",
48 %{conn: conn} do
49 token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
50
51 conn =
52 conn
53 |> assign(:user, token.user)
54 |> assign(:token, token)
55 |> OAuthScopesPlug.call(%{scopes: ["scope2", "scope3"], op: :&})
56
57 refute conn.halted
58 assert conn.assigns[:user]
59 end
60
61 describe "with `fallback: :proceed_unauthenticated` option, " do
62 test "if `token.scopes` doesn't fulfill specified conditions, " <>
63 "clears :user and :token assigns and calls EnsurePublicOrAuthenticatedPlug",
64 %{conn: conn} do
65 user = insert(:user)
66 token1 = insert(:oauth_token, scopes: ["read", "write"], user: user)
67
68 for token <- [token1, nil], op <- [:|, :&] do
69 ret_conn =
70 conn
71 |> assign(:user, user)
72 |> assign(:token, token)
73 |> OAuthScopesPlug.call(%{
74 scopes: ["follow"],
75 op: op,
76 fallback: :proceed_unauthenticated
77 })
78
79 refute ret_conn.halted
80 refute ret_conn.assigns[:user]
81 refute ret_conn.assigns[:token]
82
83 assert called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
84 end
85 end
86
87 test "with :skip_instance_privacy_check option, " <>
88 "if `token.scopes` doesn't fulfill specified conditions, " <>
89 "clears :user and :token assigns and does NOT call EnsurePublicOrAuthenticatedPlug",
90 %{conn: conn} do
91 user = insert(:user)
92 token1 = insert(:oauth_token, scopes: ["read:statuses", "write"], user: user)
93
94 for token <- [token1, nil], op <- [:|, :&] do
95 ret_conn =
96 conn
97 |> assign(:user, user)
98 |> assign(:token, token)
99 |> OAuthScopesPlug.call(%{
100 scopes: ["read"],
101 op: op,
102 fallback: :proceed_unauthenticated,
103 skip_instance_privacy_check: true
104 })
105
106 refute ret_conn.halted
107 refute ret_conn.assigns[:user]
108 refute ret_conn.assigns[:token]
109
110 refute called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_))
111 end
112 end
113 end
114
115 describe "without :fallback option, " do
116 test "if `token.scopes` does not fulfill specified 'any of' conditions, " <>
117 "returns 403 and halts",
118 %{conn: conn} do
119 for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do
120 any_of_scopes = ["follow", "push"]
121
122 ret_conn =
123 conn
124 |> assign(:token, token)
125 |> OAuthScopesPlug.call(%{scopes: any_of_scopes})
126
127 assert ret_conn.halted
128 assert 403 == ret_conn.status
129
130 expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, " | ")}."
131 assert Jason.encode!(%{error: expected_error}) == ret_conn.resp_body
132 end
133 end
134
135 test "if `token.scopes` does not fulfill specified 'all of' conditions, " <>
136 "returns 403 and halts",
137 %{conn: conn} do
138 for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do
139 token_scopes = (token && token.scopes) || []
140 all_of_scopes = ["write", "follow"]
141
142 conn =
143 conn
144 |> assign(:token, token)
145 |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&})
146
147 assert conn.halted
148 assert 403 == conn.status
149
150 expected_error =
151 "Insufficient permissions: #{Enum.join(all_of_scopes -- token_scopes, " & ")}."
152
153 assert Jason.encode!(%{error: expected_error}) == conn.resp_body
154 end
155 end
156 end
157
158 describe "with hierarchical scopes, " do
159 test "if `token.scopes` fulfills specified 'any of' conditions, " <>
160 "proceeds with no op",
161 %{conn: conn} do
162 token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user)
163
164 conn =
165 conn
166 |> assign(:user, token.user)
167 |> assign(:token, token)
168 |> OAuthScopesPlug.call(%{scopes: ["read:something"]})
169
170 refute conn.halted
171 assert conn.assigns[:user]
172 end
173
174 test "if `token.scopes` fulfills specified 'all of' conditions, " <>
175 "proceeds with no op",
176 %{conn: conn} do
177 token = insert(:oauth_token, scopes: ["scope1", "scope2", "scope3"]) |> Repo.preload(:user)
178
179 conn =
180 conn
181 |> assign(:user, token.user)
182 |> assign(:token, token)
183 |> OAuthScopesPlug.call(%{scopes: ["scope1:subscope", "scope2:subscope"], op: :&})
184
185 refute conn.halted
186 assert conn.assigns[:user]
187 end
188 end
189
190 describe "filter_descendants/2" do
191 test "filters scopes which directly match or are ancestors of supported scopes" do
192 f = fn scopes, supported_scopes ->
193 OAuthScopesPlug.filter_descendants(scopes, supported_scopes)
194 end
195
196 assert f.(["read", "follow"], ["write", "read"]) == ["read"]
197
198 assert f.(["read", "write:something", "follow"], ["write", "read"]) ==
199 ["read", "write:something"]
200
201 assert f.(["admin:read"], ["write", "read"]) == []
202
203 assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"]
204 end
205 end
206
207 describe "transform_scopes/2" do
208 clear_config([:auth, :enforce_oauth_admin_scope_usage])
209
210 setup do
211 {:ok, %{f: &OAuthScopesPlug.transform_scopes/2}}
212 end
213
214 test "with :admin option, prefixes all requested scopes with `admin:` " <>
215 "and [optionally] keeps only prefixed scopes, " <>
216 "depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting",
217 %{f: f} do
218 Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
219
220 assert f.(["read"], %{admin: true}) == ["admin:read", "read"]
221
222 assert f.(["read", "write"], %{admin: true}) == [
223 "admin:read",
224 "read",
225 "admin:write",
226 "write"
227 ]
228
229 Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
230
231 assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"]
232
233 assert f.(["read", "write:reports"], %{admin: true}) == [
234 "admin:read",
235 "admin:write:reports"
236 ]
237 end
238
239 test "with no supported options, returns unmodified scopes", %{f: f} do
240 assert f.(["read"], %{}) == ["read"]
241 assert f.(["read", "write"], %{}) == ["read", "write"]
242 end
243 end
244 end