Yesod is a terrific framework for web applications in Haskell. It has many, many built-in features. One of them is that there’s nice support for authentication and authorization. In this post I’m interested in talking about how you could write your authorization code such that it’s harder to make mistakes.
As shown on the recent example of creating a blog web app, Yesod’s approach to authorization lies within the isAuthorized
function:
...
isAuthorized :: Route a -> Bool -> GHandler s a AuthResult
...
So isAuthorized
takes a Route a
, such as EntryR 10
, and a Bool
telling if the request may do writes (e.g. POST or PUT) or not (e.g. GET or HEAD). It must return an AuthResult
that decides if the user is Authorized
, Unauthorized
or if he needs to be authenticated first (AuthenticationRequired
).
While keeping authorization code in a single place is nice, using isAuthorized
alone makes it very difficult to test your authorization code. Using the blog example again, only the admin should be able to create blog posts. So let’s take a look at the example code that decides if the user should be authorized to post to the blog:
mauth <- maybeAuth
case mauth of
Nothing -> return AuthenticationRequired
Just (_, user)
| isAdmin user -> return Authorized
| otherwise -> unauthorizedI MsgNotAnAdmin
There are many things going on here:
- There’s authentication code that checks if the current user is logged in via
maybeAuth
. - If the user is not logged in, it returns
AuthenticationRequired
. - Otherwise the user’s credentials are checked to see if he should be authorized or not.
Step 1 is pretty standard housekeeping-style code. Steps 2 and 3, however, are part of your business logic that decides who should be able to do what. This means that you should be able to create (a) an unit test that asserts that the admin can create blog posts and (b) an unit test that asserts that a non-admin can’t create blog posts. Unfortunately, with the code above writing unit tests is really difficult. You not only need to artificially run the GHandler
monad (boring), but you need to fake the current session so that Step 1’s maybeAuth
gets the right information (difficult). Even then, your isAuthorized
function is allowed to do anything, since it’s inside GHandler
.
Enter permissions. What we’re really trying to say in the code above is that (a) to create a blog post you need the “post” permission and (b) admin has “post” permission, non-admins don’t. So let’s split these things! First of all, we need a list of the permissions that we’ll need:
You should read these constructors names with “permission to” before their names. For example, the admin has permission to Post
and any logged user has permission to Comment
.
Each request needs to decide which permissions it requires. This is one of the most important pieces of your application’s security, since forgetting to ask for permissions could lead to catastrophic problems. Instead of having this core piece of your app diluted in isAuthorized
, we use a simple, clear, pure function called permissionsRequiredFor
. The idea is to make permissionsRequiredFor
as simple as possible, such that with code review alone you could determine if you’re asking for the right permissions.
permissionsRequiredFor BlogR True = [Post]
permissionsRequiredFor (EntryR _) True = [Comment]
permissionsRequiredFor _ _ = []
We also need to decide if the currently logged user has the necessary permissions or not. This is the other important piece of your authorization puzzle, and a piece that we need to make easily testable. In order to do so, we avoid maybeAuth
and take the user as an argument.
-> Permission
-> YesodDB sub Blog AuthResult
(_, user) `hasPermissionTo` Post
| isAdmin user = return Authorized
| otherwise = lift $ unauthorizedI MsgNotAnAdmin
_ `hasPermissionTo` Comment = return Authorized
isAuthorizedTo :: Maybe (UserId, User)
-> [Permission]
-> YesodDB sub Blog AuthResult
_ `isAuthorizedTo` [] = return Authorized
Nothing `isAuthorizedTo` (_:_) = return AuthenticationRequired
Just u `isAuthorizedTo` (p:ps) = do
r <- u `hasPermissionTo` p
case r of
Authorized -> Just u `isAuthorizedTo` ps
_ -> return r -- unauthorized
The hasPermissionTo
function decides if the user has a given permission or not. While in this example hasPermissionTo
could have been a pure function, in general you may need to access the database. The isAuthorizedTo
function then (a) decides if the user needs to be authenticated and (b) checks all permissions required from the list using hasPermissionTo
.
Finally, we need to implement isAuthorized
gluing everything together:
mauth <- maybeAuth
runDB $ mauth `isAuthorizedTo` permissionsRequiredFor route isWrite
Ok, so what did we gain by writing three more functions?
- You can easily review
permissionsRequiredFor
to see if you didn’t leave a restricted route in the open. - If you don’t use the wildcards on the last line of
permissionsRequiredFor
and instead list all of your routes one by one, then you’d get a compiler warning and a runtime error every time you forgot to set the permissions of a newly added route. - If you have many routes that needed the same permissions, you don’t need to recode the permission code everywhere. You just need to code it once on
hasPermissionTo
and then ask for that permission on each of your routes. In my experience, the set of permissions (i.e. thePermission
data type) is a lot smaller than the set of possible routes. - You may easily create unit tests for
hasPermissionTo
, increasing your confidence on your code’s correctness.
I should also note that this approach is easily extendable. For example, suppose that you wanted to restrict the visibility of some of your blog posts. You could change the Permission
data type into:
Now your hasPermissionTo
function is able to give a different answer depending on which blog post we’re talking about.
So far this approach has been successfully used on my day job’s Yesod application. It looks like a cousin of Yesod’s i18n support using data types.
Thanks for reading along this far! Please use the comment section below to say what think of this approach. =)
Hi ,
Thank you for the helpful article. Seems like this is one of the lonely examples on the permissions for Yesod. However this does not compile on the new versions of Yesod. Do you have some update on this ? I got it to compile but I changed the behavior slightly , main problem is that now maybeAuth returns (Maybe (Entity val)) and I don’t know how to go from that to (UserId, User)
What about using maybeAuthPair?
Ponadto, że jest rewelacyjnie to można coś przy tym zapisać. Korporacje mają zawsze pomysły na co przydzielić finansowania. Wobec tego proponuje zobacz to https://finanserek.pl/ . Takie sponsorowanie dla kompanii to jest problem przez cały czas aktualny. Potrzeb jest wiele i pomysłów na spożytkowanie pieniędzy także.
Neste momento-parece сomo Drupal é o topo plataforma ԁe blogging lá fօra agora.
(pelo qᥙe eu li) É isso que é usando no seu blog?
what is the functions of this operations?