Merge branch 'master' into fradrive/api-avs

This commit is contained in:
Steffen Jost 2022-12-05 16:45:34 +01:00
commit a4716cb92f
16 changed files with 162 additions and 107 deletions

View File

@ -2,6 +2,8 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [26.6.4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.3...v26.6.4) (2022-12-02)
## [26.6.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.2...v26.6.3) (2022-11-30) ## [26.6.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.6.2...v26.6.3) (2022-11-30)

View File

@ -1,3 +1,3 @@
{ {
"version": "26.6.3" "version": "26.6.4"
} }

View File

@ -1,3 +1,3 @@
{ {
"version": "26.6.3" "version": "26.6.4"
} }

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "26.6.3", "version": "26.6.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "26.6.3", "version": "26.6.4",
"description": "", "description": "",
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@ -1,5 +1,5 @@
name: uniworx name: uniworx
version: 26.6.3 version: 26.6.4
dependencies: dependencies:
- base - base
- yesod - yesod

View File

@ -146,7 +146,7 @@ campusUserReTest' :: (MonadMask m, MonadLogger m, MonadUnliftIO m) => Failover (
campusUserReTest' pool doTest mode User{userIdent} campusUserReTest' pool doTest mode User{userIdent}
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUserReTest pool doTest mode (Creds apLdap (CI.original userIdent) []) = runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUserReTest pool doTest mode (Creds apLdap (CI.original userIdent) [])
campusUser :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList []) campusUser :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
campusUser pool mode creds = throwLeft =<< campusUserWith withLdapFailover pool mode creds campusUser pool mode creds = throwLeft =<< campusUserWith withLdapFailover pool mode creds
campusUser' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList [])) campusUser' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))

View File

