I cleaned up the code quite a bit, might even try to use it to protect my latest project just to find all the flaws. Here are some details:
def() is overridden so it sends and receives information about the current Module, Fuction and Arity (m/f/a) and wraps the ‘real body’ in a Task so it has it’s own process with inbox. Each function called in the wrapped function will receive a message with the m/f/a that called the function.
Knowing the current m/f/a and the calling m/f/a, a map is created and passed to the authorization function which you write yourself. As it’s a map, it’s easy to do pattern matching! The examples don’t show it, but you are able to protect context (Module) borders, restrict calls to "update_" <> _
for users (for every function of the Module) or giving admins a wildcard with def authorize(%{args: %{context: %{role: :admin}}}), do: :ok
Removed: If a function calls itself (see: get_post/2) this is detected and authorization will be skipped.
Added: Sounds like a rule to me! def authorize(%{mod: mod, func: func, arity: arity, calling_mod: mod, calling_func: func, calling_arity: arity}), do: :ok
does the job as expected. Removed the loop detection.
Authorization map:
auth_map = %{
calling_mod: calling_mod,
calling_func: calling_func,
calling_arity: calling_arity,
mod: current_mod,
func: current_func,
arity: current_arity,
args: current_args
}
The rules:
defmodule Main.Authorizer do
def authorize(auth_map \\ %{})
# update post rules
def authorize(%{func: "update_post", args: %{context: %{role: :admin}}}), do: :ok
def authorize(%{func: "update_post", args: args}) do
case Main.get_post(args[:id], args[:context]) do
:unauthorized -> :unauthorized
post -> post[:author_id] == args[:context][:user_id] && {:ok, post} || :unauthorized
end
end
# get post rules
def authorize(%{func: "get_post", args: %{context: %{role: :admin}}}), do: :ok
def authorize(%{func: "get_post", args: args}) do
post = Main.get_post(args[:id], args[:context])
post[:author_id] == args[:context][:user_id] && {:ok, post} || :unauthorized
end
# sink
def authorize(_), do: :unauthorized
end
The result (User 124 is author of Post 2):
iex(auth@127.0.0.1)1> Main.update_post(2, %{content: "Updated content"}, %{user_id: 124, role: :user})
[info] Main.get_post/2 requested authorization to access Main.get_post/2. [SKIPPED AUTH]
[info] Main.update_post/3 requested authorization to access Main.get_post/2. [ACCESS GRANTED]
[info] An unknown function requested authorization to access Main.update_post/3. Are you using IEX?. [ACCESS GRANTED]
{:ok, %{content: "Updated content"}}
iex(auth@127.0.0.1)2> Main.update_post(2, %{content: "Updated content"}, %{user_id: 123, role: :user})
[info] Main.get_post/2 requested authorization to access Main.get_post/2. [SKIPPED AUTH]
[info] Main.update_post/3 requested authorization to access Main.get_post/2. [ACCESS DENIED]
[info] An unknown function requested authorization to access Main.update_post/3. Are you using IEX?. [ACCESS DENIED]
:unauthorized
iex(auth@127.0.0.1)3> Main.update_post(2, %{content: "Updated content"}, %{user_id: 123, role: :admin})
[info] An unknown function requested authorization to access Main.update_post/3. Are you using IEX?. [ACCESS GRANTED]
{:ok, %{content: "Updated content"}}
iex(auth@127.0.0.1)4>