Skip to content

Commit c3f9bf8

Browse files
committed
feat(backend): init OpenFGA provider
Adds necessary callbacks and functions to the fga service. These callbacks are necessary for - handling singel checks - handling syncronous and streamed filter Adds initial support for an `OpenFGA` provider for authentication. Signed-off-by: Luca Zaninotto <luca.zaninotto@secomind.com>
1 parent 25b11cd commit c3f9bf8

10 files changed

Lines changed: 222 additions & 12 deletions

File tree

backend/.credo.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"apps/*/test/",
3535
"apps/*/web/"
3636
],
37-
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
37+
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/", ~r/openfga/]
3838
},
3939
#
4040
# Load and configure plugins here:

backend/lib/edgehog/application.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ defmodule Edgehog.Application do
5252
EdgehogWeb.Telemetry,
5353
# Start the PubSub system
5454
{Phoenix.PubSub, name: Edgehog.PubSub},
55+
# gRPC connections batcher
56+
{GRPC.Client.Supervisor, []},
5557
# Ash GraphQL subscription batcher
5658
AshGraphql.Subscription.Batcher,
5759
# Start Finch

backend/lib/edgehog/auth/fga_service.ex

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,26 @@ defmodule Edgehog.Auth.FGAService do
3131
tuple = {subj, rel, obj}
3232

3333
provider = Keyword.fetch!(Config.authz_config!(), :provider)
34+
config = Keyword.fetch!(Config.authz_config!(), :config)
3435

35-
provider.check(tuple, provider.init_context())
36+
provider.check(tuple, provider.init_context(config))
37+
end
38+
39+
def list_objects(subj, rel, type) do
40+
tuple = {subj, rel, type}
41+
42+
provider = Keyword.fetch!(Config.authz_config!(), :provider)
43+
config = Keyword.fetch!(Config.authz_config!(), :config)
44+
45+
provider.list_objects(tuple, provider.init_context(config))
46+
end
47+
48+
def stream_list_objects(subj, rel, type) do
49+
tuple = {subj, rel, type}
50+
51+
provider = Keyword.fetch!(Config.authz_config!(), :provider)
52+
config = Keyword.fetch!(Config.authz_config!(), :config)
53+
54+
provider.stream_list_objects(tuple, provider.init_context(config))
3655
end
3756
end

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

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ defmodule Edgehog.Auth.Providers.Behaviour do
2525

2626
@type context() :: term()
2727
@type fga_tuple() :: {subj :: String.t(), rel :: String.t(), obj :: String.t()}
28+
@type fga_access_tuple() :: {subj :: String.t(), rel :: String.t(), type :: String.t()}
29+
@type objects() :: %{objects: term()}
30+
@type obj_stream() :: term()
2831

2932
@doc """
3033
The context initialization function. The context can carry useful information for subsequent actions.
@@ -42,11 +45,51 @@ defmodule Edgehog.Auth.Providers.Behaviour do
4245
The context is also provided.
4346
4447
The check can return
45-
- {:ok, context()} :: meaning that the subject has the right permission to access the resource
46-
- {:notok, context()} :: meaning that the subject has not the right permission to access the resource
47-
- {:error, error} :: meaning that there was some error in the request.
48+
- :ok :: meaning that the subject has the right permission to access the resource
49+
- :notok :: meaning that the subject has not the right permission to access the resource
50+
- {:error, error} :: meaning that there was some error in the request.
4851
4952
For successful returns the new `context` should be provided.
5053
"""
5154
@callback check(tuple :: fga_tuple(), context :: context()) :: :ok | :notok | {:error, term()}
55+
56+
@doc """
57+
A list_objects call lists all objects of the selected type a user has access to.
58+
59+
- subj :: is some id of the person making the request, ideally a OpenID Connect UUID
60+
- rel :: is the requested access to the resource (object) in order to perform the action
61+
- type :: is the type of resources we want to fetch
62+
63+
The context should also be provided.
64+
65+
NOTICE: This creates a list with all the objects ! This might be very
66+
inefficent an d possibly stalls the request (e.g. 1mln devices), consider
67+
using `stream_list_objects`
68+
69+
The call can return
70+
- {:ok, objects()} :: meaning that the user has access to the %{objects: list()} list of objects of type `type`
71+
- {:error, error} :: meaning that there was some error in the request.
72+
73+
For successful returns the new `context` should be provided.
74+
"""
75+
@callback list_objects(tuple :: fga_access_tuple(), context :: context()) ::
76+
{:ok, objects()} | {:ok, :all} | {:error, term()}
77+
78+
@doc """
79+
A stream_list_objects call streams all objects of the selected type a user has access to.
80+
81+
- subj :: is some id of the person making the request, ideally a OpenID Connect UUID
82+
- rel :: is the requested access to the resource (object) in order to perform the action
83+
- type :: is the type of resources we want to fetch
84+
85+
The context should also be provided.
86+
87+
The call can return
88+
- {:ok, stream(objects())} :: meaning that the user has access to the %{objects: list()} list of objects of type `type`
89+
- {:error, error} :: meaning that there was some error in the request.
90+
91+
For successful returns the new `context` should be provided.
92+
"""
93+
@callback stream_list_objects(tuple :: fga_access_tuple(), context :: context()) ::
94+
{:ok, obj_stream()} | {:ok, :all} | {:error, term()}
5295
end

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,34 @@ defmodule Edgehog.Auth.Providers.None do
2525