@ -4,7 +4,8 @@
module Foundation.Yesod.Auth module Foundation.Yesod.Auth
( authenticate ( authenticate
, upsertCampusUser, upsertCampusUserByCn , ldapLookupAndUpsert
, upsertCampusUser
, decodeUserTest , decodeUserTest
, CampusUserConversionException(..) , CampusUserConversionException(..)
, campusUserFailoverMode, updateUserLanguage , campusUserFailoverMode, updateUserLanguage
@ -106,10 +107,10 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
| not isDummy -> res <$ update uid [ UserLastAuthentication =. Just now ] | not isDummy -> res <$ update uid [ UserLastAuthentication =. Just now ]
_other -> return res _other -> return res
$logDebugS "auth" $ tshow Creds{..} $logDebugS "auth" $ tshow Creds{..}
UniWorX{..} <- getYesod ldapPool' <- getsYesod $ view _appLdapPool
flip catches excHandlers $ case appLdapPool of flip catches excHandlers $ case ldapPool' of
Just ldapPool Just ldapPool
| Just upsertMode' <- upsertMode -> do | Just upsertMode' <- upsertMode -> do
ldapData <- campusUser ldapPool campusUserFailoverMode Creds{..} ldapData <- campusUser ldapPool campusUserFailoverMode Creds{..}
@ -152,14 +153,25 @@ _upsertCampusUserMode mMode cs@Creds{..}
defaultOther = apHash defaultOther = apHash
ldapLookupAndUpsert :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX, MonadMask m, MonadUnliftIO m) => Text -> SqlPersistT m (Entity User)
ldapLookupAndUpsert ident =
getsYesod (view _appLdapPool) >>= \case
Nothing -> throwM $ CampusUserLdapError $ LdapHostNotResolved "No LDAP configuration in Foundation."
Just ldapPool ->
campusUser'' ldapPool campusUserFailoverMode ident >>= \case
Nothing -> throwM CampusUserNoResult
Just ldapResponse -> upsertCampusUser UpsertCampusUserGuessUser ldapResponse
{- THIS FUNCION JUST DECODES, BUT IT DOES NOT QUERY LDAP!
upsertCampusUserByCn :: forall m. upsertCampusUserByCn :: forall m.
( MonadHandler m, HandlerSite m ~ UniWorX ( MonadHandler m, HandlerSite m ~ UniWorX
, MonadThrow m , MonadThrow m
) )
=> Text -> SqlPersistT m (Entity User) => Text -> SqlPersistT m (Entity User)
upsertCampusUserByCn persNo = upsertCampusUser UpsertCampusUserGuessUser [(ldapPrimaryKey,[Text.encodeUtf8 persNo])] upsertCampusUserByCn persNo = upsertCampusUser UpsertCampusUserGuessUser [(ldapPrimaryKey,[Text.encodeUtf8 persNo])]
-}
-- | Upsert User DB according to given LDAP data (does not query LDAP itself)
upsertCampusUser :: forall m. upsertCampusUser :: forall m.
( MonadHandler m, HandlerSite m ~ UniWorX ( MonadHandler m, HandlerSite m ~ UniWorX
, MonadThrow m , MonadThrow m
@ -208,7 +220,7 @@ decodeUserTest mbIdent ldapData = do
decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_) decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_)
decodeUser now UserDefaultConf{..} upsertMode ldapData = do decodeUser now UserDefaultConf{..} upsertMode ldapData = do
let let
userTelephone = decodeLdap ldapUserTelephone userTelephone = decodeLdap ldapUserTelephone
userMobile = decodeLdap ldapUserMobile userMobile = decodeLdap ldapUserMobile

View File

@ -19,7 +19,7 @@ import Handler.Utils.Avs
import Utils.Avs import Utils.Avs
-- Button needed only here -- Button needed only here
data ButtonAvsTest = BtnCheckLicences data ButtonAvsTest = BtnCheckLicences | BtnSynchLicences
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonAvsTest instance Universe ButtonAvsTest
instance Finite ButtonAvsTest instance Finite ButtonAvsTest
@ -27,8 +27,10 @@ instance Finite ButtonAvsTest
nullaryPathPiece ''ButtonAvsTest camelToPathPiece nullaryPathPiece ''ButtonAvsTest camelToPathPiece
instance Button UniWorX ButtonAvsTest where instance Button UniWorX ButtonAvsTest where
btnLabel BtnCheckLicences = "Check all licences" -- could be msg btnLabel BtnCheckLicences = "Check all licences" -- could be msg
btnClasses BtnCheckLicences = [BCIsButton, BCPrimary] btnLabel BtnSynchLicences = "Synchronize all licences" -- could be msg
btnClasses BtnCheckLicences = [BCIsButton, BCPrimary]
btnClasses BtnSynchLicences = [BCIsButton, BCDanger]
-- END Button -- END Button
@ -36,7 +38,7 @@ avsCardNoField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field
avsCardNoField = convertField AvsCardNo avsCardNo textField avsCardNoField = convertField AvsCardNo avsCardNo textField
avsInternalPersonalNoField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m AvsInternalPersonalNo avsInternalPersonalNoField :: Monad m => RenderMessage (HandlerSite m) FormMessage => Field m AvsInternalPersonalNo
avsInternalPersonalNoField = convertField (canonical . AvsInternalPersonalNo) avsInternalPersonalNo textField avsInternalPersonalNoField = convertField mkAvsInternalPersonalNo avsInternalPersonalNo textField
makeAvsPersonForm :: Maybe AvsQueryPerson -> Form AvsQueryPerson makeAvsPersonForm :: Maybe AvsQueryPerson -> Form AvsQueryPerson
makeAvsPersonForm tmpl = identifyForm FIDAvsQueryPerson . validateForm validateAvsQueryPerson $ \html -> makeAvsPersonForm tmpl = identifyForm FIDAvsQueryPerson . validateForm validateAvsQueryPerson $ \html ->
@ -180,16 +182,42 @@ postAdminAvsR = do
((qryLicRes, qryLicWgt), qryLicEnctype) <- runFormPost $ identifyForm FIDAvsQueryLicenceDiffs (buttonForm :: Form ButtonAvsTest) ((qryLicRes, qryLicWgt), qryLicEnctype) <- runFormPost $ identifyForm FIDAvsQueryLicenceDiffs (buttonForm :: Form ButtonAvsTest)
let procFormQryLic BtnCheckLicences = do let procFormQryLic btn = case btn of
res <- try checkLicences BtnCheckLicences -> do
case res of res <- try $ do
(Right True) -> allLicences <- throwLeftM avsQueryGetAllLicences
return $ Just [whamlet|<h2>Success:</h2> Licences sychronized.|] computeDifferingLicences allLicences
(Right False) -> case res of
return $ Just [whamlet|<h2>Error:</h2> Licences could not be synchronized, see error log.|] (Right diffs) -> do
(Left e) -> do let showLics l = Text.intercalate ", " $ fmap (tshow . avsLicencePersonID) $ Set.toList $ Set.filter ((l ==) . avsLicenceRampLicence) diffs
let msg = tshow (e :: SomeException) r_grant = showLics AvsLicenceRollfeld
return $ Just [whamlet|<h2>Error:</h2> #{msg}|] f_set = showLics AvsLicenceVorfeld
revoke = showLics AvsNoLicence
return $ Just [whamlet|
<h2>Licence check differences:
<h3>Grant R:
<p>
#{r_grant}
<h3>Set to F:
<p>
#{f_set}
<h3>Revoke licence:
<p>
#{revoke}
|]
(Left e) -> do
let msg = tshow (e :: SomeException)
return $ Just [whamlet|<h2>Licence check error:</h2> #{msg}|]
BtnSynchLicences -> do
res <- try checkLicences
case res of
(Right True) ->
return $ Just [whamlet|<h2>Success:</h2> Licences sychronized.|]
(Right False) ->
return $ Just [whamlet|<h2>Error:</h2> Licences could not be synchronized, see error log.|]
(Left e) -> do
let msg = tshow (e :: SomeException)
return $ Just [whamlet|<h2>Licence synchronisation error:</h2> #{msg}|]
mbQryLic <- formResultMaybe qryLicRes procFormQryLic mbQryLic <- formResultMaybe qryLicRes procFormQryLic
actionUrl <- fromMaybe AdminAvsR <$> getCurrentRoute actionUrl <- fromMaybe AdminAvsR <$> getCurrentRoute

View File

@ -10,67 +10,46 @@ module Handler.Admin.Ldap
) where ) where
import Import import Import
import qualified Control.Monad.State.Class as State -- import qualified Control.Monad.State.Class as State
-- import Data.Aeson (encode) -- import Data.Aeson (encode)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
-- import qualified Data.Set as Set -- import qualified Data.Set as Set
import Foundation.Yesod.Auth (decodeUserTest) import Foundation.Yesod.Auth (decodeUserTest,ldapLookupAndUpsert,campusUserFailoverMode,CampusUserConversionException())
import Handler.Utils import Handler.Utils
import qualified Ldap.Client as Ldap import qualified Ldap.Client as Ldap
import Auth.LDAP import Auth.LDAP
data LdapQueryPerson = LdapQueryPerson
{ ldapQueryIdent :: Maybe Text
-- , ldapQueryName :: Maybe Text
, ldapQueryPNum :: Maybe Text
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeLdapPersonForm :: Maybe LdapQueryPerson -> Form LdapQueryPerson
makeLdapPersonForm tmpl = validateForm validateLdapQueryPerson $ \html ->
flip (renderAForm FormStandard) html $ LdapQueryPerson
<$> aopt textField (fslI MsgAdminUserIdent) (ldapQueryIdent <$> tmpl)
-- <*> aopt textField (fslI MsgAdminUserSurname) (ldapQueryName <$> tmpl)
<*> aopt textField (fslI MsgAdminUserFPersonalNumber) (ldapQueryPNum <$> tmpl)
validateLdapQueryPerson :: FormValidator LdapQueryPerson Handler ()
validateLdapQueryPerson = do
LdapQueryPerson{..} <- State.get
guardValidation MsgAvsQueryEmpty $
is _Just ldapQueryIdent ||
-- is _Just ldapQueryName ||
is _Just ldapQueryPNum
getAdminLdapR, postAdminLdapR :: Handler Html getAdminLdapR, postAdminLdapR :: Handler Html
getAdminLdapR = postAdminLdapR getAdminLdapR = postAdminLdapR
postAdminLdapR = do postAdminLdapR = do
((presult, pwidget), penctype) <- runFormPost $ makeLdapPersonForm Nothing ((presult, pwidget), penctype) <- runFormPost $ identifyForm ("adminLdapLookup"::Text) $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
let procFormPerson :: LdapQueryPerson -> Handler (Maybe (Ldap.AttrList []))
procFormPerson LdapQueryPerson{..} = do
ldapPool' <- getsYesod $ view _appLdapPool
if isNothing ldapPool'
then addMessage Warning $ text2Html "LDAP Configuration missing."
else addMessage Info $ text2Html "Input for LDAP test received."
fmap join . for ldapPool' $ \ldapPool -> do
ldapData <- if | Just lqi <- ldapQueryIdent -> campusUser'' ldapPool FailoverUnlimited lqi
| Just lqn <- ldapQueryPNum -> campusUserMatr' ldapPool FailoverUnlimited lqn
| otherwise -> addMessageI Error MsgAvsQueryEmpty >> pure Nothing
decodedErr <- decodeUserTest (CI.mk <$> ldapQueryIdent) $ concat ldapData
whenIsLeft decodedErr $ addMessageI Error
return ldapData
let procFormPerson :: Text -> Handler (Maybe (Ldap.AttrList []))
procFormPerson lid = do
ldapPool' <- getsYesod $ view _appLdapPool
case ldapPool' of
Nothing -> addMessage Error (text2Html "LDAP Configuration missing.") >> return Nothing
Just ldapPool -> do
addMessage Info $ text2Html "Input for LDAP test received."
ldapData <- campusUser'' ldapPool campusUserFailoverMode lid
decodedErr <- decodeUserTest (pure $ CI.mk lid) $ concat ldapData
whenIsLeft decodedErr $ addMessageI Error
return ldapData
mbLdapData <- formResultMaybe presult procFormPerson mbLdapData <- formResultMaybe presult procFormPerson
((uresult, uwidget), uenctype) <- runFormPost $ identifyForm ("adminLdapUpsert"::Text) $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
let procFormUpsert :: Text -> Handler (Maybe (Either CampusUserConversionException (Entity User)))
procFormUpsert lid = pure <$> runDB (try $ ldapLookupAndUpsert lid)
mbLdapUpsert <- formResultMaybe uresult procFormUpsert
actionUrl <- fromMaybe AdminLdapR <$> getCurrentRoute actionUrl <- fromMaybe AdminLdapR <$> getCurrentRoute
siteLayoutMsg MsgMenuLdap $ do siteLayoutMsg MsgMenuLdap $ do
setTitleI MsgMenuLdap setTitleI MsgMenuLdap
@ -78,7 +57,10 @@ postAdminLdapR = do
{ formAction = Just $ SomeRoute actionUrl { formAction = Just $ SomeRoute actionUrl
, formEncoding = penctype , formEncoding = penctype
} }
upsertForm = wrapForm uwidget def
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = uenctype
}
presentUtf8 lv = Text.intercalate ", " (either tshow id . Text.decodeUtf8' <$> lv) presentUtf8 lv = Text.intercalate ", " (either tshow id . Text.decodeUtf8' <$> lv)
presentLatin1 lv = Text.intercalate ", " ( Text.decodeLatin1 <$> lv) presentLatin1 lv = Text.intercalate ", " ( Text.decodeLatin1 <$> lv)

View File

@ -8,9 +8,10 @@
module Handler.Utils.Avs module Handler.Utils.Avs
( upsertAvsUser, upsertAvsUserById, upsertAvsUserByCard ( upsertAvsUser, upsertAvsUserById, upsertAvsUserByCard
-- , getLicence, getLicenceDB, getLicenceByAvsId -- not supported by interface -- , getLicence, getLicenceDB, getLicenceByAvsId -- not supported by interface
, setLicence, setLicenceAvs, setLicencesAvs , setLicence, setLicenceAvs, setLicencesAvs, computeDifferingLicences
, checkLicences , checkLicences
, lookupAvsUser, lookupAvsUsers , lookupAvsUser, lookupAvsUsers
, AvsException(..)
) where ) where
import Import import Import
@ -25,7 +26,7 @@ import qualified Data.Map as Map
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
-- import Auth.LDAP (ldapUserPrincipalName) -- import Auth.LDAP (ldapUserPrincipalName)
import Foundation.Yesod.Auth (upsertCampusUserByCn,CampusUserConversionException()) import Foundation.Yesod.Auth (ldapLookupAndUpsert, CampusUserConversionException())
import Handler.Utils.Company import Handler.Utils.Company
import Handler.Users.Add import Handler.Users.Add
@ -111,24 +112,35 @@ setLicenceAvs apid lic = do
let req = Set.singleton $ AvsPersonLicence { avsLicenceRampLicence = lic, avsLicencePersonID = apid } let req = Set.singleton $ AvsPersonLicence { avsLicenceRampLicence = lic, avsLicencePersonID = apid }
setLicencesAvs req setLicencesAvs req
--setLicencesAvs :: Set AvsPersonLicence -> Handler Bool --setLicencesAvs :: Set AvsPersonLicence -> Handler Bool
setLicencesAvs :: (MonadHandler m, MonadThrow m, HandlerSite m ~ UniWorX) => setLicencesAvs :: (MonadHandler m, MonadThrow m, HandlerSite m ~ UniWorX) =>
Set AvsPersonLicence -> m Bool Set AvsPersonLicence -> m Bool
setLicencesAvs pls = do setLicencesAvs persLics = do
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery AvsQuery{avsQuerySetLicences=aqsl} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
response <- throwLeftM $ avsQuerySetLicences $ AvsQuerySetLicences pls aux aqsl True persLics
case response of where
AvsResponseSetLicencesError{..} -> do aux aqsl batch0_ok pls
let msg = "Set AVS licences failed utterly: " <> avsResponseSetLicencesStatus <> ". Details: " <> cropText avsResponseSetLicencesMessage | Set.null pls = return batch0_ok
$logErrorS "AVS" msg | otherwise = do
throwM $ AvsSetLicencesFailed avsResponseSetLicencesStatus let (batch1, batch2) = Set.splitAt avsMaxSetLicenceAtOnce pls
response <- throwLeftM $ aqsl $ AvsQuerySetLicences batch1
case response of
AvsResponseSetLicencesError{..} -> do
let msg = "Set AVS licences failed utterly: " <> avsResponseSetLicencesStatus <> ". Details: " <> cropText avsResponseSetLicencesMessage
$logErrorS "AVS" msg
throwM $ AvsSetLicencesFailed avsResponseSetLicencesStatus
AvsResponseSetLicences msgs -> do
let (ok,bad') = Set.partition (sloppyBool . avsResponseSuccess) msgs
ok_ids = Set.map avsResponsePersonID ok
bad = Map.withoutKeys (setToMap avsResponsePersonID bad') ok_ids -- it is possible to receive an id multiple times, with only one success, but this is sufficient
batch1_ok = length ok == length batch1
forM_ bad $ \AvsLicenceResponse { avsResponsePersonID=api, avsResponseMessage=msg} ->
$logErrorS "AVS" $ "Set AVS Licence failed for " <> tshow api <> " due to " <> cropText msg
-- TODO: Admin Error page
aux aqsl (batch0_ok && batch1_ok) batch2 -- yay for tail recursion (TODO: maybe refactor?)
AvsResponseSetLicences msgs -> do
let (ok,bad) = Set.partition (sloppyBool . avsResponseSuccess) msgs
forM_ bad $ \AvsLicenceResponse { avsResponsePersonID=api, avsResponseMessage=msg} ->
$logErrorS "AVS" $ "Set AVS Licence failed for " <> tshow api <> " due to " <> cropText msg
-- TODO: Admin Error page
return $ length ok == length pls
-- | Retrieve all currently valid driving licences and check against our database -- | Retrieve all currently valid driving licences and check against our database
-- Only react to changes as compared to last seen status in avs.model -- Only react to changes as compared to last seen status in avs.model
@ -216,20 +228,17 @@ computeDifferingLicences (AvsResponseGetLicences licences) = do
<> Set.map (AvsPersonLicence AvsLicenceVorfeld) setTo1 <> Set.map (AvsPersonLicence AvsLicenceVorfeld) setTo1
<> Set.map (AvsPersonLicence AvsLicenceRollfeld) setTo2 <> Set.map (AvsPersonLicence AvsLicenceRollfeld) setTo2
-- | Always update AVS Data
upsertAvsUser :: Text -> Handler (Maybe UserId) upsertAvsUser :: Text -> Handler (Maybe UserId) -- TODO: change to Entity
upsertAvsUser (discernAvsCardPersonalNo -> Just someid) = upsertAvsUserByCard someid -- Note: Right case is a number, it could be AvsCardNumber or AvsInternalPersonalNumber; we cannot know, but the latter is much more likely and useful to users! upsertAvsUser (discernAvsCardPersonalNo -> Just someid) = upsertAvsUserByCard someid -- Note: Right case is a number, it could be AvsCardNumber or AvsInternalPersonalNumber; we cannot know, but the latter is much more likely and useful to users!
upsertAvsUser _other = return Nothing -- TODO: attempt LDAP lookup to find by eMail; merely for convenience, not necessary right now upsertAvsUser otherId = -- attempt LDAP lookup to find by eMail
{- maybe this code helps? try (runDB $ ldapLookupAndUpsert otherId) >>= \case
upsRes :: Either CampusUserConversionException (Entity User) Right Entity{entityVal=User{userCompanyPersonalNumber=Just persNo}} -> upsertAvsUserByCard (Right $ mkAvsInternalPersonalNo persNo)
<- try $ upsertCampusUserByOther persNo Left (_err::SomeException) -> return Nothing -- TODO: ; merely for convenience, not necessary right now
case upsRes of _ -> return Nothing
Right Entity{entityKey=uid} -> insertUniqueEntity $ UserAvs api uid
_other -> return mbuid -- ==Nothing -- user could not be created somehow
-}
-- | Given CardNo or internal Number, retrieve UserId. Create non-existing users, if possible. -- | Given CardNo or internal Number, retrieve UserId. Create non-existing users, if possible. Always update.
-- Throws errors if the avsInterface in unavailable or the user is non-unique within external AVS DB. -- Throws errors if the avsInterface in unavailable or the user is non-unique within external AVS DB.
upsertAvsUserByCard :: Either AvsFullCardNo AvsInternalPersonalNo -> Handler (Maybe UserId) -- Idee: Eingabe ohne Punkt is AvsInternalPersonalNo mit Punkt is Ausweisnummer?! upsertAvsUserByCard :: Either AvsFullCardNo AvsInternalPersonalNo -> Handler (Maybe UserId) -- Idee: Eingabe ohne Punkt is AvsInternalPersonalNo mit Punkt is Ausweisnummer?!
upsertAvsUserByCard persNo = do upsertAvsUserByCard persNo = do
@ -241,11 +250,12 @@ upsertAvsUserByCard persNo = do
case Set.elems adps of case Set.elems adps of
[] -> throwM AvsPersonSearchEmpty [] -> throwM AvsPersonSearchEmpty
(_:_:_) -> throwM AvsPersonSearchAmbiguous (_:_:_) -> throwM AvsPersonSearchAmbiguous
[AvsDataPerson{avsPersonPersonID=appi}] -> do [AvsDataPerson{avsPersonPersonID=api}] -> upsertAvsUserById api -- always trigger an update
mbuid <- runDB $ getBy $ UniqueUserAvsId appi -- do
case mbuid of -- mbuid <- runDB $ getBy $ UniqueUserAvsId api
(Just (Entity _ UserAvs{userAvsUser=uau})) -> return $ Just uau -- case mbuid of
Nothing -> upsertAvsUserById appi -- (Just (Entity _ UserAvs{userAvsUser=uau})) -> return $ Just uau
-- Nothing -> upsertAvsUserById api
@ -259,13 +269,15 @@ upsertAvsUserById api = do
case (mbuid, mbapd) of case (mbuid, mbapd) of
(Nothing, Just AvsDataPerson{..}) -- FRADriver User does not exist yet, but found in AVS and has Internal Personal Number (Nothing, Just AvsDataPerson{..}) -- FRADriver User does not exist yet, but found in AVS and has Internal Personal Number
| Just (avsInternalPersonalNo -> persNo) <- canonical avsPersonInternalPersonalNo -> do | Just (avsInternalPersonalNo -> persNo) <- canonical avsPersonInternalPersonalNo -> do
$logInfoS "AVS" $ "Creating new user with avsInternalPersonalNo " <> tshow persNo
candidates <- selectKeysList [UserCompanyPersonalNumber ==. Just persNo] [] candidates <- selectKeysList [UserCompanyPersonalNumber ==. Just persNo] []
case candidates of case candidates of
[uid] -> insertUniqueEntity $ UserAvs api uid [uid] -> $logInfoS "AVS" "Matching user found, linking." >> insertUniqueEntity (UserAvs api uid)
(_:_) -> throwM AvsUserAmbiguous (_:_) -> throwM AvsUserAmbiguous
[] -> do [] -> do
upsRes :: Either CampusUserConversionException (Entity User) upsRes :: Either CampusUserConversionException (Entity User)
<- try $ upsertCampusUserByCn persNo <- try $ ldapLookupAndUpsert persNo
$logInfoS "AVS" $ "No matching user found. attempted LDAP upsert returned: " <> tshow upsRes
case upsRes of case upsRes of
Right Entity{entityKey=uid} -> insertUniqueEntity $ UserAvs api uid -- pin/addr are updated in next step anyway Right Entity{entityKey=uid} -> insertUniqueEntity $ UserAvs api uid -- pin/addr are updated in next step anyway
_other -> return mbuid -- ==Nothing -- user could not be created somehow _other -> return mbuid -- ==Nothing -- user could not be created somehow

View File

@ -86,8 +86,13 @@ newtype AvsInternalPersonalNo = AvsInternalPersonalNo { avsInternalPersonalNo ::
deriving newtype (NFData, PathPiece, PersistField, PersistFieldSql, Csv.ToField, Csv.FromField) deriving newtype (NFData, PathPiece, PersistField, PersistFieldSql, Csv.ToField, Csv.FromField)
instance E.SqlString AvsInternalPersonalNo instance E.SqlString AvsInternalPersonalNo
-- AvsInternalPersonalNo is an untagged Text with respect to FromJSON/ToJSON, as needed by AVS API -- AvsInternalPersonalNo is an untagged Text with respect to FromJSON/ToJSON, as needed by AVS API
normalizeAvsInternalPersonalNo :: Text -> Text normalizeAvsInternalPersonalNo :: Text -> Text
normalizeAvsInternalPersonalNo = Text.dropWhile (\c -> '0' == c || Char.isSpace c) normalizeAvsInternalPersonalNo = Text.dropWhile (\c -> '0' == c || Char.isSpace c)
mkAvsInternalPersonalNo :: Text -> AvsInternalPersonalNo
mkAvsInternalPersonalNo = AvsInternalPersonalNo . normalizeAvsInternalPersonalNo
instance Canonical AvsInternalPersonalNo where instance Canonical AvsInternalPersonalNo where
canonical (AvsInternalPersonalNo ipn) = AvsInternalPersonalNo $ Text.dropWhile (\c -> '0' == c || Char.isSpace c) ipn canonical (AvsInternalPersonalNo ipn) = AvsInternalPersonalNo $ Text.dropWhile (\c -> '0' == c || Char.isSpace c) ipn
instance FromJSON AvsInternalPersonalNo where instance FromJSON AvsInternalPersonalNo where
@ -163,7 +168,7 @@ readAvsFullCardNo _ = Nothing
discernAvsCardPersonalNo :: Text -> Maybe (Either AvsFullCardNo AvsInternalPersonalNo) -- Just implies it is a whole number or decimal with one digit after the point discernAvsCardPersonalNo :: Text -> Maybe (Either AvsFullCardNo AvsInternalPersonalNo) -- Just implies it is a whole number or decimal with one digit after the point
discernAvsCardPersonalNo (Text.span Char.isDigit -> (c, pv)) discernAvsCardPersonalNo (Text.span Char.isDigit -> (c, pv))
| Text.null pv | Text.null pv
= Just $ Right $ AvsInternalPersonalNo c = Just $ Right $ mkAvsInternalPersonalNo c
| not $ Text.null c | not $ Text.null c
, Just ('.', v) <- Text.uncons pv , Just ('.', v) <- Text.uncons pv
, Just (Char.isDigit -> True, "") <- Text.uncons v , Just (Char.isDigit -> True, "") <- Text.uncons v

View File

@ -788,6 +788,9 @@ partitionKeysEither = over _2 (Map.mapKeysMonotonic . view $ singular _Right) .
mapFromSetM :: Applicative m => (k -> m v) -> Set k -> m (Map k v) mapFromSetM :: Applicative m => (k -> m v) -> Set k -> m (Map k v)
mapFromSetM = (sequenceA .) . Map.fromSet mapFromSetM = (sequenceA .) . Map.fromSet
setToMap :: (Ord k) => (v -> k) -> Set v -> Map k v
setToMap mkKey = Map.fromList . fmap (\x -> (mkKey x, x)) . Set.toList
mapFM :: (Applicative m, Ord k, Finite k) => (k -> m v) -> m (Map k v) mapFM :: (Applicative m, Ord k, Finite k) => (k -> m v) -> m (Map k v)
mapFM = sequenceA . mapF mapFM = sequenceA . mapF

View File

@ -27,6 +27,8 @@ type AVSPersonStatus = "PersonStatus" :> ReqBody '[JSON] AvsQueryStatus :> Po
type AVSGetRampLicences = "RampDrivingLicenceInfo" :> ReqBody '[JSON] AvsQueryGetLicences :> Post '[JSON] AvsResponseGetLicences type AVSGetRampLicences = "RampDrivingLicenceInfo" :> ReqBody '[JSON] AvsQueryGetLicences :> Post '[JSON] AvsResponseGetLicences
type AVSSetRampLicences = "RampDrivingLicence" :> ReqBody '[JSON] AvsQuerySetLicences :> Post '[JSON] AvsResponseSetLicences type AVSSetRampLicences = "RampDrivingLicence" :> ReqBody '[JSON] AvsQuerySetLicences :> Post '[JSON] AvsResponseSetLicences
avsMaxSetLicenceAtOnce :: Int
avsMaxSetLicenceAtOnce = 99 -- maximum input set size for avsQuerySetLicences as enforced by AVS
avsApi :: Proxy AVS avsApi :: Proxy AVS
avsApi = Proxy avsApi = Proxy

View File

@ -22,3 +22,12 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
UTF8: #{presentUtf8 lv} UTF8: #{presentUtf8 lv}
&#8212; &#8212;
Latin: #{presentLatin1 lv} Latin: #{presentLatin1 lv}
<section>
<p>
LDAP Upsert user in DB:
^{upsertForm}
$maybe answer <- mbLdapUpsert
<h1>
Antwort: #
<p>
#{tshow answer}

View File

@ -15,8 +15,8 @@ instance Arbitrary SloppyBool where
shrink (SloppyBool x) = SloppyBool <$> shrink x shrink (SloppyBool x) = SloppyBool <$> shrink x
instance Arbitrary AvsInternalPersonalNo where instance Arbitrary AvsInternalPersonalNo where
arbitrary = canonical . AvsInternalPersonalNo <$> arbitrary arbitrary = mkAvsInternalPersonalNo <$> arbitrary
shrink (AvsInternalPersonalNo x) = canonical . AvsInternalPersonalNo <$> shrink x shrink (AvsInternalPersonalNo x) = mkAvsInternalPersonalNo <$> shrink x
instance Arbitrary AvsPersonId where instance Arbitrary AvsPersonId where
arbitrary = AvsPersonId <$> arbitrary arbitrary = AvsPersonId <$> arbitrary