Skip to content

Commit 5684cdf

Browse files
committed
chore(backend): init check and filter policies
Filter and check policies can be used in various parts of the code. These policies are meant to trigger authorization based on the given provider. Signed-off-by: Luca Zaninotto <luca.zaninotto@secomind.com>
1 parent c3f9bf8 commit 5684cdf

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.Policies.Check do
22+
@moduledoc """
23+
An `Ash.Policy.SimpleCheck` check to query the provider for a specific `relation` with `object`
24+
25+
To use it, call it this way in a policy
26+
```elixir
27+
{Edgehog.Auth.Policies.Check, id: :id_attribute, rel: :can_view, obj: "deployment"}
28+
```
29+
30+
The following opts are available:
31+
- `rel` (required) :: the relationship you want to check for.
32+
- `obj` (required) :: type object you want to check for.
33+
- `obj_id` :: the id of the resource. This is used to build the value `type:id` for the object in the tuple.
34+
35+
Example: for devices
36+
```elixir
37+
# alias Edgehog.Auth.Policies.Check
38+
# This can be used to authorize reads on single devices
39+
authorize_if {Check, rel: :can_view, obj: :device, obj_id: :device_id}
40+
```
41+
"""
42+
use Ash.Policy.SimpleCheck
43+
44+
alias Edgehog.Auth.FGAService
45+
46+
require Logger
47+
48+
@subject_type "user"
49+
50+
@impl Ash.Policy.SimpleCheck
51+
def match?(actor, %{data: data}, opts) do
52+
obj_id = Keyword.get(opts, :obj_id, :id)
53+
54+
subj = actor.sub
55+
rel = opts |> Keyword.fetch!(:rel) |> to_string()
56+
obj = opts |> Keyword.fetch!(:obj) |> to_string()
57+
58+
obj_id =
59+
data
60+
|> Ash.load!(obj_id)
61+
|> Map.get(obj_id, "*")
62+
|> to_string()
63+
64+
Logger.debug("Authorizing a tuple",
65+
subject: @subject_type,
66+
subject_id: subj,
67+
relationship: rel,
68+
object: obj,
69+
object_id: obj
70+
)
71+
72+
FGAService.check("#{@subject_type}:#{subj}", rel, "#{obj}:#{obj_id}")
73+
end
74+
75+
@impl Ash.Policy.Check
76+
def describe(opts) do
77+
rel = Keyword.fetch!(opts, :rel)
78+
obj = Keyword.fetch!(opts, :obj)
79+
obj_id = Keyword.get(opts, :obj_id, :id)
80+
81+
"Checking if the actor has relation #{inspect(rel)} on #{inspect(obj)} (using field #{inspect(obj_id)} for ID)"
82+
end
83+
end
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.Policies.Filter do
22+
@moduledoc """
23+
An `Ash.Policy.FilterCheck`, filtering the object of type `obj` available to the user `subj`.
24+
25+
To use it in a policy:
26+
```elixir
27+
filter_by {Edgehog.Auth.Policies.Filter, rel: :can_view, obj: :device}
28+
```
29+
"""
30+
31+
use Ash.Policy.FilterCheck
32+
33+
alias Ash.Policy.FilterCheck
34+
alias Edgehog.Auth.FGAService
35+
36+
require Logger
37+
38+
@impl FilterCheck
39+
def filter(actor, _context, opts) do
40+
subj = actor.sub
41+
42+
rel = opts |> Keyword.fetch!(:rel) |> to_string()
43+
obj_type = opts |> Keyword.fetch!(:obj) |> to_string()
44+
obj_id = Keyword.get(opts, :obj_id, :id)
45+
46+
log_context = [
47+
subj: subj,
48+
obj_type: obj_type,
49+
obj_id: obj_id
50+
]
51+
52+
subj
53+
|> FGAService.list_objects(rel, obj_type)
54+
|> to_ash_expr(obj_id, log_context)
55+
end
56+
57+
@impl FilterCheck
58+
def reject(actor, _context, opts) do
59+
subj = actor.sub
60+
61+
rel = opts |> Keyword.fetch!(:rel) |> to_string()
62+
obj_type = opts |> Keyword.fetch!(:obj) |> to_string()
63+
obj_id = Keyword.get(opts, :obj_id, :id)
64+
65+
log_context = [
66+
subj: subj,
67+
obj_type: obj_type,
68+
obj_id: obj_id
69+
]
70+
71+
subj
72+
|> FGAService.list_objects(rel, obj_type)
73+
|> to_ash_expr(obj_id, log_context)
74+
end
75+
76+
@impl Ash.Policy.Check
77+
def describe(opts) do
78+
rel = Keyword.fetch!(opts, :rel)
79+
obj = Keyword.fetch!(opts, :obj)
80+
"Filtering objects of type #{inspect(obj)} where actor has relation #{inspect(rel)}"
81+
end
82+
83+
defp to_ash_expr({:ok, ids}, id_attribute, _log_context), do: expr(^ref(id_attribute) in ^ids)
84+
85+
defp to_ash_expr({:error, error}, _id_attribute, log_context) do
86+
Logger.error("Error while filtering: #{inspect(error)}", log_context)
87+
88+
# We had an error while interrogating the provider. Poison everything.
89+
expr(nil)
90+
end
91+
end

0 commit comments

Comments
 (0)