feat(ldap): automatically synchronise user data from ldap
This commit is contained in:
parent
7d927fdd5f
commit
b39ba8b268
@ -41,6 +41,9 @@ health-check-delay-notify: "_env:HEALTHCHECK_DELAY_NOTIFY:true"
|
|||||||
health-check-http: "_env:HEALTHCHECK_HTTP:true" # Can we assume, that we can reach ourselves under APPROOT via HTTP (reverse proxies or firewalls might prevent this)?
|
health-check-http: "_env:HEALTHCHECK_HTTP:true" # Can we assume, that we can reach ourselves under APPROOT via HTTP (reverse proxies or firewalls might prevent this)?
|
||||||
health-check-active-job-executors-timeout: "_env:HEALTHCHECK_ACTIVE_JOB_EXECUTORS_TIMEOUT:5"
|
health-check-active-job-executors-timeout: "_env:HEALTHCHECK_ACTIVE_JOB_EXECUTORS_TIMEOUT:5"
|
||||||
|
|
||||||
|
synchronise-ldap-users-within: "_env:SYNCHRONISE_LDAP_WITHIN:604800"
|
||||||
|
synchronise-ldap-users-interval: "_env:SYNCHRONISE_LDAP_INTERVAL:3600"
|
||||||
|
|
||||||
log-settings:
|
log-settings:
|
||||||
detailed: "_env:DETAILED_LOGGING:false"
|
detailed: "_env:DETAILED_LOGGING:false"
|
||||||
all: "_env:LOG_ALL:false"
|
all: "_env:LOG_ALL:false"
|
||||||
|
|||||||
@ -14,6 +14,8 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
|||||||
ident (CI Text) -- Case-insensitive user-identifier
|
ident (CI Text) -- Case-insensitive user-identifier
|
||||||
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
|
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
|
||||||
lastAuthentication UTCTime Maybe -- last login date
|
lastAuthentication UTCTime Maybe -- last login date
|
||||||
|
created UTCTime default=now()
|
||||||
|
lastLdapSynchronisation UTCTime Maybe
|
||||||
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
|
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
|
||||||
matrikelnummer UserMatriculation Maybe -- optional immatriculation-string; usually a number, but not always (e.g. lecturers, pupils, guests,...)
|
matrikelnummer UserMatriculation Maybe -- optional immatriculation-string; usually a number, but not always (e.g. lecturers, pupils, guests,...)
|
||||||
firstName Text -- For export in tables, pre-split firstName from displayName
|
firstName Text -- For export in tables, pre-split firstName from displayName
|
||||||
|
|||||||
@ -3110,12 +3110,15 @@ upsertCampusUser ldapData Creds{..} = do
|
|||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userMailLanguages = def
|
, userMailLanguages = def
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
|
, userCreated = now
|
||||||
|
, userLastLdapSynchronisation = Just now
|
||||||
, ..
|
, ..
|
||||||
}
|
}
|
||||||
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
|
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
|
||||||
, UserDisplayName =. userDisplayName
|
, UserDisplayName =. userDisplayName
|
||||||
, UserSurname =. userSurname
|
, UserSurname =. userSurname
|
||||||
, UserEmail =. userEmail
|
, UserEmail =. userEmail
|
||||||
|
, UserLastLdapSynchronisation =. Just now
|
||||||
] ++
|
] ++
|
||||||
[ UserLastAuthentication =. Just now | not isDummy ]
|
[ UserLastAuthentication =. Just now | not isDummy ]
|
||||||
|
|
||||||
|
|||||||
19
src/Jobs.hs
19
src/Jobs.hs
@ -66,6 +66,7 @@ import Jobs.Handler.SendCourseCommunication
|
|||||||
import Jobs.Handler.Invitation
|
import Jobs.Handler.Invitation
|
||||||
import Jobs.Handler.SendPasswordReset
|
import Jobs.Handler.SendPasswordReset
|
||||||
import Jobs.Handler.TransactionLog
|
import Jobs.Handler.TransactionLog
|
||||||
|
import Jobs.Handler.SynchroniseLdap
|
||||||
|
|
||||||
import Jobs.HealthReport
|
import Jobs.HealthReport
|
||||||
|
|
||||||
@ -428,11 +429,19 @@ jLocked jId act = do
|
|||||||
pruneLastExecs :: Crontab JobCtl -> DB ()
|
pruneLastExecs :: Crontab JobCtl -> DB ()
|
||||||
pruneLastExecs crontab = runConduit $ selectSource [] [] .| C.mapM_ ensureCrontab
|
pruneLastExecs crontab = runConduit $ selectSource [] [] .| C.mapM_ ensureCrontab
|
||||||
where
|
where
|
||||||
ensureCrontab (Entity leId CronLastExec{..})
|
ensureCrontab (Entity leId CronLastExec{..}) = void . runMaybeT $ do
|
||||||
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
|
now <- liftIO getCurrentTime
|
||||||
, HashMap.member (JobCtlQueue job) crontab
|
flushInterval <- MaybeT . getsYesod . view $ appSettings . _appJobFlushInterval
|
||||||
= return ()
|
|
||||||
| otherwise = delete leId
|
|
||||||
|
if
|
||||||
|
| abs (now `diffUTCTime` cronLastExecTime) > flushInterval * 2
|
||||||
|
-> return ()
|
||||||
|
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
|
||||||
|
, HashMap.member (JobCtlQueue job) crontab
|
||||||
|
-> return ()
|
||||||
|
| otherwise
|
||||||
|
-> lift $ delete leId
|
||||||
|
|
||||||
determineCrontab' :: DB (Crontab JobCtl)
|
determineCrontab' :: DB (Crontab JobCtl)
|
||||||
determineCrontab' = (\ct -> ct <$ pruneLastExecs ct) =<< determineCrontab
|
determineCrontab' = (\ct -> ct <$ pruneLastExecs ct) =<< determineCrontab
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import qualified Data.Map as Map
|
|||||||
import Data.Semigroup (Max(..))
|
import Data.Semigroup (Max(..))
|
||||||
|
|
||||||
import Data.Time.Zones
|
import Data.Time.Zones
|
||||||
|
import Data.Time.Clock.POSIX
|
||||||
|
|
||||||
import Control.Monad.Trans.Writer (WriterT, execWriterT)
|
import Control.Monad.Trans.Writer (WriterT, execWriterT)
|
||||||
import Control.Monad.Writer.Class (MonadWriter(..))
|
import Control.Monad.Writer.Class (MonadWriter(..))
|
||||||
@ -23,7 +24,7 @@ import qualified Database.Esqueleto as E
|
|||||||
determineCrontab :: DB (Crontab JobCtl)
|
determineCrontab :: DB (Crontab JobCtl)
|
||||||
-- ^ Extract all future jobs from the database (sheet deadlines, ...)
|
-- ^ Extract all future jobs from the database (sheet deadlines, ...)
|
||||||
determineCrontab = execWriterT $ do
|
determineCrontab = execWriterT $ do
|
||||||
AppSettings{..} <- getsYesod appSettings'
|
UniWorX{ appSettings' = AppSettings{..}, .. } <- getYesod
|
||||||
|
|
||||||
case appJobFlushInterval of
|
case appJobFlushInterval of
|
||||||
Just interval -> tell $ HashMap.singleton
|
Just interval -> tell $ HashMap.singleton
|
||||||
@ -84,6 +85,48 @@ determineCrontab = execWriterT $ do
|
|||||||
, cronNotAfter = Right CronNotScheduled
|
, cronNotAfter = Right CronNotScheduled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if
|
||||||
|
| is _Just appLdapConf
|
||||||
|
, is _Just appLdapConf
|
||||||
|
, Just syncWithin <- appSynchroniseLdapUsersWithin
|
||||||
|
-> do
|
||||||
|
now <- liftIO getPOSIXTime
|
||||||
|
let
|
||||||
|
interval = appSynchroniseLdapUsersInterval
|
||||||
|
|
||||||
|
(ldapEpoch, epochNow) = now `divMod'` syncWithin
|
||||||
|
ldapInterval = epochNow `div'` interval
|
||||||
|
numIntervals = floor $ syncWithin / interval
|
||||||
|
|
||||||
|
nextIntervals = do
|
||||||
|
let
|
||||||
|
n = ceiling $ 4 * appJobCronInterval / appSynchroniseLdapUsersInterval
|
||||||
|
i <- [negate (ceiling $ n % 2) .. ceiling $ n % 2]
|
||||||
|
let
|
||||||
|
((+ ldapEpoch) -> nextEpoch, nextInterval) = (ldapInterval + i) `divMod` numIntervals
|
||||||
|
nextIntervalTime
|
||||||
|
= posixSecondsToUTCTime $ fromInteger nextEpoch * syncWithin + fromInteger nextInterval * interval
|
||||||
|
return (nextEpoch, nextInterval, nextIntervalTime)
|
||||||
|
|
||||||
|
forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime) -> do
|
||||||
|
$logDebugS "SynchroniseLdap" [st|currentTime: #{tshow ldapEpoch}.#{tshow epochNow}; upcomingSync: #{tshow nextEpoch}.#{tshow (fromInteger nextInterval * interval)}; upcomingData: #{tshow (numIntervals, nextEpoch, nextInterval)}|]
|
||||||
|
tell $ HashMap.singleton
|
||||||
|
(JobCtlQueue JobSynchroniseLdap
|
||||||
|
{ jEpoch = fromInteger nextEpoch
|
||||||
|
, jNumIterations = fromInteger numIntervals
|
||||||
|
, jIteration = fromInteger nextInterval
|
||||||
|
})
|
||||||
|
Cron
|
||||||
|
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ nextIntervalTime
|
||||||
|
, cronRepeat = CronRepeatNever
|
||||||
|
, cronRateLimit = appSynchroniseLdapUsersInterval
|
||||||
|
, cronNotAfter = Left syncWithin
|
||||||
|
}
|
||||||
|
| otherwise
|
||||||
|
-> return ()
|
||||||
|
|
||||||
|
|
||||||
let
|
let
|
||||||
sheetJobs (Entity nSheet Sheet{..}) = do
|
sheetJobs (Entity nSheet Sheet{..}) = do
|
||||||
tell $ HashMap.singleton
|
tell $ HashMap.singleton
|
||||||
|
|||||||
55
src/Jobs/Handler/SynchroniseLdap.hs
Normal file
55
src/Jobs/Handler/SynchroniseLdap.hs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
module Jobs.Handler.SynchroniseLdap
|
||||||
|
( dispatchJobSynchroniseLdap
|
||||||
|
, SynchroniseLdapException(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
import Auth.LDAP
|
||||||
|
|
||||||
|
data SynchroniseLdapException
|
||||||
|
= SynchroniseLdapNoLdap
|
||||||
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Exception SynchroniseLdapException
|
||||||
|
|
||||||
|
dispatchJobSynchroniseLdap :: Natural -> Natural -> Natural -> Handler ()
|
||||||
|
dispatchJobSynchroniseLdap numIterations epoch iteration = do
|
||||||
|
UniWorX{ appSettings' = AppSettings{..}, .. } <- getYesod
|
||||||
|
case (,) <$> appLdapConf <*> appLdapPool of
|
||||||
|
Just (ldapConf, ldapPool) ->
|
||||||
|
runDB . runConduit $
|
||||||
|
readUsers .| filterIteration .| synchroniseUser ldapConf ldapPool
|
||||||
|
Nothing ->
|
||||||
|
throwM SynchroniseLdapNoLdap
|
||||||
|
where
|
||||||
|
readUsers :: Source (YesodDB UniWorX) UserId
|
||||||
|
readUsers = selectKeys [] []
|
||||||
|
|
||||||
|
filterIteration :: Conduit UserId (YesodDB UniWorX) User
|
||||||
|
filterIteration = C.mapMaybeM $ \userId -> runMaybeT $ do
|
||||||
|
let
|
||||||
|
userIteration, currentIteration :: Integer
|
||||||
|
userIteration = toInteger (hash epoch `hashWithSalt` userId) `mod` toInteger numIterations
|
||||||
|
currentIteration = toInteger iteration `mod` toInteger numIterations
|
||||||
|
$logDebugS "SynchroniseLdap" [st|User ##{tshow (fromSqlKey userId)}: sync on #{tshow userIteration}/#{tshow numIterations}, now #{tshow currentIteration}|]
|
||||||
|
guard $ userIteration == currentIteration
|
||||||
|
|
||||||
|
MaybeT $ get userId
|
||||||
|
|
||||||
|
synchroniseUser :: LdapConf -> LdapPool -> Sink User (YesodDB UniWorX) ()
|
||||||
|
synchroniseUser conf pool = C.mapM_ $ \user -> void . runMaybeT . handleExc $ do
|
||||||
|
$logInfoS "SynchroniseLdap" [st|Synchronising #{userIdent user}|]
|
||||||
|
|
||||||
|
ldapAttrs <- MaybeT $ campusUser' conf pool user
|
||||||
|
void . lift $ upsertCampusUser ldapAttrs Creds
|
||||||
|
{ credsIdent = CI.original $ userIdent user
|
||||||
|
, credsPlugin = "dummy"
|
||||||
|
, credsExtra = []
|
||||||
|
}
|
||||||
|
where
|
||||||
|
handleExc
|
||||||
|
= catchMPlus (Proxy @CampusUserException)
|
||||||
|
. catchMPlus (Proxy @CampusUserConversionException)
|
||||||
@ -80,6 +80,7 @@ writeJobCtlBlock = writeJobCtlBlock' writeJobCtl
|
|||||||
|
|
||||||
queueJobUnsafe :: Bool -> Job -> YesodDB UniWorX QueuedJobId
|
queueJobUnsafe :: Bool -> Job -> YesodDB UniWorX QueuedJobId
|
||||||
queueJobUnsafe queuedJobWriteLastExec job = do
|
queueJobUnsafe queuedJobWriteLastExec job = do
|
||||||
|
$logInfoS "queueJob" $ tshow job
|
||||||
queuedJobCreationTime <- liftIO getCurrentTime
|
queuedJobCreationTime <- liftIO getCurrentTime
|
||||||
queuedJobCreationInstance <- getsYesod appInstanceID
|
queuedJobCreationInstance <- getsYesod appInstanceID
|
||||||
insert QueuedJob
|
insert QueuedJob
|
||||||
|
|||||||
@ -50,6 +50,10 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
|
|||||||
}
|
}
|
||||||
| JobTruncateTransactionLog
|
| JobTruncateTransactionLog
|
||||||
| JobDeleteTransactionLogIPs
|
| JobDeleteTransactionLogIPs
|
||||||
|
| JobSynchroniseLdap { jNumIterations
|
||||||
|
, jEpoch
|
||||||
|
, jIteration :: Natural
|
||||||
|
}
|
||||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||||
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
||||||
| NotificationSheetActive { nSheet :: SheetId }
|
| NotificationSheetActive { nSheet :: SheetId }
|
||||||
|
|||||||
@ -103,7 +103,7 @@ createLdapPool host port stripes timeoutConn (round . (* 1e6) -> timeoutAct) lim
|
|||||||
setup <- liftIO newEmptyTMVarIO
|
setup <- liftIO newEmptyTMVarIO
|
||||||
|
|
||||||
ldapAsync <- allocateAsync . flip runLoggingT logFunc $ do
|
ldapAsync <- allocateAsync . flip runLoggingT logFunc $ do
|
||||||
$logInfoS "LdapExecutor" "Starting"
|
$logDebugS "LdapExecutor" "Starting"
|
||||||
res <- liftIO . Ldap.with host port $ flip runLoggingT logFunc . go (Just setup)
|
res <- liftIO . Ldap.with host port $ flip runLoggingT logFunc . go (Just setup)
|
||||||
case res of
|
case res of
|
||||||
Left exc -> do
|
Left exc -> do
|
||||||
|
|||||||
@ -118,6 +118,9 @@ data AppSettings = AppSettings
|
|||||||
, appHealthCheckHTTP :: Bool
|
, appHealthCheckHTTP :: Bool
|
||||||
, appHealthCheckActiveJobExecutorsTimeout :: NominalDiffTime
|
, appHealthCheckActiveJobExecutorsTimeout :: NominalDiffTime
|
||||||
|
|
||||||
|
, appSynchroniseLdapUsersWithin :: Maybe NominalDiffTime
|
||||||
|
, appSynchroniseLdapUsersInterval :: NominalDiffTime
|
||||||
|
|
||||||
, appInitialLogSettings :: LogSettings
|
, appInitialLogSettings :: LogSettings
|
||||||
|
|
||||||
, appTransactionLogIPRetentionTime :: NominalDiffTime
|
, appTransactionLogIPRetentionTime :: NominalDiffTime
|
||||||
@ -396,6 +399,9 @@ instance FromJSON AppSettings where
|
|||||||
|
|
||||||
appSessionTimeout <- o .: "session-timeout"
|
appSessionTimeout <- o .: "session-timeout"
|
||||||
|
|
||||||
|
appSynchroniseLdapUsersWithin <- o .:? "synchronise-ldap-users-within"
|
||||||
|
appSynchroniseLdapUsersInterval <- o .: "synchronise-ldap-users-interval"
|
||||||
|
|
||||||
appMaximumContentLength <- o .: "maximum-content-length"
|
appMaximumContentLength <- o .: "maximum-content-length"
|
||||||
|
|
||||||
appReloadTemplates <- o .:? "reload-templates" .!= defaultDev
|
appReloadTemplates <- o .:? "reload-templates" .!= defaultDev
|
||||||
|
|||||||
@ -51,7 +51,7 @@ import Control.Arrow as Utils ((>>>))
|
|||||||
import Control.Monad.Trans.Except (ExceptT(..), throwE, runExceptT)
|
import Control.Monad.Trans.Except (ExceptT(..), throwE, runExceptT)
|
||||||
import Control.Monad.Except (MonadError(..))
|
import Control.Monad.Except (MonadError(..))
|
||||||
import Control.Monad.Trans.Maybe as Utils (MaybeT(..))
|
import Control.Monad.Trans.Maybe as Utils (MaybeT(..))
|
||||||
import Control.Monad.Catch hiding (throwM)
|
import Control.Monad.Catch (catchIf)
|
||||||
|
|
||||||
import Language.Haskell.TH
|
import Language.Haskell.TH
|
||||||
import Language.Haskell.TH.Instances ()
|
import Language.Haskell.TH.Instances ()
|
||||||
@ -497,6 +497,12 @@ hoistMaybe = maybe mzero return
|
|||||||
catchIfMaybeT :: (MonadCatch m, Exception e) => (e -> Bool) -> m a -> MaybeT m a
|
catchIfMaybeT :: (MonadCatch m, Exception e) => (e -> Bool) -> m a -> MaybeT m a
|
||||||
catchIfMaybeT p act = catchIf p (lift act) (const mzero)
|
catchIfMaybeT p act = catchIf p (lift act) (const mzero)
|
||||||
|
|
||||||
|
catchMaybeT :: forall p m e a. (MonadCatch m, Exception e) => p e -> m a -> MaybeT m a
|
||||||
|
catchMaybeT _ act = catch (lift act) (const mzero :: e -> MaybeT m a)
|
||||||
|
|
||||||
|
catchMPlus :: forall p m e a. (MonadPlus m, MonadCatch m, Exception e) => p e -> m a -> m a
|
||||||
|
catchMPlus _ = handle (const mzero :: e -> m a)
|
||||||
|
|
||||||
mcons :: Maybe a -> [a] -> [a]
|
mcons :: Maybe a -> [a] -> [a]
|
||||||
mcons Nothing xs = xs
|
mcons Nothing xs = xs
|
||||||
mcons (Just x) xs = x:xs
|
mcons (Just x) xs = x:xs
|
||||||
|
|||||||
Reference in New Issue
Block a user