Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: Current tenant and user for the domain API #28

Open
saurabhnanda opened this issue Oct 15, 2016 · 5 comments
Open

Discussion: Current tenant and user for the domain API #28

saurabhnanda opened this issue Oct 15, 2016 · 5 comments

Comments

@saurabhnanda
Copy link
Contributor

saurabhnanda commented Oct 15, 2016

  • 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 a user and tenant value
  • cretaeUser which logically may have a user 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?

@jfoutz
Copy link

jfoutz commented Oct 15, 2016

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.

@wz1000
Copy link
Collaborator

wz1000 commented Oct 16, 2016

I believe with a little work you can make Servant automatically pass these around for you as arguments to the handlers that require them.

@sudhirvkumar
Copy link

@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.

@sudhirvkumar
Copy link

@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!

@saurabhnanda
Copy link
Contributor Author

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 a user and tenant value
  • cretaeUser which logically may have a user 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants