Skip to content

Commit 606ff61

Browse files
committed
chore: init writes on authorization providers (#1410)
Auth providers should be updated with the state of the application. This can happen after each transaction, effectively writing the `tenant` relationship also in the authorization level Signed-off-by: Luca Zaninotto <luca.zaninotto@secomind.com>
1 parent 0e8adeb commit 606ff61

6 files changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#
2+
# This file is part of Edgehog.
3+
#
4+
# Copyright 2026 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# SPDX-License-Identifier: Apache-2.0
19+
#
20+
21+
defmodule Edgehog.Auth.Changes.WriteTenant do
22+
@moduledoc """
23+
Generic tuple writer.
24+
"""
25+
26+
use Ash.Resource.Change
27+
28+
alias Edgehog.Auth.FGAService
29+
30+
require Logger
31+
32+
@impl Ash.Resource.Change
33+
def init(opts) do
34+
errors =
35+
if Keyword.has_key?(opts, :obj),
36+
do: [],
37+
else: [:obj_missing]
38+
39+
errors =
40+
if Keyword.has_key?(opts, :obj_id),
41+
do: errors,
42+
else: [:obj_id_missing | errors]
43+
44+
if Enum.empty?(errors),
45+
do: {:ok, opts},
46+
else: {:error, errors}
47+
end
48+
49+
@impl Ash.Resource.Change
50+
def change(changeset, opts, context) do
51+
Ash.Changeset.after_transaction(changeset, &write_tenant_tuple(&1, &2, opts, context))
52+
end
53+
54+
defp write_tenant_tuple(_changeset, {:ok, result}, opts, %{tenant: tenant}) do
55+
# Writes the tuple
56+
# {"tenant:slug", "tenant", "obj:obj_id"}
57+
58+
obj =
59+
opts
60+
|> Keyword.fetch!(:obj)
61+
|> to_string()
62+
63+
obj_id = opts[:obj_id]
64+
65+
obj_id =
66+
result
67+
|> Ash.load!(obj_id)
68+
|> Map.get(obj_id)
69+
|> to_string()
70+
71+
obj = "#{obj}:#{obj_id}"
72+
73+
rel = "tenant"
74+
75+
slug = tenant.slug
76+
subj = "tenant:#{slug}"
77+
78+
with {:ok, _res} <- FGAService.write(subj, rel, obj) do
79+
{:ok, result}
80+
end
81+
end
82+
83+
defp write_tenant_tuple(_changeset, error, opts, context) do
84+
Logger.debug("Error while executing DB transaction. Skipping writing tuple on the provider.",
85+
error: error,
86+
opts: opts,
87+
context: context
88+
)
89+
90+
error
91+
end
92+
end

backend/lib/edgehog/auth/fga_service.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ defmodule Edgehog.Auth.FGAService do
3838
end
3939
end
4040

41+
def write(subj, rel, obj) do
42+
tuple = {subj, rel, obj}
43+
44+
provider = Keyword.fetch!(Config.authz_config!(), :provider)
45+
config = Keyword.fetch!(Config.authz_config!(), :config)
46+
47+
with {:ok, context} <- provider.init_context(config) do
48+
provider.write(tuple, context)
49+
end
50+
end
51+
4152
def list_objects(subj, rel, type) do
4253
tuple = {subj, rel, type}
4354

backend/lib/edgehog/auth/providers/behaviour.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ defmodule Edgehog.Auth.Providers.Behaviour do
3535
"""
3636
@callback init_context(args :: Keyword.t()) :: {:ok, context()} | {:error, term()}
3737

38+
@doc """
39+
Writes a tuple to the provider.
40+
41+
- tuple :: the fga tuple, composed of `subject`, `relationship` and `object`
42+
- context :: the context from `init_context`
43+
44+
A write can return
45+
- {:ok, result} :: if the write was successful
46+
- {:error, error} :: if some error was encountered while writing
47+
"""
48+
@callback write(tuple :: fga_tuple(), context :: context()) :: {:ok, term()} | {:error, term()}
49+
3850
@doc """
3951
A check call checks a tuple against the provider
4052

backend/lib/edgehog/auth/providers/none.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,12 @@ defmodule Edgehog.Auth.Providers.None do
5555

5656
{:ok, :all}
5757
end
58+
59+
@impl Behaviour
60+
def write(tuple, _context) do
61+
Logger.debug("Writing tuple onto the service", tuple: tuple)
62+
63+
# Result actually not important here
64+
{:ok, nil}
65+
end
5866
end

backend/lib/edgehog/auth/providers/openfga.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,25 @@ defmodule Edgehog.Auth.Providers.OpenFGA do
8484

8585
Stub.streamed_list_objects(channel, request)
8686
end
87+
88+
@impl Behaviour
89+
def write({subj, rel, obj}, %{channel: channel, store_id: store_id}) do
90+
tuple = %Openfga.V1.TupleKey{
91+
user: subj,
92+
relation: rel,
93+
object: obj
94+
}
95+
96+
to_write = %Openfga.V1.WriteRequestWrites{
97+
tuple_keys: [tuple],
98+
on_duplicate: "ignore"
99+
}
100+
101+
request = %Openfga.V1.WriteRequest{
102+
store_id: store_id,
103+
writes: to_write
104+
}
105+
106+
Stub.write(channel, request)
107+
end
87108
end

backend/test/edgehog/auth/providers/openfga_integration_test.exs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,30 @@ defmodule Edgehog.Auth.Providers.OpenFGAIntegrationTests do
4545
assert ctx.store_id == config[:store_id]
4646
end
4747

48+
describe "write/2" do
49+
setup do
50+
config = Edgehog.Config.authz_config!()[:config]
51+
52+
{:ok, ctx} = OpenFGA.init_context(config)
53+
54+
%{context: ctx}
55+
end
56+
57+
test "Writes correct tuples", %{context: context} do
58+
opts = [
59+
subj_type: "user",
60+
subj_id: System.unique_integer([:positive]),
61+
rel: "owner",
62+
obj_type: "tenant",
63+
obj_id: "test"
64+
]
65+
66+
tuple = TupleFixtures.tuple(opts)
67+
68+
assert {:ok, _} = OpenFGA.write(tuple, context)
69+
end
70+
end
71+
4872
describe "check/2" do
4973
setup do
5074
config = Edgehog.Config.authz_config!()[:config]

0 commit comments

Comments
 (0)