defmodule Carny.Policy do @moduledoc """ Policies are lists of statements that you can validate an identifier against. """ alias __MODULE__ alias Carny.{Statement, Identifier} defstruct statements: [] @doc ~S""" Adds a statement to a given policy ## Examples iex> Carny.Policy.add_statement(%Carny.Policy{statements: [%Carny.Statement{effect: :deny}]}, %Carny.Statement{}) %Carny.Policy{statements: [%Carny.Statement{}, %Carny.Statement{effect: :deny}]} """ def add_statement(%Policy{} = policy, %Statement{} = statement) do Map.update!(policy, :statements, fn statements -> [statement | statements] end) end @doc """ Does the id pass a specific policy? The use case here is we have already looked up a policy for our given resource and want to make sure that it has access to some other resource (id). ## Examples iex> Carny.Policy.authorized?("start", %Carny.Identifier{}, %Carny.Policy{statements: [%Carny.Statement{privileges: ["start"], resources: [%Carny.Identifier{}]}]}) true """ def authorized?(privilege, %Identifier{} = id, %Policy{} = policy) do # As soon as we reach a "true" value then halt # because we know it should have access Enum.reduce_while(policy.statements, false, fn statement, false -> {:cont, Statement.passes?(privilege, id, statement)} _statement, true -> {:halt, true} end) end def from_map(%{"statements" => statements} = policy) do statements = Enum.map(statements, &Statement.from_map/1) struct(Policy, %{policy | "statements" => statements} |> atom_map()) end defp atom_map(t), do: Map.new(t, fn {k, v} -> {String.to_atom(k), v} end) end