Skip to content

Commit 5190510

Browse files
authored
fix: adds sub and role into authorization context (#1503)
1 parent 3c0c0cb commit 5190510

File tree

9 files changed

+264
-185
lines changed

9 files changed

+264
-185
lines changed

lib/realtime/tenants/authorization.ex

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,27 @@ defmodule Realtime.Tenants.Authorization do
1818
alias Realtime.GenRpc
1919
alias Realtime.Tenants.Authorization.Policies
2020

21-
defstruct [:tenant_id, :topic, :headers, :jwt, :claims, :role]
21+
defstruct [:tenant_id, :topic, :headers, :jwt, :claims, :role, :sub]
2222

2323
@type t :: %__MODULE__{
2424
:tenant_id => binary | nil,
2525
:topic => binary | nil,
2626
:claims => map,
2727
:headers => list({binary, binary}),
28-
:role => binary
28+
:role => binary,
29+
:sub => binary | nil
2930
}
3031

3132
@doc """
3233
Builds a new authorization struct which will be used to retain the information required to check Policies.
3334
3435
Requires a map with the following keys:
36+
* tenant_id: The tenant id
3537
* topic: The name of the channel being accessed taken from the request
36-
* headers: Request headers when the connection was made or WS was updated
38+
* headers: Request headers when the connection was made or WS was upgraded
3739
* claims: JWT claims
38-
* role: JWT role
40+
* role: JWT role claim
41+
* sub: JWT sub claim
3942
"""
4043
@spec build_authorization_params(map()) :: t()
4144
def build_authorization_params(map) do
@@ -44,7 +47,8 @@ defmodule Realtime.Tenants.Authorization do
4447
topic: Map.get(map, :topic),
4548
headers: Map.get(map, :headers),
4649
claims: Map.get(map, :claims),
47-
role: Map.get(map, :role)
50+
role: Map.get(map, :role),
51+
sub: Map.get(map, :sub)
4852
}
4953
end
5054

