hsoz-0.0.0.2: src/Network/Oz/Ticket.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
-- | This module is best imported qualified.
-- Unless you are writing your own Oz endpoints, all you
-- will need for a normal application server is 'rsvp'.
module Network.Oz.Ticket
( rsvp
, issue
, reissue
, parse
) where
import Control.Monad (liftM, void, when)
import Control.Monad.IO.Class (MonadIO (..), liftIO)
import Control.Applicative ((<|>))
import Data.Monoid ((<>))
import Crypto.Random
import Data.Aeson (Object (..), Value (..), object,
toJSON)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.List (isInfixOf, nub)
import Data.Maybe (catMaybes, fromMaybe, isJust)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime)
import Network.Hawk.Server.Types
import qualified Network.Iron as Iron
import Network.Oz.JSON
import Network.Oz.Types
-- | When the user authorizes the application access request, the
-- server issues an /rsvp/ which is an encoded string containing the
-- application identifier, the grant identifier, and an expiration.
--
-- This function generates the /rsvp/ string.
rsvp :: MonadIO m => OzAppId -> Maybe OzGrantId -> Key -> TicketOpts -> m (Maybe ByteString)
rsvp app grant (Key p) TicketOpts{..} = liftIO $ do
now <- getPOSIXTime
Iron.sealWith ticketOptsIron (Iron.password p) (envelope now)
where
envelope now = OzTicket
{ ozTicketExp = now + ticketOptsRsvpTtl
, ozTicketApp = app
, ozTicketGrant = grant
, ozTicketUser = Nothing
, ozTicketScope = []
, ozTicketDelegate = False
, ozTicketDlg = Nothing
}
-- | Issues a new application or user ticket.
issue :: MonadIO m => Key -> OzApp -> Maybe OzGrant -> TicketOpts -> m (Either String OzSealedTicket)
issue p app mgrant opts = case checkGrant app mgrant of
Right scope -> issueTicket p mgrant
(fromMaybe [] scope)
(ozAppId app)
Nothing True opts
Left e -> return (Left e)
where
checkGrant _ Nothing = Right Nothing
checkGrant OzApp{..} (Just OzGrant{..}) = checkGrantScope ozAppScope ozGrantScope -- fixme: what else to check?
-- | Generates a ticket without any checking
issueTicket :: MonadIO m => Key -> Maybe OzGrant -> OzScope -> OzAppId
-> Maybe OzAppId -> Bool -> TicketOpts
-> m (Either String OzSealedTicket)
issueTicket p mgrant scope app dlg delegate opts = do
exp <- getExpiry opts mgrant
let ticket = OzTicket { ozTicketExp = exp
, ozTicketApp = app
, ozTicketScope = scope
, ozTicketGrant = ozGrantId <$> mgrant
, ozTicketUser = ozGrantUser <$> mgrant
, ozTicketDlg = dlg
, ozTicketDelegate = ticketOptsDelegate opts && delegate
}
res <- liftIO $ generateTicket opts p ticket
return $ maybe (Left "Could not issue ticket") Right res
-- | Reissues an application or user ticket.
reissue :: MonadIO m => Key -> OzApp -> Maybe OzGrant
-> TicketOpts -> Maybe OzScope -> Maybe OzAppId
-> OzSealedTicket -> m (Either String OzSealedTicket)
reissue p app mgrant opts@TicketOpts{..} mscope issueTo t = case checks of
Right () -> issueTicket p mgrant
(fromMaybe ozTicketScope mscope)
(fromMaybe ozTicketApp issueTo)
(issueTo <|> ozTicketDlg)
ozTicketDelegate
opts'
Left e -> return (Left e)
where
checks :: Either String ()
checks = do
void $ checkParentScope (Just ozTicketScope) mscope
when (ticketOptsDelegate && not ozTicketDelegate)
$ Left "Cannot override ticket delegate restriction"
when (isJust issueTo) $ do
when (isJust ozTicketDlg) $ Left "Cannot re-delegate" -- fixme: http bad request
when (not ozTicketDelegate) $ Left "Ticket does not allow delegation"
when (ozTicketGrant /= fmap ozGrantId mgrant) $
Left "Parent ticket grant does not match options.grant"
OzTicket{..} = ozTicket t
opts' = if ticketOptsExt == mempty && not (null (ozTicketExt t))
then opts { ticketOptsExt = OzExt (ozTicketExt t) mempty }
else opts
getExpiry :: MonadIO m => TicketOpts -> Maybe OzGrant -> m POSIXTime
getExpiry opts mgrant = do
now <- liftIO getPOSIXTime
return $ calc (ticketOptsTicketTtl opts) mgrant now
where
calc ttl mgrant now = maybe id (min . ozGrantExp) mgrant (now + ttl)
-- | Probably not a worthy function
checkPassword :: Key -> Either String ()
checkPassword (Key p) | BS.null p = Left "Invalid encryption password"
| otherwise = Right ()
-- | Validate a grant scope in comparison to an app scope.
checkGrantScope :: Maybe OzScope -> Maybe OzScope -> Either String (Maybe OzScope)
checkGrantScope app grant = mapLeft (const msg) (checkScopes app grant)
where msg = "Grant scope is not a subset of the application scope"
checkParentScope :: Maybe OzScope -> Maybe OzScope -> Either String (Maybe OzScope)
checkParentScope parent scope = mapLeft (const msg) (checkScopes parent scope)
where msg = "New scope is not a subset of the parent ticket scope"
checkScopes :: Maybe OzScope -> Maybe OzScope -> Either String (Maybe OzScope)
checkScopes Nothing Nothing = Right Nothing
checkScopes Nothing (Just _) = Left ""
checkScopes (Just big) Nothing = Just <$> checkScope big
checkScopes (Just big) (Just little) | isInfixOf little big = Just <$> checkScope little
| otherwise = Left "not a subset"
-- | Validate scope array strings.
checkScope :: OzScope -> Either String OzScope
checkScope scope | any T.null scope = Left "scope includes empty string value"
| length (nub scope) /= length scope = Left "scope includes duplicated item"
| otherwise = Right scope
mapLeft :: (a -> c) -> Either a b -> Either c b
mapLeft f (Left a) = Left (f a)
mapLeft _ (Right b) = Right b
randomKey :: TicketOpts -> IO ByteString
randomKey TicketOpts{..} = do
-- fixme: check that this is seeded properly
drg <- getSystemDRG
return (fst $ withRandomBytes drg ticketOptsKeyBytes base64)
base64 :: ByteString -> ByteString
base64 = Iron.urlSafeBase64 . B64.encode
-- | Adds the cryptographic properties to a ticket and prepares it for
-- sending.
generateTicket :: TicketOpts -> Key -> OzTicket -> IO (Maybe OzSealedTicket)
generateTicket opts@TicketOpts{..} (Key p) t = do
key <- randomKey opts
let Object ext = toJSON ticketOptsExt
let sealed = OzSealedTicket t (Key key) ticketOptsHmacAlgorithm ext ""
mid <- Iron.sealWith ticketOptsIron (Iron.password p) sealed
return (finishSeal ticketOptsExt sealed <$> mid)
-- | Removes the private ext part and adds the ticket ID.
finishSeal :: OzExt -> OzSealedTicket -> ByteString -> OzSealedTicket
finishSeal ext ticket ticketId = ticket { ozTicketId = decodeUtf8 ticketId
, ozTicketExt = ozExtPublic ext
}
-- | Decodes a Hawk "app" string into an Oz Ticket.
parse :: TicketOpts -> Key -> ByteString -> IO (Either String OzSealedTicket)
parse TicketOpts{..} (Key p) = Iron.unsealWith ticketOptsIron lookup
where lookup = Iron.onePassword p