Skip to content

Commit 83da23c

Browse files
committed
chore(backend): init filter policy
Adds a filter policy that can be used to filter based on the value of a specific field and the relation the actor has with such field. Signed-off-by: Luca Zaninotto <luca.zaninotto@secomind.com>
1 parent c5563a6 commit 83da23c

1 file changed

Lines changed: 108 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
authorize_if {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+
@subject_type "user"
39+
40+
@impl FilterCheck
41+
def filter(nil, _context, _opts), do: expr(nil)
42+
43+
@impl FilterCheck
44+
def filter(actor, _context, opts) do
45+
subj = Map.get(actor, :sub, "anon")
46+
47+
rel = opts |> Keyword.fetch!(:rel) |> to_string()
48+
obj_type = opts |> Keyword.fetch!(:obj) |> to_string()
49+
obj_id = Keyword.get(opts, :obj_id, :id)
50+
51+
log_context = [
52+
subj: subj,
53+
obj_type: obj_type,
54+
obj_id: obj_id
55+
]
56+
57+
subject = "#{@subject_type}:#{subj}"
58+
59+
subject
60+
|> FGAService.list_objects(rel, obj_type)
61+
|> to_ash_expr(obj_id, log_context)
62+
end
63+
64+
@impl FilterCheck
65+
def reject(actor, _context, opts) do
66+
subj = Map.get(actor, :sub, "anon")
67+
68+
rel = opts |> Keyword.fetch!(:rel) |> to_string()
69+
obj_type = opts |> Keyword.fetch!(:obj) |> to_string()
70+
obj_id = Keyword.get(opts, :obj_id, :id)
71+
72+
log_context = [
73+
subj: subj,
74+
obj_type: obj_type,
75+
obj_id: obj_id
76+
]
77+
78+
subject = "#{@subject_type}:#{subj}"
79+
80+
subject
81+
|> FGAService.list_objects(rel, obj_type)
82+
|> to_ash_expr(obj_id, log_context)
83+
end
84+
85+
@impl Ash.Policy.Check
86+
def describe(opts) do
87+
rel = Keyword.fetch!(opts, :rel)
88+
obj = Keyword.fetch!(opts, :obj)
89+
"Filtering objects of type #{inspect(obj)} where actor has relation #{inspect(rel)}"
90+
end
91+
92+
defp to_ash_expr({:ok, :all}, _id_attribute, _log_context), do: expr(true)
93+
94+
defp to_ash_expr(
95+
{:ok, %Openfga.V1.ListObjectsResponse{objects: ids}},
96+
id_attribute,
97+
_log_context
98+
) do
99+
expr(^ref(id_attribute) in ^ids)
100+
end
101+
102+
defp to_ash_expr({:error, error}, _id_attribute, log_context) do
103+
Logger.error("Error while filtering: #{inspect(error)}", log_context)
104+
105+
# We had an error while interrogating the provider. Poison everything.
106+
expr(nil)
107+
end
108+
end

0 commit comments

Comments
 (0)