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
prehooks, 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
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
requireUseraround 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.
getCustomershas the type
ActionCtxT 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.
Instead of defining an
requireUser like function, we will define a hook for use in the
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
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
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)
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.