haskell-webapps
haskell-webapps copied to clipboard
Discussion: Current tenant and user for the domain API
- how to communicate tenant and user performing the action to the domain APi. Explicit arguments or reader monad
- how to represent an action/event initiated by the system, eg via a cron job?
Domain API refers to the Haskell library that implements actions, like createTenant
, createProduct
, etc. Functions in the domain API may need the current user and tenant for the following:
- Implementing an audit log
- Implementing authorization (as opposed to authentication)
There are three possible ways to implement this:
Option 1: Explicit arguments
createTenant :: NewTenant -> AppM (Tenant)
createUser :: Tenant -> NewUser -> AppM(Tenant)
createProduct :: Tenant -> User -> NewProduct -> AppM(Product)
editProduct :: Tenant -> User -> EditProduct -> AppM(Product)
Simplest to implement. However, tedious to pass around a tenant
and user
to every single function in the domain API.
Option 2a: Reader monad with the entire "request context" as the reader env
data RequestContext = ReqestContext{user :: Maybe User, tenant :: Maybe Tenant, dbPool :: ConnectionPool}
type AppM = ReaderT ReqestContext
createTenant :: NewTenant -> AppM(Tenant)
createUser :: NewUser -> AppM(User)
createProduct :: NewProduct -> AppM(Product)
createProduct newproduction = do
ctx@RequestContext{user=user, tenant=tenant, dbPool=dbPool} <- ask
-- do whatever we need to do with `user`, `tenant` or `dbPool`
-- we can introduce some helper functions to get user, tenant, dbPool easily
askUser :: Monad m => ReaderT RequestContext m (Maybe User)
askUser = ask >>= (\ctx -> return $ ctx ^. user) -- shorter way to write this?
askTenant :: Monad m => ReaderT RequestContext m (Maybe Tenant)
askTenant = ask >>= (\ctx -> return $ ctx ^. tenant) -- shorter way to write this?
-- helper function to run domain API functions in the RequestContext
withRequestContext user tenant dbPool action = runReaderT action RequestContext{user=user, tenant=tenant, dbPool=dbPool}
The obvious advantage is that one doesn't have to pass around a user
and tenant
value to every domain function. Our domain API will anyways be in a ReaderT
transformer stack (to be able to access things like the dbpool or the logger) and this integrates nicely with it. However, due to two edge cases we need to have a Maybe User
and Maybe Tenant
, (instead of a regular User
and `Tenant):
-
createTenant
which logically can not have auser
andtenant
value -
cretaeUser
which logically may have auser
value only if it's NOT the first user of the tenant being created.
Option 2b: Monad with type-class contraints
class (Monad m) => HasUser m where
askUser :: m User
class (Monad m) => HasTenant m where
askUser :: m Tenant
data RequestContext t u = RequestContext {dbPool :: ConnectionPool, tenant :: t, user :: u}
type FullRequestContext = RequestContext Tenant User
type NoTenantRequestContext = RequestContext () ()
type NoUserRequestContext = RequestContext Tenant ()
newtype BaseAppM = ReaderT RequestContext
newtype AppM = ReaderT FullRequestContext
newtype NoTenantAppM = ReaderT NoTenantRequestContext
newtype NoUserAppM = ReaderT NoUserRequestContext
instance HasUser (BaseAppM t u) where
askUser = ask >>= (\ctx -> ctx ^. user)
instance HasTenant (BaseAppM t u) where
askUser = ask >>= (\ctx -> ctx ^. user)
createProduct :: (HasUser m, HasTenant m) => NewProduct -> m Product
createTenant :: (Monad m) => NewTenant -> m Product
createFirstUser :: (HasTenant m) => NewUser -> m User
createUser :: (HasTenant m, HasUser m) => NewUser -> m User
Reference for this idea: https://lexi-lambda.github.io/blog/2016/06/12/four-months-with-haskell/
Actually, I'm not sure if using type-class constraings for HasTenant
and HasUser
is bringing any advantages to the table here. The one idea that can be merged with Option 2a is to parameterize the RequestContext
with tenant and user so that we can run the createTenant
and createFirstUser
functions in a monad where user & tenant are unit types ()
easily, thus avoiding Maybe User
and Maybe Tenant
easily.
Option 3: Implicit parameters
createProduct :: (?user :: User, ?tenant :: Tenant) => NewProduct -> AppM Product
createTenant :: NewTenant -> AppM Tenant
createFirstUser :: (?tenant :: Tenant) => NewUser -> AppM User
createUser :: (?tenant :: Tenant, ?user :: User) => NewUser -> AppM User
-- Possible usage
randomServantHandler param1 param2 = do
session <- getSessionIdFromServantCookie -- don't know what the actual function is called
(tenant, user) <- getTenantAndUserFromSession session
createProduct newProduct
More about implicit paramaters extenstion: https://ocharles.org.uk/blog/posts/2014-12-11-implicit-params.html Does this really give any advantages over a ReaderT
monad?
i'd go with explicit arguments, but that's lack of experience. it seems like every method will need these facts, so tucking them away in a reader seems like the Haskell way of doing things.
I believe with a little work you can make Servant automatically pass these around for you as arguments to the handlers that require them.
@saurabhnanda @jfoutz @wz1000
how to communicate tenant and user performing the action to the domain APi. Explicit arguments or reader monad
http://haskell-servant.readthedocs.io/en/stable/tutorial/Authentication.html#generalized-authentication
AuthProtect "auth-name"
combinator can be used to get whatever type we want. We need to define a function which will take Request
and return Handler user
. user is polymorphic... can be anything we want. It can be a record with both the Authenticated User, Role, Current Tenant and also permissions if we so desire.
With the Request
we will be able to access any header information and use that to determine the user or raise an error.
example lookup "servant-auth-cookie" (requestHeaders req)
with this code we will be able to lookup a particular request header.
We can use IO to check with the DB and return a user type or any type we prefer and this will be automatically passed to the handler as the first argument.
We are using it and it works! Servant documentation explains clearly with code examples.
@saurabhnanda
how to represent an action/event initiated by the system, eg via a cron job?
I would create a user system
or cron
with required role & permissions and pass that to the functions as the first argument.
Here we can create multiple system users with different role and permissions so that we can track system actions and also define restrictions for each system user too.
btw.. I hope you will be ok to open another ticket to discuss security!
I believe with a little work you can make Servant automatically pass these around for you as arguments to the handlers that require them.
So, when I say domain API I mean the Haskell library that implements actions, like createTenant
, createProduct
, etc. Is it a good idea to couple them with Servant's types or functions?
Functions in the domain API may need the current user and tenant for the following:
- Implementing an audit log
- Implementing authorization (as opposed to authentication)
I can think of three ways to do this:
Option 1: Explicit arguments
createTenant :: NewTenant -> AppM (Tenant)
createUser :: Tenant -> NewUser -> AppM(Tenant)
createProduct :: Tenant -> User -> NewProduct -> AppM(Product)
editProduct :: Tenant -> User -> EditProduct -> AppM(Product)
Simplest to implement. However, tedious to pass around a tenant
and user
to every single function in the domain API.
Option 2a: Reader monad with the entire "request context" as the reader env
data RequestContext = ReqestContext{user :: Maybe User, tenant :: Maybe Tenant, dbPool :: ConnectionPool}
type AppM = ReaderT ReqestContext
createTenant :: NewTenant -> AppM(Tenant)
createUser :: NewUser -> AppM(User)
createProduct :: NewProduct -> AppM(Product)
createProduct newproduction = do
ctx@RequestContext{user=user, tenant=tenant, dbPool=dbPool} <- ask
-- do whatever we need to do with `user`, `tenant` or `dbPool`
-- we can introduce some helper functions to get user, tenant, dbPool easily
askUser :: Monad m => ReaderT RequestContext m (Maybe User)
askUser = ask >>= (\ctx -> return $ ctx ^. user) -- shorter way to write this?
askTenant :: Monad m => ReaderT RequestContext m (Maybe Tenant)
askTenant = ask >>= (\ctx -> return $ ctx ^. tenant) -- shorter way to write this?
-- helper function to run domain API functions in the RequestContext
withRequestContext user tenant dbPool action = runReaderT action RequestContext{user=user, tenant=tenant, dbPool=dbPool}
The obvious advantage is that one doesn't have to pass around a user
and tenant
value to every domain function. Our domain API will anyways be in a ReaderT
transformer stack (to be able to access things like the dbpool or the logger) and this integrates nicely with it. However, due to two edge cases we need to have a Maybe User
and Maybe Tenant
, (instead of a regular User
and `Tenant):
-
createTenant
which logically can not have auser
andtenant
value -
cretaeUser
which logically may have auser
value only if it's NOT the first user of the tenant being created.
Option 2b: Monad with type-class contraints
class (Monad m) => HasUser m where
askUser :: m User
class (Monad m) => HasTenant m where
askUser :: m Tenant
data RequestContext t u = RequestContext {dbPool :: ConnectionPool, tenant :: t, user :: u}
type FullRequestContext = RequestContext Tenant User
type NoTenantRequestContext = RequestContext () ()
type NoUserRequestContext = RequestContext Tenant ()
newtype BaseAppM = ReaderT RequestContext
newtype AppM = ReaderT FullRequestContext
newtype NoTenantAppM = ReaderT NoTenantRequestContext
newtype NoUserAppM = ReaderT NoUserRequestContext
instance HasUser (BaseAppM t u) where
askUser = ask >>= (\ctx -> ctx ^. user)
instance HasTenant (BaseAppM t u) where
askUser = ask >>= (\ctx -> ctx ^. user)
createProduct :: (HasUser m, HasTenant m) => NewProduct -> m Product
createTenant :: (Monad m) => NewTenant -> m Product
createFirstUser :: (HasTenant m) => NewUser -> m User
createUser :: (HasTenant m, HasUser m) => NewUser -> m User
Reference for this idea: https://lexi-lambda.github.io/blog/2016/06/12/four-months-with-haskell/
Actually, I'm not sure if using type-class constraings for HasTenant
and HasUser
is bringing any advantages to the table here. The one idea that can be merged with Option 2a is to parameterize the RequestContext
with tenant and user so that we can run the createTenant
and createFirstUser
functions in a monad where user & tenant are unit types ()
easily, thus avoiding Maybe User
and Maybe Tenant
easily.
Option 3: Implicit parameters
createProduct :: (?user :: User, ?tenant :: Tenant) => NewProduct -> AppM Product
createTenant :: NewTenant -> AppM Tenant
createFirstUser :: (?tenant :: Tenant) => NewUser -> AppM User
createUser :: (?tenant :: Tenant, ?user :: User) => NewUser -> AppM User
-- Possible usage
randomServantHandler param1 param2 = do
session <- getSessionIdFromServantCookie -- don't know what the actual function is called
(tenant, user) <- getTenantAndUserFromSession session
createProduct newProduct
More about implicit paramaters extension: https://ocharles.org.uk/blog/posts/2014-12-11-implicit-params.html Does this really give any advantages over a ReaderT
monad?
PS: Updating this issue's description with this comment.