2626
@behaviour Edgehog.Auth.Providers.Behaviour
2727

28+
alias Edgehog.Auth.Providers.Behaviour
29+
2830
require Logger
2931

30-
@impl Edgehog.Auth.Providers.Behaviour
32+
@impl Behaviour
3133
def init_context(_args) do
3234
# We do not need a context in this case
3335
{:ok, []}
3436
end
3537

36-
@impl Edgehog.Auth.Providers.Behaviour
38+
@impl Behaviour
3739
def check({subj, rel, obj}, _context) do
3840
Logger.debug("Authorizing tuple {#{subj}, #{rel}, #{obj}}.")
3941

4042
:ok
4143
end
44+
45+
@impl Behaviour
46+
def list_objects({subj, rel, type}, _context) do
47+
Logger.debug("Filtering objects for: {#{subj}, #{rel}, #{type}}.")
48+
49+
{:ok, :all}
50+
end
51+
52+
@impl Behaviour
53+
def stream_list_objects({subj, rel, type}, _context) do
54+
Logger.debug("Filtering objects for: {#{subj}, #{rel}, #{type}}.")
55+
56+
{:ok, :all}
57+
end
4258
end
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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.Providers.OpenFGA do
22+
@moduledoc """
23+
OpenFGA Auth provider.
24+
25+
This provider queries OpenFGA based on some model description to check tuples.
26+
"""
27+
28+
@behaviour Edgehog.Auth.Providers.Behaviour
29+
30+
alias Edgehog.Auth.Providers.Behaviour
31+
alias Openfga.V1.OpenFGAService.Stub
32+
33+
@impl Behaviour
34+
def init_context(args) do
35+
ctx = Map.new(args)
36+
37+
{endpoint, ctx} = Map.pop!(ctx, :endpoint)
38+
39+
with {:ok, channel} <- GRPC.Stub.connect(endpoint) do
40+
ctx = Map.put(ctx, :channel, channel)
41+
42+
{:ok, ctx}
43+
end
44+
end
45+
46+
@impl Behaviour
47+
def check({subj, rel, obj}, %{channel: channel, store_id: store_id}) do
48+
tuple = %Openfga.V1.CheckRequestTupleKey{
49+
user: subj,
50+
relation: rel,
51+
object: obj
52+
}
53+
54+
request = %Openfga.V1.CheckRequest{
55+
store_id: store_id,
56+
tuple_key: tuple
57+
}
58+
59+
case Stub.check(channel, request) do
60+
{:ok, %{allowed: true}} -> :ok
61+
{:ok, %{allowed: false}} -> :notok
62+
error -> error
63+
end
64+
end
65+
66+
@impl Behaviour
67+
def list_objects({subj, rel, type}, %{channel: channel, store_id: store_id}) do
68+
request = %Openfga.V1.ListObjectsRequest{
69+
store_id: store_id,
70+
type: type,
71+
relation: rel,
72+
user: subj
73+
}
74+
75+
Stub.list_objects(channel, request)
76+
end
77+
78+
@impl Behaviour
79+
def stream_list_objects({subj, rel, type}, %{channel: channel, store_id: store_id}) do
80+
request = %Openfga.V1.StreamedListObjectsRequest{
81+
store_id: store_id,
82+
type: type,
83+
relation: rel,
84+
user: subj
85+
}
86+
87+
Stub.streamed_list_objects(channel, request)
88+
end
89+
end

backend/lib/edgehog/config.ex

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ defmodule Edgehog.Config do
2424
"""
2525
use Skogsra
2626

27+
alias Edgehog.Auth.Providers.None
2728
alias Edgehog.Config.AuthzProvider
2829
alias Edgehog.Config.GeocodingProviders
2930
alias Edgehog.Config.GeolocationProviders
@@ -104,7 +105,23 @@ defmodule Edgehog.Config do
104105
app_env :authz_provider, :edgehog, :authz_provider,
105106
os_env: "AUTHZ_PROVIDER",
106107
type: AuthzProvider,
107-
default: Edgehog.Auth.Providers.None
108+
default: None
109+
110+
@envdoc """
111+
OpenFGA gRPC endpoint.
112+
Defaults to `localhost:8081`
113+
"""
114+
app_env :openfga_grpc_endpoint, :edgehog, :openfga_grpc_endpoint,
115+
os_env: "OPENFGA_GRPC_ENDPOINT",
116+
type: :binary,
117+
default: "localhost:8081"
118+
119+
@envdoc """
120+
OpenFGA store id. A store in OpenFGA has a unique id associated.
121+
"""
122+
app_env :openfga_store_id, :edgehog, :openfga_store_id,
123+
os_env: "OPENFGA_STORE_ID",
124+
type: :binary
108125

109126
@doc """
110127
Returns true if edgehog should use an ssl connection with the database.
@@ -201,14 +218,32 @@ defmodule Edgehog.Config do
201218
:ok
202219
end
203220

221+
@doc """
222+
Returns the config of the required provider
223+
"""
224+
@spec authz_provider_config!(provider :: module()) :: Keyword.t()
225+
def authz_provider_config!(provider)
226+
227+
def authz_provider_config!(None), do: Keyword.new()
228+
229+
def authz_provider_config!(Edgehog.Auth.Providers.OpenFGA) do
230+
[
231+
endpoint: openfga_grpc_endpoint!(),
232+
store: openfga_store_id!()
233+
]
234+
end
235+
204236
@doc """
205237
Returns the FGA provider configuration.
206238
"""
207239
@spec authz_config!() :: Keyword.t()
208240
def authz_config! do
209241
provider = authz_provider!()
242+
config = authz_provider_config!(provider)
210243

211244
# Generate the argument that will be passed down to the providers
212-
Keyword.put(Keyword.new(), :provider, provider)
245+
Keyword.new()
246+
|> Keyword.put(:provider, provider)
247+
|> Keyword.put(:config, config)
213248
end
214249
end

backend/lib/edgehog/config/authz_provider.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ defmodule Edgehog.Config.AuthzProvider do
2222
@moduledoc false
2323
use Skogsra.Type
2424

25-
@allowed_providers ~w(none)
26-
@providers [Edgehog.Auth.Providers.None]
25+
@allowed_providers ~w(none openfga)
26+
@providers [Edgehog.Auth.Providers.None, Edgehog.Auth.Providers.OpenFGA]
2727
@providers_map @allowed_providers |> Enum.zip(@providers) |> Map.new()
2828

2929
@impl Skogsra.Type

backend/mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ defmodule Edgehog.MixProject do
135135
{:absinthe_phoenix, "~> 2.0"},
136136
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
137137
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
138-
{:ex_doc, "~> 0.40", only: :dev}
138+
{:ex_doc, "~> 0.40", only: :dev},
139+
{:grpc, "~> 0.11"}
139140
]
140141
end
141142

backend/mix.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,18 @@
3838
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
3939
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
4040
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
41+
"flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"},
42+
"gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"},
4143
"gen_state_machine": {:hex, :gen_state_machine, "3.0.0", "1e57f86a494e5c6b14137ebef26a7eb342b3b0070c7135f2d6768ed3f6b6cdff", [:mix], [], "hexpm", "0a59652574bebceb7309f6b749d2a41b45fdeda8dbb4da0791e355dd19f0ed15"},
4244
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
4345
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
4446
"google_api_storage": {:hex, :google_api_storage, "0.46.1", "f944e40828b7fd90d00cef1ddb9ac542acb88adb18d6bc7c2015bafbd696eb6c", [:mix], [{:google_gax, "~> 0.4", [hex: :google_gax, repo: "hexpm", optional: false]}], "hexpm", "a6e162017b51bf1d80ae9d0e5cd47c8a3d246d52d9f025c9ca5e2eb71b1b6f75"},
4547
"google_gax": {:hex, :google_gax, "0.4.0", "83651f8561c02a295826cb96b4bddde030e2369747bbddc592c4569526bafe94", [:mix], [{:poison, ">= 3.0.0 and < 5.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "a95d36f1dd753ab31268dd8bb6de9911243c911cfda9080f64778f6297b9ac57"},
48+
"googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"},
4649
"goth": {:hex, :goth, "1.4.5", "ee37f96e3519bdecd603f20e7f10c758287088b6d77c0147cd5ee68cf224aade", [:mix], [{:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "0fc2dce5bd710651ed179053d0300ce3a5d36afbdde11e500d57f05f398d5ed5"},
50+
"grpc": {:hex, :grpc, "0.11.5", "5dbde9420718b58712779ad98fff1ef50349ca0fa7cc0858ae0f826015068654", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:cowboy, "~> 2.10", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowlib, "~> 2.12", [hex: :cowlib, repo: "hexpm", optional: false]}, {:flow, "~> 1.2", [hex: :flow, repo: "hexpm", optional: false]}, {:googleapis, "~> 0.1.0", [hex: :googleapis, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.14", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0a5d8673ef16649bef0903bca01c161acfc148e4d269133b6834b2af1f07f45e"},
4751
"guardian": {:hex, :guardian, "2.4.0", "efbbb397ecca881bb548560169922fc4433a05bc98c2eb96a7ed88ede9e17d64", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "5c80103a9c538fbc2505bf08421a82e8f815deba9eaedb6e734c66443154c518"},
52+
"gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
4853
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
4954
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
5055
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},

0 commit comments

Comments
 (0)