Skip to content

Commit 832c4b8

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 18a9cee commit 832c4b8

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
37+
```elixir
38+
alias Edgehog.Auth.Policies.Check
39+
40+
# This can be used to authorize reads on single devices
41+
authorize_if {Check, rel: :can_view, obj: :device, obj_id: :device_id}
42+
```
43+
"""
44+
use Ash.Policy.SimpleCheck
45+
46+
alias Edgehog.Auth.FGAService
47+
48+
require Logger
49+
50+
@subject_type "user"
51+
52+
@impl Ash.Policy.SimpleCheck
53+
def match?(actor, %{data: data}, opts) do
54+
obj_id = Keyword.get(opts, :obj_id, :id)
55+
56+
subj = actor.sub
57+
rel = opts |> Keyword.fetch!(:rel) |> to_string()
58+
obj = opts |> Keyword.fetch!(:obj) |> to_string()
59+
60+
obj_id =
61+
data
62+
|> Ash.load!(obj_id)
63+
|> Map.get(obj_id, "*")
64+
|> to_string()
65+
66+
Logger.debug("Authorizing a tuple",
67+
subject: @subject_type,
68+
subject_id: subj,
69+
relationship: rel,
70+
object: obj,
71+
object_id: obj
72+
)
73+
74+
FGAService.check("#{@subject_type}:#{subj}", rel, "#{obj}:#{obj_id}")
75+
end
76+
77+
@impl Ash.Policy.Check
78+
def describe(opts) do
79+
rel = Keyword.fetch!(opts, :rel)
80+
obj = Keyword.fetch!(opts, :obj)
81+
obj_id = Keyword.get(opts, :obj_id, :id)
82+
83+
"Checking if the actor has relation #{inspect(rel)} on #{inspect(obj)} (using field #{inspect(obj_id)} for ID)"
84+
end
85+
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)