Modules, Functions, and Pattern Matching. Oh My!
As a relatively new Elixir developer, I continue to be impressed by the things the language allows me to accomplish relatively easily. Features that seemed daunting or time consuming before end up being as simple as: “Modules. Functions. Pattern Matching.” Instead of relying on pre-built solutions, a lot of the time your code ends up simpler and easier to reason about if you take advantage of what is in front of you.
Example: Web App Authorization
Let’s pretend we’re building a web application where users submit talk ideas for a conference and then the best ones are selected to be included. Users can submit and edit their own proposals, and admins can mark a talk as chosen
. We want to enforce that a rogue user can’t select random talks or edit other users’ talks to be about nonsense.
Not worrying about how the logic works within the application, let’s just lay out some rules for what users can do to talks.
defmodule FakeConf.TalkAuthorizer do
alias FakeConf.{Talk, User}
# Anyone can go and create a talk
def authorize(:create_talk, %User{} = _user), do: :ok
# Only the user that created a talk can edit it
def authorize(:edit_talk, %User{} = user, %Talk{} = talk) do
if owned_by?(user, talk) do
:ok
else
{:error, :unauthorized}
end
end
# Only admins can 'choose' talks
def authorize(:choose_talk, %User{is_admin: true} = user, %Talk{}), do: :ok
def authorize(:choose_talk, %User{}, %Talk{}), do: {:error, :unauthorized}
defp owned_by?(%User{} = user, %Talk = talk) do
talk.user_id == user.id
end
end
This keeps our authorization code nice and contained! When we implement more features, it will be easy to add them to this module.
Now that we have these rules, we can use them in our application. We’ll use the new with macro here to chain authorization into our existing app logic.
defmodule FakeConf.Talks do
alias FakeConf.{Talk, Repo, TalkAuthorizer}
def create_talk(%User{} = user, talk_params) do
talk = %Talk{user_id: user.id}
with :ok <- TalkAuthorizer.authorize(:create_talk, user),
{:ok, talk} <- Repo.insert(changeset(talk, talk_params)) do
:ok
else
{:error, :unauthorized} -> {:error, :unauthorized}
end
end
def edit_talk(%User{} = user, talk_id, talk_params) do
with %Talk{} = talk <- Repo.get(Talk, talk_id),
:ok <- TalkAuthorizer.authorize(:edit_talk, user, talk),
{:ok, talk} <- Repo.update(changeset(talk, talk_params)) do
:ok
else
{:error, :unauthorized} -> {:error, :unauthorized}
{:error, :not_found} -> {:error, :not_found}
end
end
def choose_talk(%User{} = user, talk_id) do
with %Talk{} = talk <- Repo.get(Talk, talk_id),
:ok <- TalkAuthorizer.authorize(:choose_talk, user, talk),
{:ok, talk} <- Repo.insert(changeset(talk, %{chosen: true})) do
:ok
else
{:error, :unauthorized} -> {:error, :unauthorized}
{:error, :not_found} -> {:error, :not_found}
end
end
end
Super simple, clean, and easy to read because we’re combining functions that are organized in modules. Nothing crazy happening.
This isn’t just an example of how to do authorization. When you’re looking to add a feature to your application, consider it might be simpler than you think when you take advantage of the tools you have available. When in doubt… ‘Keep It Simple, Silly’!