@@ -123,29 +127,31 @@ defmodule Realtime.Tenants.Authorization do
123127
* request.jwt.claims: The claims of the JWT token
124128
* request.headers: The headers of the request
125129
"""
126-
@spec set_conn_config(DBConnection.t(), t()) ::
127-
{:ok, Postgrex.Result.t()} | {:error, Exception.t()}
130+
@spec set_conn_config(DBConnection.t(), t()) :: Postgrex.Result.t()
128131
def set_conn_config(conn, authorization_context) do
129132
%__MODULE__{
130133
topic: topic,
131134
headers: headers,
132135
claims: claims,
133-
role: role
136+
role: role,
137+
sub: sub
134138
} = authorization_context
135139

136140
claims = Jason.encode!(claims)
137141
headers = headers |> Map.new() |> Jason.encode!()
138142

139-
Postgrex.query(
143+
Postgrex.query!(
140144
conn,
141145
"""
142146
SELECT
143-
set_config('role', $1, true),
144-
set_config('realtime.topic', $2, true),
145-
set_config('request.jwt.claims', $3, true),
146-
set_config('request.headers', $4, true)
147+
set_config('role', $1, true),
148+
set_config('realtime.topic', $2, true),
149+
set_config('request.jwt.claims', $3, true),
150+
set_config('request.jwt.claim.sub', $4, true),
151+
set_config('request.jwt.claim.role', $5, true),
152+
set_config('request.headers', $6, true)
147153
""",
148-
[role, topic, claims, headers]
154+
[role, topic, claims, sub, role, headers]
149155
)
150156
end
151157

lib/realtime/tenants/batch_broadcast.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ defmodule Realtime.Tenants.BatchBroadcast do
3939
tenant_id: tenant.external_id,
4040
headers: conn.req_headers,
4141
claims: conn.assigns.claims,
42-
role: conn.assigns.role
42+
role: conn.assigns.role,
43+
sub: conn.assigns.sub
4344
}
4445

4546
broadcast(auth_params, %Tenant{} = tenant, messages, super_user)

lib/realtime_web/channels/realtime_channel.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,8 @@ defmodule RealtimeWeb.RealtimeChannel do
715715
topic: topic,
716716
headers: Map.get(socket.assigns, :headers, []),
717717
claims: claims,
718-
role: claims["role"]
718+
role: claims["role"],
719+
sub: claims["sub"]
719720
})
720721

721722
assign(socket, :authorization_context, authorization_context)

lib/realtime_web/plugs/auth_tenant.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule RealtimeWeb.AuthTenant do
2525
|> assign(:claims, claims)
2626
|> assign(:jwt, token)
2727
|> assign(:role, claims["role"])
28+
|> assign(:sub, claims["sub"])
2829
else
2930
_error -> unauthorized(conn)
3031
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule Realtime.MixProject do
44
def project do
55
[
66
app: :realtime,
7-
version: "2.43.0",
7+
version: "2.43.1",
88
elixir: "~> 1.17.3",
99
elixirc_paths: elixirc_paths(Mix.env()),
1010
start_permanent: Mix.env() == :prod,

test/integration/rt_channel_test.exs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1864,6 +1864,45 @@ defmodule Realtime.Integration.RtChannelTest do
18641864
describe "authorization handling" do
18651865
setup [:rls_context]
18661866

1867+
@tag policies: [:read_matching_user_role, :write_matching_user_role], role: "anon"
1868+
test "role policies are respected when accessing the channel", %{tenant: tenant} do
1869+
{socket, _} = get_connection(tenant, "anon")
1870+
config = %{broadcast: %{self: true}, private: true, presence: %{enabled: false}}
1871+
topic = random_string()
1872+
realtime_topic = "realtime:#{topic}"
1873+
1874+
WebsocketClient.join(socket, realtime_topic, %{config: config})
1875+
1876+
assert_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}, topic: ^realtime_topic}, 500
1877+
1878+
{socket, _} = get_connection(tenant, "potato")
1879+
topic = random_string()
1880+
realtime_topic = "realtime:#{topic}"
1881+
1882+
WebsocketClient.join(socket, realtime_topic, %{config: config})
1883+
refute_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}, topic: ^realtime_topic}, 500
1884+
end
1885+
1886+
@tag policies: [:authenticated_read_matching_user_sub, :authenticated_write_matching_user_sub],
1887+
sub: Ecto.UUID.generate()
1888+
test "sub policies are respected when accessing the channel", %{tenant: tenant, sub: sub} do
1889+
{socket, _} = get_connection(tenant, "authenticated", %{sub: sub})
1890+
config = %{broadcast: %{self: true}, private: true, presence: %{enabled: false}}
1891+
topic = random_string()
1892+
realtime_topic = "realtime:#{topic}"
1893+
1894+
WebsocketClient.join(socket, realtime_topic, %{config: config})
1895+
1896+
assert_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}, topic: ^realtime_topic}, 500
1897+
1898+
{socket, _} = get_connection(tenant, "authenticated", %{sub: Ecto.UUID.generate()})
1899+
topic = random_string()
1900+
realtime_topic = "realtime:#{topic}"
1901+
1902+
WebsocketClient.join(socket, realtime_topic, %{config: config})
1903+
refute_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}, topic: ^realtime_topic}, 500
1904+
end
1905+
18671906
@tag role: "authenticated",
18681907
policies: [:broken_read_presence, :broken_write_presence]
18691908

@@ -2305,10 +2344,13 @@ defmodule Realtime.Integration.RtChannelTest do
23052344
{:ok, db_conn} = Database.connect(tenant, "realtime_test", :stop)
23062345
clean_table(db_conn, "realtime", "messages")
23072346
topic = Map.get(context, :topic, random_string())
2347+
policies = Map.get(context, :policies, nil)
2348+
role = Map.get(context, :role, nil)
2349+
sub = Map.get(context, :sub, nil)
23082350

2309-
if policies = context[:policies], do: create_rls_policies(db_conn, policies, %{topic: topic})
2351+
if policies, do: create_rls_policies(db_conn, policies, %{topic: topic, role: role, sub: sub})
23102352

2311-
%{topic: topic}
2353+
%{topic: topic, role: role, sub: sub}
23122354
end
23132355

23142356
defp setup_trigger(%{tenant: tenant, topic: topic}) do

test/realtime/tenants/authorization_test.exs

Lines changed: 47 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -23,44 +23,57 @@ defmodule Realtime.Tenants.AuthorizationTest do
2323
]
2424
test "authenticated user has expected policies", context do
2525
{:ok, policies} =
26-
Authorization.get_read_authorizations(
27-
%Policies{},
28-
context.db_conn,
29-
context.authorization_context
30-
)
26+
Authorization.get_read_authorizations(%Policies{}, context.db_conn, context.authorization_context)
3127

3228
{:ok, policies} =
33-
Authorization.get_write_authorizations(
34-
policies,
35-
context.db_conn,
36-
context.authorization_context
37-
)
29+
Authorization.get_write_authorizations(policies, context.db_conn, context.authorization_context)
3830

3931
assert %Policies{
4032
broadcast: %BroadcastPolicies{read: true, write: true},
4133
presence: %PresencePolicies{read: true, write: true}
4234
} == policies
4335
end
4436

37+
@tag role: "authenticated",
38+
policies: [:authenticated_read_matching_user_sub],
39+
sub: "ccbdfd51-c5aa-4d61-8c17-647664466a26"
40+
test "authenticated user sub is available", context do
41+
assert {:ok, %Policies{broadcast: %BroadcastPolicies{read: true, write: nil}}} =
42+
Authorization.get_read_authorizations(%Policies{}, context.db_conn, context.authorization_context)
43+
44+
authorization_context = %{context.authorization_context | sub: "135f6d25-5840-4266-a8ca-b9a45960e424"}
45+
46+
assert {:ok, %Policies{broadcast: %BroadcastPolicies{read: false, write: nil}}} =
47+
Authorization.get_read_authorizations(%Policies{}, context.db_conn, authorization_context)
48+
end
49+
50+
@tag role: "authenticated",
51+
policies: [:read_matching_user_role]
52+
test "user role is exposed", context do
53+
# policy role is checking for "authenticated"
54+
# set_config is setting request.jwt.claim.role to authenticated as well
55+
assert {:ok, %Policies{broadcast: %BroadcastPolicies{read: true, write: nil}}} =
56+
Authorization.get_read_authorizations(%Policies{}, context.db_conn, context.authorization_context)
57+
58+
authorization_context = %{context.authorization_context | role: "anon"}
59+
60+
# policy role is checking for "authenticated"
61+
# set_config is setting request.jwt.claim.role to anon
62+
assert {:ok, %Policies{broadcast: %BroadcastPolicies{read: false, write: nil}}} =
63+
Authorization.get_read_authorizations(%Policies{}, context.db_conn, authorization_context)
64+
end
65+
4566
@tag role: "anon",
4667
policies: [
4768
:authenticated_read_broadcast_and_presence,
4869
:authenticated_write_broadcast_and_presence
4970
]
5071
test "anon user has no policies", context do
5172
{:ok, policies} =
52-
Authorization.get_read_authorizations(
53-
%Policies{},
54-
context.db_conn,
55-
context.authorization_context
56-
)
73+
Authorization.get_read_authorizations(%Policies{}, context.db_conn, context.authorization_context)
5774

5875
{:ok, policies} =
59-
Authorization.get_write_authorizations(
60-
policies,
61-
context.db_conn,
62-
context.authorization_context
63-
)
76+
Authorization.get_write_authorizations(policies, context.db_conn, context.authorization_context)
6477

6578
assert %Policies{
6679
broadcast: %BroadcastPolicies{read: false, write: false},
@@ -119,39 +132,19 @@ defmodule Realtime.Tenants.AuthorizationTest do
119132
policies: [:broken_read_presence, :broken_write_presence]
120133
test "broken RLS policy sets policies to false and shows error to user", context do
121134
assert {:error, :rls_policy_error, %Postgrex.Error{}} =
122-
Authorization.get_read_authorizations(
123-
%Policies{},
124-
context.db_conn,
125-
context.authorization_context
126-
)
135+
Authorization.get_read_authorizations(%Policies{}, context.db_conn, context.authorization_context)
127136

128137
assert {:error, :rls_policy_error, %Postgrex.Error{}} =
129-
Authorization.get_write_authorizations(
130-
%Policies{},
131-
context.db_conn,
132-
context.authorization_context
133-
)
138+
Authorization.get_write_authorizations(%Policies{}, context.db_conn, context.authorization_context)
134139

135140
assert {:error, :rls_policy_error, %Postgrex.Error{}} =
136-
Authorization.get_read_authorizations(
137-
%Policies{},
138-
context.db_conn,
139-
context.authorization_context
140-
)
141+
Authorization.get_read_authorizations(%Policies{}, context.db_conn, context.authorization_context)
141142

142143
assert {:error, :rls_policy_error, %Postgrex.Error{}} =
143-
Authorization.get_write_authorizations(
144-
%Policies{},
145-
context.db_conn,
146-
context.authorization_context
147-
)
144+
Authorization.get_write_authorizations(%Policies{}, context.db_conn, context.authorization_context)
148145

149146
assert {:error, :rls_policy_error, %Postgrex.Error{}} =
150-
Authorization.get_write_authorizations(
151-
%Policies{},
152-
context.db_conn,
153-
context.authorization_context
154-
)
147+
Authorization.get_write_authorizations(%Policies{}, context.db_conn, context.authorization_context)
155148
end
156149
end
157150

@@ -162,19 +155,8 @@ defmodule Realtime.Tenants.AuthorizationTest do
162155
:authenticated_write_broadcast_and_presence
163156
]
164157
test "authenticated user has expected policies", context do
165-
{:ok, _} =
166-
Authorization.get_read_authorizations(
167-
%Policies{},
168-
context.db_conn,
169-
context.authorization_context
170-
)
171-
172-
{:ok, _} =
173-
Authorization.get_write_authorizations(
174-
%Policies{},
175-
context.db_conn,
176-
context.authorization_context
177-
)
158+
{:ok, _} = Authorization.get_read_authorizations(%Policies{}, context.db_conn, context.authorization_context)
159+
{:ok, _} = Authorization.get_write_authorizations(%Policies{}, context.db_conn, context.authorization_context)
178160

179161
{:ok, db_conn} = Database.connect(context.tenant, "realtime_test")
180162
assert {:ok, []} = Repo.all(db_conn, Message, Message)
@@ -205,19 +187,8 @@ defmodule Realtime.Tenants.AuthorizationTest do
205187
%{}
206188
)
207189

208-
{:ok, _} =
209-
Authorization.get_read_authorizations(
210-
%Policies{},
211-
context.db_conn,
212-
context.authorization_context
213-
)
214-
215-
{:ok, _} =
216-
Authorization.get_write_authorizations(
217-
%Policies{},
218-
context.db_conn,
219-
context.authorization_context
220-
)
190+
{:ok, _} = Authorization.get_read_authorizations(%Policies{}, context.db_conn, context.authorization_context)
191+
{:ok, _} = Authorization.get_write_authorizations(%Policies{}, context.db_conn, context.authorization_context)
221192

222193
external_id = context.authorization_context.tenant_id
223194

@@ -232,19 +203,20 @@ defmodule Realtime.Tenants.AuthorizationTest do
232203
def rls_context(context) do
233204
tenant = Containers.checkout_tenant(run_migrations: true)
234205
{:ok, db_conn} = Database.connect(tenant, "realtime_test", :stop)
235-
topic = random_string()
206+
topic = context[:topic] || random_string()
236207

237-
create_rls_policies(db_conn, context.policies, %{topic: topic})
208+
create_rls_policies(db_conn, context.policies, %{topic: topic, sub: context[:sub], role: context.role})
238209

239-
claims = %{sub: random_string(), role: context.role, exp: Joken.current_time() + 1_000}
210+
claims = %{"sub" => context[:sub] || random_string(), "role" => context.role, "exp" => Joken.current_time() + 1_000}
240211

241212
authorization_context =
242213
Authorization.build_authorization_params(%{
243214
tenant_id: tenant.external_id,
244215
topic: topic,
245216
claims: claims,
246217
headers: [{"header-1", "value-1"}],
247-
role: claims.role
218+
role: claims["role"],
219+
sub: claims["sub"]
248220
})
249221

250222
Realtime.Tenants.Migrations.create_partitions(db_conn)

0 commit comments

Comments
 (0)