Skip to content

Instantly share code, notes, and snippets.

@mgwidmann mgwidmann/on_definition.ex
Last active May 26, 2016

Embed
What would you like to do?
$ mix new policy_enforcement_playground
$ cd policy_enforcement_playground
# Make a new file lib/authorization.ex
defmodule Authorization do
def authorize_first_thing(_user, resource) do
# Do some authorization here, modifying what gets returned based on the
# authorization level of the user
resource
end
end
# Make a new file lib/actions.ex
defmodule Actions do
def do_first_thing(user, resource) do
resource = Authorization.authorize_first_thing(user, resource)
resource + 1
end
end
# So we have an Actions module that we want it to use the Authorization
# module to ensure the user is accessing only content which they're allowed.
# But theres a problem with this simple approach, anyone can come in and add
# a new function that doesn't call our Authorization layer and we've got a
# serious problem. Theres a few things we can do in most programming languages
# to address this issue.
#
# 1. The Ruby Approach - Write metaprogramming to make sure its called
# - Will be more complex and difficult to maintain code than the above
# 2. Put a comment at the top and hope devs read it or hope for code review to catch it
# - If only we could trust ourselves enough to do this...
# 3. The Java Approach - Build a convoluted object inheritance structure that enforces authorization
# - Also complex and difficult to maintian, but gives a compile time guarantee
# 4. Elixir's @on_definition hook...
# Lets add to lib/authorization.ex, inside the module add a nested module:
defmodule Enforcement do
# Defining a new exception
defmodule LacksAuthorizationError do
defexception [:message]
end
def __on_definition__(_env, kind, name, args, guards, body) do
IO.puts "Defining #{kind} named #{name} with args:"
IO.inspect args
IO.puts "and guards"
IO.inspect guards
IO.puts "and body"
IO.puts Macro.to_string(body)
end
end
# And add to the top of lib/actions.ex
@on_definition Authorization.Enforcement
# Compile to see the output
$ mix do clean, compile
# Important Output (compiler output removed):
# Defining def named do_first_thing with args:
# [{:user, [line: 3], nil}, {:resource, [line: 3], nil}]
# and guards
# []
# and body
# (
# resource = Authorization.authorize_first_thing(user, resource)
# resource + 1
# )
# So we want to check that there is a call to our authorization module
# on the first line or raise an exception at compile time. Change the
# last IO.puts line of the body to the following:
IO.puts Macro.to_string(body |> get_first_line)
# And add the get_first_line function below
def get_first_line({:__block__, _, expr_list}) do
hd(expr_list)
end
def get_first_line(expr) do
expr
end
# Recompile to see we got the first line
$ mix do clean, compile
# Important Output:
# Defining def named do_first_thing with args:
# [{:user, [line: 3], nil}, {:resource, [line: 3], nil}]
# and guards
# []
# and body
# resource = Authorization.authorize_first_thing(user, resource)
# Lets remove the print statements and make the __on_definition__/6 look like this:
body
|> get_first_line
|> IO.inspect
|> case do
# If it starts with that function call, we don't really care.
:what_goes_here? -> :ok
# If it didn't, we'd like to raise an error.
_ -> raise LacksAuthorizationError, message: "Function must begin with a call to a function from the Authorization module, didn't you read the comment?."
end
# If we try to compile we get output like this:
# {:=, [line: 4],
# [{:resource, [line: 4], nil},
# {{:., [line: 4],
# [{:__aliases__, [counter: 0, line: 4], [:Authorization]},
# :authorize_first_thing]}, [line: 4],
# [{:user, [line: 4], nil}, {:resource, [line: 4], nil}]}]}
#
# == Compilation error on file lib/actions.ex ==
# ** (Authorization.Enforcement.LacksAuthorizationError) Function must begin with a call to a function from the Authorization module, didn't you read the comment?.
# lib/authorization.ex:22: Authorization.Enforcement.__on_definition__/6
# (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
# Lets change the :what_goes_here? to be what it should be to enforce the call.
# Change the case statement to look like this:
|> case do
# If it starts with that function call, we don't really care.
{:=, _,
[
_,
{
{:., _, [{:__aliases__, _, [:Authorization]}, _]},
_, _
}
]
} -> :ok
# If it didn't, we'd like to raise an error.
_ -> raise LacksAuthorizationError, message: "Function must begin with a call to a function from the Authorization module, didn't you read the comment?."
end
# Compilation should succeed now.
# Try adding another function to the Actions module:
def do_another_thing(user, resource) do
:wont_compile
end
# Output is:
# == Compilation error on file lib/actions.ex ==
# ** (Authorization.Enforcement.LacksAuthorizationError) Function must begin with a call to a function from the Authorization module, didn't you read the comment?.
# lib/authorization.ex:30: Authorization.Enforcement.__on_definition__/6
# (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
# We can make our error message better by getting the line number, lets look at the meta info in the body
# Add the following code to the top of __on_definition__/6
[{_, [line: line], _}|_] = args
# Then just change the error message to say the following:
_ ->
raise(LacksAuthorizationError, message: """
Function must begin with a call to a function from the Authorization module, didn't you read the comment?.
#{Exception.format_file_line(env.file, line)}: #{Exception.format_mfa(env.context_modules |> hd, name, length(args))}
""" |> String.strip)
# Check out the docs here on all the other module attributes that elixir uses:
# http://elixir-lang.org/docs/stable/elixir/Module.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.