chore(model): remigrate ExternalAuth -> ExternalUser for more general data lookup; redefine lastSync timestamp semantics contd

This commit is contained in:
Sarah Vaupel 2024-03-01 12:03:38 +01:00
parent 40fe8ecfc6
commit f88e527fe4
11 changed files with 36 additions and 21 deletions

View File

@ -1534,7 +1534,7 @@ tagAccessPredicate AuthIsExternal = APDB $ \_ _ _ route _ -> exceptT return retu
let availableSources = error "tagAccessPredicate: no available sources yet" -- TODO: implement once config supports source idents let availableSources = error "tagAccessPredicate: no available sources yet" -- TODO: implement once config supports source idents
maybeTMExceptT (unauthorizedI MsgUnauthorizedExternal) $ do maybeTMExceptT (unauthorizedI MsgUnauthorizedExternal) $ do
Entity uid _ <- MaybeT $ getEntity referencedUser' Entity uid _ <- MaybeT $ getEntity referencedUser'
guardM . lift $ exists [ ExternalAuthUser ==. uid, ExternalAuthSource <-. availableSources ] guardM . lift $ exists [ ExternalUserUser ==. uid, ExternalUserSource <-. availableSources ]
return Authorized return Authorized
tagAccessPredicate AuthIsInternal = APDB $ \_ _ _ route _ -> exceptT return return $ do tagAccessPredicate AuthIsInternal = APDB $ \_ _ _ route _ -> exceptT return return $ do
referencedUser <- case route of referencedUser <- case route of

View File

@ -321,7 +321,6 @@ decodeUser now UserDefaultConf{..} upsertData = do
, userNotificationSettings = def , userNotificationSettings = def
, userCsvOptions = def , userCsvOptions = def
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userCreated = now
, userDisplayEmail = userEmail , userDisplayEmail = userEmail
, userMatrikelnummer = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS , userMatrikelnummer = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS
, userPostAddress = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS , userPostAddress = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS
@ -330,6 +329,8 @@ decodeUser now UserDefaultConf{..} upsertData = do
, userPrefersPostal = userDefaultPrefersPostal , userPrefersPostal = userDefaultPrefersPostal
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userCreated = now
, userLastSync = Just now
, .. , ..
} }
userUpdate = userUpdate =
@ -341,6 +342,7 @@ decodeUser now UserDefaultConf{..} upsertData = do
, UserMobile =. userMobile , UserMobile =. userMobile
, UserCompanyPersonalNumber =. userCompanyPersonalNumber , UserCompanyPersonalNumber =. userCompanyPersonalNumber
, UserCompanyDepartment =. userCompanyDepartment , UserCompanyDepartment =. userCompanyDepartment
, UserLastSync =. Just now
] ]
return (newUser, userUpdate) return (newUser, userUpdate)

View File

@ -90,6 +90,7 @@ fakeQualificationUsers (Entity qid Qualification{qualificationRefreshWithin}) (u
userDisplayName = Text.unwords $ firstNames <> [userSurname] userDisplayName = Text.unwords $ firstNames <> [userSurname]
userMatrikelnummer = Just "TESTUSER" userMatrikelnummer = Just "TESTUSER"
userCreated = now userCreated = now
userLastSync = Just now
userTokensIssuedAfter = Nothing userTokensIssuedAfter = Nothing
userFirstName = Text.unwords firstNames userFirstName = Text.unwords firstNames
userTitle = Nothing userTitle = Nothing

View File

@ -584,7 +584,7 @@ makeProfileData :: Entity User -> DB Widget
makeProfileData (Entity uid User{..}) = do makeProfileData (Entity uid User{..}) = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid) avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
externalAuths <- (\(Entity _ ExternalAuth{..}) -> ("" :: Text, externalAuthSource, externalAuthLastSync)) <<$>> selectList [ ExternalAuthUser ==. uid ] [] -- TODO: define and use user identification in ExternalAuth model externalUsers <- (\(Entity _ ExternalUser{..}) -> ("" :: Text, externalUserSource, externalUserLastSync)) <<$>> selectList [ ExternalUserUser ==. uid ] [] -- TODO: define and use user identification in ExternalUser model
-- avsCards <- maybe (pure mempty) (\a -> selectList [UserAvsCardPersonId ==. userAvsPersonId a] []) avsId -- avsCards <- maybe (pure mempty) (\a -> selectList [UserAvsCardPersonId ==. userAvsPersonId a] []) avsId
functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] [] functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] []

View File

