Much cleaner storage of remote address within audit

This commit is contained in:
Gregor Kleen 2019-03-31 14:23:30 +02:00
parent 14efbd8656
commit 9894dcd0fd
6 changed files with 52 additions and 54 deletions

View File

@ -3,8 +3,8 @@ TransactionLog
time UTCTime time UTCTime
instance InstanceId instance InstanceId
initiator (CI Text) Maybe -- Case-insensitive user-identifier associated with performing this action initiator (CI Text) Maybe -- Case-insensitive user-identifier associated with performing this action
remote IP Maybe -- Remote party that triggered this action via HTTP
info Value -- JSON-encoded `Transaction` info Value -- JSON-encoded `Transaction`
request RequestInfo Maybe -- HTTP-Request info
-- Best guess of users affected by a change in database-state at time of transaction -- Best guess of users affected by a change in database-state at time of transaction
TransactionLogAffected TransactionLogAffected
transaction TransactionLogId transaction TransactionLogId

View File

@ -118,6 +118,7 @@ dependencies:
- hsass - hsass
- semigroupoids - semigroupoids
- http-types - http-types
- ip
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving

View File

@ -1,7 +1,8 @@
module Audit module Audit
( module Audit.Types ( module Audit.Types
, audit , audit, audit'
, getRequestInfo , AuditRemoteException(..)
, getRemote
) where ) where
@ -13,25 +14,27 @@ import Model
import Utils.Lens import Utils.Lens
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import qualified Network.Socket as Wai
import qualified Net.IP as IP
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-} {-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
data RequestInfoException data AuditRemoteException
= RICouldNotParseMethod Method = ARUnsupportedSocketKind
deriving (Show, Generic, Typeable) deriving (Show, Generic, Typeable)
instance Exception RequestInfoException instance Exception AuditRemoteException
getRequestInfo :: (MonadHandler m, MonadThrow m) => m RequestInfo getRemote :: (MonadHandler m, MonadThrow m) => m IP
getRequestInfo = do getRemote = do
wai <- waiRequest wai <- waiRequest
riMethod <- either (throwM . RICouldNotParseMethod) return . parseMethod $ Wai.requestMethod wai case Wai.remoteHost wai of
let riRemote = Wai.remoteHost wai Wai.SockAddrInet _ hAddr -> let (b1, b2, b3, b4) = Wai.hostAddressToTuple hAddr in return $ IP.ipv4 b1 b2 b3 b4
riPath = Wai.pathInfo wai Wai.SockAddrInet6 _ _ hAddr _ -> let (w1, w2, w3, w4, w5, w6, w7, w8) = Wai.hostAddress6ToTuple hAddr in return $ IP.ipv6 w1 w2 w3 w4 w5 w6 w7 w8
riQueryString = queryToQueryText $ Wai.queryString wai _other -> throwM ARUnsupportedSocketKind
return RequestInfo{..}
audit :: ( AuthId site ~ Key User audit :: ( AuthId site ~ Key User
@ -41,23 +44,35 @@ audit :: ( AuthId site ~ Key User
, HasInstanceID site InstanceId , HasInstanceID site InstanceId
, YesodAuthPersist site , YesodAuthPersist site
) )
=> [UserId] -- ^ Affected users => Transaction -- ^ Transaction to record
-> Transaction -- ^ Transaction to record -> [UserId] -- ^ Affected users
-> YesodDB site () -> YesodDB site ()
-- ^ Log a transaction using information available from `HandlerT`: -- ^ Log a transaction using information available from `HandlerT`:
-- --
-- - `transactionLogTime` is now -- - `transactionLogTime` is now
-- - `transactionLogInitiator` is currently logged in user (or none) -- - `transactionLogInitiator` is currently logged in user (or none)
-- - `transactionLogRequest` is current HTTP-Request -- - `transactionLogRequest` is current HTTP-Request
audit affected (toJSON -> transactionLogInfo) = do audit (toJSON -> transactionLogInfo) affected = do
uid <- liftHandlerT maybeAuthId uid <- liftHandlerT maybeAuthId
transactionLogTime <- liftIO getCurrentTime transactionLogTime <- liftIO getCurrentTime
transactionLogInstance <- getsYesod $ view instanceID transactionLogInstance <- getsYesod $ view instanceID
transactionLogInitiator <- for uid $ fmap userIdent . getJust transactionLogInitiator <- for uid $ fmap userIdent . getJust
transactionLogRequest <- Just <$> getRequestInfo transactionLogRemote <- Just <$> getRemote
tlId <- insert TransactionLog{..} tlId <- insert TransactionLog{..}
affectedUsers <- forM affected getJust affectedUsers <- forM affected getJust
insertMany_ [ TransactionLogAffected tlId aident | aident <- userIdent <$> affectedUsers ] insertMany_ [ TransactionLogAffected tlId aident | aident <- userIdent <$> affectedUsers ]
audit' :: ( AuthId site ~ Key User
, AuthEntity site ~ User
, IsSqlBackend (YesodPersistBackend site)
, SqlBackendCanWrite (YesodPersistBackend site)
, HasInstanceID site InstanceId
, YesodAuthPersist site
)
=> Transaction -- ^ Transaction to record
-> YesodDB site ()
-- ^ Special case of `audit` for when there are no affected users
audit' = flip audit []

View File

@ -183,7 +183,7 @@ termEditHandler term = do
-- term <- runDB $ get $ TermKey termName -- term <- runDB $ get $ TermKey termName
runDB $ do runDB $ do
repsert tid res repsert tid res
audit [] $ TransactionTermEdit tid audit' $ TransactionTermEdit tid
-- VOR INTERNATIONALISIERUNG: -- VOR INTERNATIONALISIERUNG:
-- let tid = termToText $ termName res -- let tid = termToText $ termName res
-- let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert." -- let msg = "Semester " `T.append` tid `T.append` " erfolgreich editiert."

View File

@ -9,7 +9,7 @@ module Model.Types
, module Mail , module Mail
, module Utils.DateTime , module Utils.DateTime
, module Data.UUID.Types , module Data.UUID.Types
, StdMethod, QueryText, SockAddr, PortNumber , module Net.IP
) where ) where
import ClassyPrelude import ClassyPrelude
@ -27,7 +27,6 @@ import Data.Universe
import Data.Universe.Helpers import Data.Universe.Helpers
import Data.UUID.Types (UUID) import Data.UUID.Types (UUID)
import qualified Data.UUID.Types as UUID import qualified Data.UUID.Types as UUID
import Data.Scientific (Scientific, toBoundedInteger)
import Data.Default import Data.Default
@ -44,6 +43,7 @@ import Web.PathPieces
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Lens as Text import qualified Data.Text.Lens as Text
import qualified Data.Text.Encoding as Text
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
@ -71,7 +71,6 @@ import Utils.DateTime (DateTimeFormat(..), SelDateTimeFormat(..))
import Numeric.Natural import Numeric.Natural
import Data.Word.Word24 (Word24) import Data.Word.Word24 (Word24)
import Data.Word (Word16)
import Data.Bits import Data.Bits
import Data.Ix import Data.Ix
import Data.List (genericIndex, elemIndex) import Data.List (genericIndex, elemIndex)
@ -83,9 +82,8 @@ import Data.Text.Metrics (damerauLevenshtein)
import Data.Binary (Binary) import Data.Binary (Binary)
import Network.HTTP.Types.URI (QueryText) import Net.IP (IP)
import Network.HTTP.Types.Method (StdMethod(..)) import qualified Net.IP as IP
import Network.Socket
instance PathPiece UUID where instance PathPiece UUID where
@ -793,35 +791,14 @@ deriveJSON defaultOptions
derivePersistFieldJSON ''LecturerType derivePersistFieldJSON ''LecturerType
deriving instance Generic StdMethod instance PersistField IP where
deriving instance Generic SockAddr toPersistValue = PersistDbSpecific . encodeUtf8 . IP.encode
fromPersistValue (PersistDbSpecific bs) = first tshow (Text.decodeUtf8' bs) >>= maybe (Left "Could not parse IP-address") Right . IP.decode
deriveJSON defaultOptions fromPersistValue (PersistByteString bs) = first tshow (Text.decodeUtf8' bs) >>= maybe (Left "Could not parse IP-address") Right . IP.decode
{ constructorTagModifier = camelToPathPiece fromPersistValue (PersistText t) = maybe (Left "Could not parse IP-address") Right $ IP.decode t
} ''StdMethod fromPersistValue _ = Left "IP-address values must be converted from PersistDbSpecific, PersistText, or PersistByteString"
deriveJSON defaultOptions instance PersistFieldSql IP where
{ constructorTagModifier = camelToPathPiece' 2 sqlType _ = SqlOther "inet"
} ''SockAddr
instance FromJSON PortNumber where
parseJSON = Aeson.withScientific "PortNumber" $ \sciNum -> case toBoundedInteger sciNum of
Just int -> return $ fromIntegral (int :: Word16)
Nothing -> fail "Expected whole number of plausible size to denote port"
instance ToJSON PortNumber where
toJSON = toJSON . (fromIntegral :: PortNumber -> Scientific)
data RequestInfo = RequestInfo
{ riMethod :: StdMethod
, riRemote :: SockAddr
, riPath :: [Text]
, riQueryString :: QueryText
} deriving (Eq, Ord, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''RequestInfo
derivePersistFieldJSON ''RequestInfo
-- Type synonyms -- Type synonyms

View File

@ -14,7 +14,7 @@ import ClassyPrelude.Yesod
import Data.UUID (UUID) import Data.UUID (UUID)
import qualified Control.Exception as Exception import qualified Control.Exception as Exception
import Data.Aeson (Result (..), fromJSON, withObject import Data.Aeson (Result (..), fromJSON, withObject
,(.!=), (.:?) ,(.!=), (.:?), withScientific
) )
import qualified Data.Aeson.Types as Aeson import qualified Data.Aeson.Types as Aeson
import Data.Aeson.TH import Data.Aeson.TH
@ -272,6 +272,11 @@ deriveJSON
} }
''LogLevel ''LogLevel
instance FromJSON HaskellNet.PortNumber where
parseJSON = withScientific "PortNumber" $ \sciNum -> case toBoundedInteger sciNum of
Just int -> return $ fromIntegral (int :: Word16)
Nothing -> fail "Expected whole number of plausible size to denote port"
deriveFromJSON deriveFromJSON
defaultOptions defaultOptions
{ constructorTagModifier = unpack . intercalate "-" . Text.splitOn "_" . toLower . pack { constructorTagModifier = unpack . intercalate "-" . Text.splitOn "_" . toLower . pack