I’ve just released a new version of Spock: 0.9.0.0. Along with some small improvements a big feature has dropped: Contexts. Let’s take a look at them and how they can help out in an authentication scenario.
What are ‘Contexts’?
A context is a value that lives during a request. When a request arrives at your application, the context will be initialized with ()
. Hence the type of the basic route wiring monad SpockT m a
which is an alias for SpockCtxT () m a
. Inside your wired routes (ActionCtxT () m a
) monad you can now access the context using the getContext :: MonadIO m => ActionCtxT ctx m ctx
function. Let’s take a look at this:
Now this isn’t really much news and it does not seem very useful. That’s why you can now register prehook
s, that is actions that run before contained wired routes:
We’ve now hooked an action return 42
before our get "other-action"
route. The type of prehook :: forall m ctx ctx'. MonadIO m => ActionCtxT ctx m ctx' -> SpockCtxT ctx' m () -> SpockCtxT ctx m ()
might seem a little bit complicated at first sight, but it really is not: You supply the action that runs in the current context ctx
and return a new context ctx'
. Then you supply new routes that will work on that context ctx'
. The type “inside” the prehook
in our example above would be SpockCtxT Int m a
and getContext
will thus return an Int
.
Authentication
Prior contexts
Up to recently, authentication in Spock was tiring and not very safe from a programmers point of view. You had to define functions like
and remember to use them in all your routes that you wanted protected. It would even make your code a little bumpy when combined with route parameters:
Oops, we even forgot to write requireUser
for the /all-customers
route, so now even not logged in users can access all our customers and their secret data. Not good!
This approach has several problems:
- Not very DRY: we have to write
requireUser
around every route we want protected - One can not easily see (types!) if a route should only run if the user is logged in (e.g.
getCustomers
has the typeActionCtxT ctx m [Customers]
and can thus be run in any action) - Error prone
The new world
Contexts make authentication very elegant. Let’s dive right into code - you’ll need the hvect package in at least version 0.3.0.0.
Defining hooks
Instead of defining an requireUser
like function, we will define a hook for use in the prehook
function.
HVect
is a heterogenous strict list, learn more about heterogenous collections here
This hook will extend any HVect xs
context and insert the current User
into it. If the current user is not logged in the request will be aborted with text "Sorry, no access!"
like above. If we like to ensure that a user is administrator too, we can extend the hook by using another hook:
We indicate at type level that this route needs a User
in the context and will add IsAdmin
to the context. Using findFirst
we extract the first (and only) occurence of the User
in our context and check if he is an administrator.
To bootstrap everything, we need to lift the default context ()
to HVect '[]
:
Wiring it together
Now that we’ve written hooks it is time to wire them with our routes:
Great! No more requireUser
all over the place. Now let’s make sure that the getCustomers
function can only be called when a user is logged in:
ListContains n t xs
is a TypeFamily that indicates that the type t
is contained at position n
in the type level list xs
.
Now if we try to use getCustomers
in an action/route outside of the prehook authHook
(e.g. "public"
), we will get a type error! And even better: We can use getCustomers
inside the prehook adminHook
too, because our ListContains
predicates will simply compose. (Side note: you can also indicate that a type t
should not be contained in the list using NotInList t xs ~ 'True
)
Closing Notes
That’s it for now - if you are looking for a larger example check out funblog’s Web.Blog module which I’ve recently updated to demonstrate usage. If you find the documentation of Spock lacking in any way or you run into a bug, please feel free to create an issue on Github.
Comments
Looking forward to your Feedback on Reddit and HackerNews.