@ -119,10 +119,10 @@ getQualificationSAPDirectR = do
E.where_ $ E.isJust (qual E.^. QualificationSapId) E.where_ $ E.isJust (qual E.^. QualificationSapId)
E.&&. E.isJust (user E.^. UserCompanyPersonalNumber) E.&&. E.isJust (user E.^. UserCompanyPersonalNumber)
E.where_ . E.exists $ do E.where_ . E.exists $ do
externalAuth <- E.from $ E.table @ExternalAuth externalUser <- E.from $ E.table @ExternalUser
E.where_ $ externalAuth E.^. ExternalAuthUser E.==. user E.^. UserId E.where_ $ externalUser E.^. ExternalUserUser E.==. user E.^. UserId
E.&&. externalAuth E.^. ExternalAuthSource `E.in_` E.valList ldapSources E.&&. externalUser E.^. ExternalUserSource `E.in_` E.valList ldapSources
E.&&. externalAuth E.^. ExternalAuthLastSync E.>=. E.val ldapCutoff E.&&. externalUser E.^. ExternalUserLastSync E.>=. E.val ldapCutoff
E.groupBy ( user E.^. UserCompanyPersonalNumber E.groupBy ( user E.^. UserCompanyPersonalNumber
, qualUser E.^. QualificationUserFirstHeld , qualUser E.^. QualificationUserFirstHeld
, qualUser E.^. QualificationUserValidUntil , qualUser E.^. QualificationUserValidUntil

View File

@ -125,8 +125,8 @@ postUsersR = do
icnReroute = text2widget " " <> toWgt (icon IconLetter) icnReroute = text2widget " " <> toWgt (icon IconLetter)
pure $ mconcat supervisors pure $ mconcat supervisors
, sortable (Just "last-login") (i18nCell MsgLastLogin) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastAuthentication , sortable (Just "last-login") (i18nCell MsgLastLogin) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastAuthentication
-- , sortable (Just "auth-ldap") (i18nCell MsgAuthMode) $ \DBRow{ dbrOutput = Entity _ User{..} } -> i18nCell userAuthentication -- TODO: reintroduce via ExternalAuth -- , sortable (Just "auth-ldap") (i18nCell MsgAuthMode) $ \DBRow{ dbrOutput = Entity _ User{..} } -> i18nCell userAuthentication -- TODO: reintroduce via ExternalUser
-- , sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation -- TODO: reintroduce via ExternalAuth -- , sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation -- TODO: reintroduce via ExternalUser
, flip foldMap universeF $ \function -> , flip foldMap universeF $ \function ->
sortable (Just $ SortingKey $ CI.mk $ toPathPiece function) (i18nCell function) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do sortable (Just $ SortingKey $ CI.mk $ toPathPiece function) (i18nCell function) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do
schools <- liftHandler . runDB . E.select . E.from $ \(school `E.InnerJoin` userFunction) -> do schools <- liftHandler . runDB . E.select . E.from $ \(school `E.InnerJoin` userFunction) -> do
@ -223,13 +223,13 @@ postUsersR = do
) )
-- , ( "auth-ldap" -- , ( "auth-ldap"
-- , SortColumn $ \user -> user E.^. UserAuthentication E.!=. E.val AuthLDAP -- , SortColumn $ \user -> user E.^. UserAuthentication E.!=. E.val AuthLDAP
-- ) -- TODO: reintroduce via ExternalAuth -- ) -- TODO: reintroduce via ExternalUser
, ( "last-login" , ( "last-login"
, SortColumn $ \user -> user E.^. UserLastAuthentication , SortColumn $ \user -> user E.^. UserLastAuthentication
) )
-- , ( "ldap-sync" -- , ( "ldap-sync"
-- , SortColumn $ \user -> user E.^. UserLastLdapSynchronisation -- , SortColumn $ \user -> user E.^. UserLastLdapSynchronisation
-- ) -- TODO: reintroduce via ExternalAuth -- ) -- TODO: reintroduce via ExternalUser
, ( "user-company" , ( "user-company"
, SortColumn $ \user -> E.subSelect $ E.from $ \(usrComp `E.InnerJoin` comp) -> do , SortColumn $ \user -> E.subSelect $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
@ -276,7 +276,7 @@ postUsersR = do
-- -> (user E.^. UserAuthentication E.==. E.val AuthLDAP) E.==. E.val crit -- -> (user E.^. UserAuthentication E.==. E.val AuthLDAP) E.==. E.val crit
-- | otherwise -- | otherwise
-- -> E.true -- -> E.true
-- ) -- TODO: reintroduce via ExternalAuth -- ) -- TODO: reintroduce via ExternalUser
, ( "school", FilterColumn $ \user criterion -> if , ( "school", FilterColumn $ \user criterion -> if
| Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool) | Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> let schools = E.valList (Set.toList criterion) in | otherwise -> let schools = E.valList (Set.toList criterion) in
@ -288,7 +288,7 @@ postUsersR = do
-- -> let minTime = minimum (criteria' :: NonNull (Set UTCTime)) -- -> let minTime = minimum (criteria' :: NonNull (Set UTCTime))
-- in E.maybe E.true (E.<=. E.val minTime) $ user E.^. UserLastLdapSynchronisation -- in E.maybe E.true (E.<=. E.val minTime) $ user E.^. UserLastLdapSynchronisation
-- | otherwise -> E.val True :: E.SqlExpr (E.Value Bool) -- | otherwise -> E.val True :: E.SqlExpr (E.Value Bool)
-- ) -- TODO: reintroduce via ExternalAuth -- ) -- TODO: reintroduce via ExternalUser
, ( "user-company", FilterColumn . E.mkExistsFilter $ \user criterion -> , ( "user-company", FilterColumn . E.mkExistsFilter $ \user criterion ->
E.from $ \(usrComp `E.InnerJoin` comp) -> do E.from $ \(usrComp `E.InnerJoin` comp) -> do
let testname = (E.val criterion :: E.SqlExpr (E.Value (CI Text))) `E.isInfixOf` let testname = (E.val criterion :: E.SqlExpr (E.Value (CI Text))) `E.isInfixOf`
@ -330,8 +330,8 @@ postUsersR = do
, prismAForm (singletonFilter "user-supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor) , prismAForm (singletonFilter "user-supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor)
, prismAForm (singletonFilter "school") mPrev $ aopt (lift `hoistField` selectFieldList schoolOptions) (fslI MsgCourseSchool) , prismAForm (singletonFilter "school") mPrev $ aopt (lift `hoistField` selectFieldList schoolOptions) (fslI MsgCourseSchool)
, prismAForm (singletonFilter "is-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgUserIsSupervisor) , prismAForm (singletonFilter "is-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgUserIsSupervisor)
-- , prismAForm (singletonFilter "auth-ldap" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` selectFieldList [(MsgAuthPWHash "", False), (MsgAuthLDAP, True)]) (fslI MsgAuthMode) -- TODO: reintroduce via ExternalAuth -- , prismAForm (singletonFilter "auth-ldap" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` selectFieldList [(MsgAuthPWHash "", False), (MsgAuthLDAP, True)]) (fslI MsgAuthMode) -- TODO: reintroduce via ExternalUser
-- , prismAForm (singletonFilter "ldap-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLdapSyncedBefore) -- TODO: reintroduce via ExternalAuth -- , prismAForm (singletonFilter "ldap-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLdapSyncedBefore) -- TODO: reintroduce via ExternalUser
] ]
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout } , dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
, dbtParams = DBParamsForm , dbtParams = DBParamsForm

