Last active
March 14, 2020 22:04
-
-
Save mgwidmann/5ec3ae765f531b28d049637105c6f50e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ 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