View File

@ -120,9 +120,9 @@ dispatchHealthCheckLDAPAdmins = fmap HealthLDAPAdmins . yesodTimeout (^. _appHea
ldapAdminUsers' <- fmap (map E.unValue) . runDB . E.select . E.from $ \(user `E.InnerJoin` userFunction) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do ldapAdminUsers' <- fmap (map E.unValue) . runDB . E.select . E.from $ \(user `E.InnerJoin` userFunction) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
E.on $ user E.^. UserId E.==. userFunction E.^. UserFunctionUser E.on $ user E.^. UserId E.==. userFunction E.^. UserFunctionUser
E.where_ $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolAdmin E.where_ $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolAdmin
E.where_ . E.exists . E.from $ \externalAuth -> E.where_ $ E.where_ . E.exists . E.from $ \externalUser -> E.where_ $
externalAuth E.^. ExternalAuthUser E.==. user E.^. UserId externalUser E.^. ExternalUserUser E.==. user E.^. UserId
E.&&. externalAuth E.^. ExternalAuthSource `E.in_` E.valList currentLdapSources E.&&. externalUser E.^. ExternalUserSource `E.in_` E.valList currentLdapSources
return $ user E.^. UserIdent return $ user E.^. UserIdent
for (assertM' (not . null) ldapAdminUsers') $ \ldapAdminUsers -> do for (assertM' (not . null) ldapAdminUsers') $ \ldapAdminUsers -> do
let numAdmins = genericLength ldapAdminUsers let numAdmins = genericLength ldapAdminUsers

View File

@ -72,7 +72,7 @@ type AzureScopes = Set Text
type UserEduPersonPrincipalName = Text type UserEduPersonPrincipalName = Text
-- | Subset of the configuration settings of an authentication source that uniquely identify a given source -- | Subset of the configuration settings of an authentication source that uniquely identify a given source
-- | Used for uniquely storing ExternalAuth entries per user and source -- | Used for uniquely storing ExternalUser entries per user and source
data AuthSourceIdent data AuthSourceIdent
= AuthSourceIdAzure = AuthSourceIdAzure
{ authSourceIdAzureClientId :: UUID { authSourceIdAzureClientId :: UUID

View File

@ -77,5 +77,6 @@ addNewUser AddUserData{..} = do
, userPrefersPostal = audPrefersPostal , userPrefersPostal = audPrefersPostal
, userPinPassword = audPinPassword , userPinPassword = audPinPassword
, userMatrikelnummer = audMatriculation , userMatrikelnummer = audMatriculation
, userLastSync = Nothing -- TODO: combine add user with external sync?
} }
runDB $ insertUnique newUser runDB $ insertUnique newUser

View File

@ -123,13 +123,13 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<dt .deflist__dt> <dt .deflist__dt>
_{MsgAdminUserAuthentication} _{MsgAdminUserAuthentication}
<dd .deflist__dd> <dd .deflist__dd>
$if null externalAuths && is _Nothing userPasswordHash $if null externalUsers && is _Nothing userPasswordHash
_{MsgAuthKindNoLogin} _{MsgAuthKindNoLogin}
$else $else
<ul> <ul>
$if is _Just userPasswordHash $if is _Just userPasswordHash
<li>_{MsgAuthKindPWHash} <li>_{MsgAuthKindPWHash}
$forall (authIdent, sourceIdent, lsync) <- externalAuths $forall (authIdent, sourceIdent, lsync) <- externalUsers
<li> <li>
$case sourceIdent $case sourceIdent
$of AuthSourceIdAzure _clientId $of AuthSourceIdAzure _clientId

View File

@ -84,6 +84,7 @@ fillDb = do
{ userIdent = "G.Kleen@campus.lmu.de" { userIdent = "G.Kleen@campus.lmu.de"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Just now , userLastAuthentication = Just now
, userLastSync = Just now
, userTokensIssuedAfter = Just now , userTokensIssuedAfter = Just now
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "G.Kleen@campus.lmu.de" , userEmail = "G.Kleen@campus.lmu.de"
@ -122,6 +123,7 @@ fillDb = do
{ userIdent = "felix.hamann@campus.lmu.de" { userIdent = "felix.hamann@campus.lmu.de"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "noEmailKnown" , userEmail = "noEmailKnown"
@ -165,6 +167,7 @@ fillDb = do
{ userIdent = "jost@tcs.ifi.lmu.de" { userIdent = "jost@tcs.ifi.lmu.de"
, userPasswordHash = Just pwSimple , userPasswordHash = Just pwSimple
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just "12345678" , userMatrikelnummer = Just "12345678"
, userEmail = "S.Jost@Fraport.de" , userEmail = "S.Jost@Fraport.de"
@ -203,6 +206,7 @@ fillDb = do
{ userIdent = "max@campus.lmu.de" { userIdent = "max@campus.lmu.de"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Just now , userLastAuthentication = Just now
, userLastSync = Just now
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just "1299" , userMatrikelnummer = Just "1299"
, userEmail = "max@campus.lmu.de" , userEmail = "max@campus.lmu.de"
@ -241,6 +245,7 @@ fillDb = do
{ userIdent = "tester@campus.lmu.de" { userIdent = "tester@campus.lmu.de"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just "999" , userMatrikelnummer = Just "999"
, userEmail = "tester@campus.lmu.de" , userEmail = "tester@campus.lmu.de"
@ -279,6 +284,7 @@ fillDb = do
{ userIdent = "vaupel.sarah@campus.lmu.de" { userIdent = "vaupel.sarah@campus.lmu.de"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "vaupel.sarah@campus.lmu.de" , userEmail = "vaupel.sarah@campus.lmu.de"
@ -317,6 +323,7 @@ fillDb = do
{ userIdent = "Stephan.Barth@campus.lmu.de" { userIdent = "Stephan.Barth@campus.lmu.de"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "Stephan.Barth@lmu.de" , userEmail = "Stephan.Barth@lmu.de"
@ -355,6 +362,7 @@ fillDb = do
{ userIdent = "AVSID:996699" { userIdent = "AVSID:996699"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "E996699@fraport.de" , userEmail = "E996699@fraport.de"
@ -393,6 +401,7 @@ fillDb = do
{ userIdent = "AVSID:669966" { userIdent = "AVSID:669966"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "E669966@fraport.de" , userEmail = "E669966@fraport.de"
@ -431,6 +440,7 @@ fillDb = do
{ userIdent = "AVSID:6969" { userIdent = "AVSID:6969"
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "E6969@fraport.de" , userEmail = "E6969@fraport.de"
@ -507,6 +517,7 @@ fillDb = do
{ userIdent { userIdent
, userPasswordHash = Nothing , userPasswordHash = Nothing
, userLastAuthentication = Nothing , userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing , userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just userMatrikelnummer' , userMatrikelnummer = Just userMatrikelnummer'
, userEmail = userEmail' , userEmail = userEmail'