chore(auth): rewrote authenticate (still WIP)

This commit is contained in:
Sarah Vaupel 2024-02-18 05:06:23 +01:00
parent a0e7b2f96c
commit bf13473954
4 changed files with 321 additions and 238 deletions

View File

@ -34,8 +34,8 @@ AuthSourceLdap
-- | User authentication data, source-agnostic data -- | User authentication data, source-agnostic data
UserAuth UserAuth
ident UserIdent -- Human-readable text uniquely identifying a user ident UserIdent -- Human-readable text uniquely identifying a user
lastLogin UTCTime -- When did the corresponding User last authenticate using this entry? lastLogin UTCTime Maybe -- When did the corresponding User last authenticate using this entry?
lastSync UTCTime Maybe -- When was the corresponding User entry last synced with any external source? -- TODO rethink lastSync UTCTime Maybe -- When was the corresponding User entry last synced with any external source?
Primary ident Primary ident
UniqueAuthentication ident UniqueAuthentication ident
deriving Show Eq Ord Generic deriving Show Eq Ord Generic

View File

@ -172,7 +172,7 @@ instance YesodAuth UniWorX where
BearerToken{..} <- MaybeT . liftHandler $ runDBRead maybeBearerToken BearerToken{..} <- MaybeT . liftHandler $ runDBRead maybeBearerToken
hoistMaybe bearerImpersonate hoistMaybe bearerImpersonate
-- TODO: update? -- TODO: update to new AuthId!
instance YesodAuthPersist UniWorX where instance YesodAuthPersist UniWorX where
getAuthEntity :: (HasCallStack, MonadHandler m, HandlerSite m ~ UniWorX) => UserId -> m (Maybe User) getAuthEntity :: (HasCallStack, MonadHandler m, HandlerSite m ~ UniWorX) => UserId -> m (Maybe User)
getAuthEntity = liftHandler . runDBRead . get getAuthEntity = liftHandler . runDBRead . get

View File

@ -4,8 +4,11 @@
module Foundation.Types module Foundation.Types
( UpsertUserMode(..) ( UpsertUserMode(..)
, _UpsertUserLoginLdap, _UpsertUserLoginAzure, _UpsertUserLoginDummy, _UpsertUserLoginOther, _UpsertUserSync, _UpsertUserGuessUser , _UpsertUserLogin, _UpsertUserLoginDummy, _UpsertUserLoginOther, _UpsertUserSync, _UpsertUserGuessUser
, _upsertUserLdapSource, _upsertUserLdapData, _upsertUserAzureSource, _upsertUserAzureData, _upsertUserIdent , _upsertUserSource, _upsertUserIdent
, UpsertUserData(..)
, _UpsertUserDataAzure, _UpsertUserDataLdap
, _upsertUserAzureConf, _upsertUserAzureData, _upsertUserLdapConf, _upsertUserLdapData
) where ) where
import Import.NoFoundation import Import.NoFoundation
@ -14,25 +17,27 @@ import qualified Ldap.Client as Ldap
data UpsertUserMode data UpsertUserMode
= UpsertUserLoginLdap = UpsertUserLogin { upsertUserSource :: Text } -- TODO: use type synonym?
{ upsertUserLdapSource :: AuthSourceLdapId | UpsertUserLoginDummy { upsertUserIdent :: UserIdent }
, upsertUserLdapData :: Ldap.AttrList [] | UpsertUserLoginOther { upsertUserIdent :: UserIdent } -- does not allow further login
} | UpsertUserSync { upsertUserIdent :: UserIdent }
| UpsertUserLoginAzure
{ upsertUserAzureSource :: AuthSourceAzureId
, upsertUserAzureData :: [(Text, [ByteString])] -- TODO: use type synonym?
}
| UpsertUserLoginDummy
{ upsertUserIdent :: UserIdent
}
| UpsertUserLoginOther -- does not allow further login
{ upsertUserIdent :: UserIdent
}
| UpsertUserSync
{ upsertUserIdent :: UserIdent
}
| UpsertUserGuessUser | UpsertUserGuessUser
deriving (Eq, Ord, Read, Show, Generic) deriving (Show)
makeLenses_ ''UpsertUserMode makeLenses_ ''UpsertUserMode
makePrisms ''UpsertUserMode makePrisms ''UpsertUserMode
data UpsertUserData
= UpsertUserDataAzure
{ upsertUserAzureConf :: AzureConf
, upsertUserAzureData :: [(Text, [ByteString])] -- TODO: use type synonym?
}
| UpsertUserDataLdap
{ upsertUserLdapConf :: LdapConf
, upsertUserLdapData :: Ldap.AttrList []
}
deriving (Show)
makeLenses_ ''UpsertUserData
makePrisms ''UpsertUserData

View File

@ -4,9 +4,9 @@
module Foundation.Yesod.Auth module Foundation.Yesod.Auth
( authenticate ( authenticate
, ldapLookupAndUpsert , ldapLookupAndUpsert -- TODO generalize
, upsertUser , upsertUser
, decodeLdapUserTest, decodeAzureUserTest , decodeUserTest
, UserConversionException(..) , UserConversionException(..)
, updateUserLanguage , updateUserLanguage
) where ) where
@ -20,13 +20,13 @@ import Auth.PWHash (apHash)
import qualified Control.Monad.Catch as C (Handler(..)) import qualified Control.Monad.Catch as C (Handler(..))
-- import qualified Data.Aeson as Json (encode)
import qualified Data.ByteString as ByteString import qualified Data.ByteString as ByteString
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set import qualified Data.Set as Set
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.UUID as UUID
import Foundation.Authorization (AuthorizationCacheKey(..)) import Foundation.Authorization (AuthorizationCacheKey(..))
import Foundation.I18n import Foundation.I18n
@ -55,7 +55,7 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
userAuthConf <- getsYesod $ view _appUserAuthConf userAuthConf <- getsYesod $ view _appUserAuthConf
let let
uAuth = UniqueExternalAuth $ CI.mk credsIdent uAuth = UniqueAuthentication $ CI.mk credsIdent
upsertMode = creds ^? _upsertUserMode upsertMode = creds ^? _upsertUserMode
isDummy = is (_Just . _UpsertUserLoginDummy) upsertMode isDummy = is (_Just . _UpsertUserLoginDummy) upsertMode
@ -96,29 +96,29 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
acceptExisting = do acceptExisting = do
res <- maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth res <- maybe (UserError $ IdentifierNotFound credsIdent) (Authenticated . entityKey) <$> getBy uAuth
case res of case res of
Authenticated euid Authenticated uid
-> associateUserSchoolsByTerms euid -> associateUserSchoolsByTerms uid
_other _other
-> return () -> return ()
case res of case res of
Authenticated uid Authenticated uid
| not isDummy -> res <$ update euid [ ExternalUserLastAuth =. Just now ] | not isDummy -> res <$ update uid [ UserAuthLastLogin =. Just now ]
_other -> return res _other -> return res
$logDebugS "Auth" $ tshow Creds{..} $logDebugS "Auth" $ tshow Creds{..}
flip catches excHandlers $ case userAuthConf of flip catches excHandlers $ case userAuthConf of
UserAuthConfSingleSource (AuthSourceConfAzureAdV2 azureConf) UserAuthConfSingleSource (AuthSourceConfAzureAdV2 upsertUserAzureConf)
| Just upsertMode' <- upsertMode -> do | Just upsertMode' <- upsertMode -> do
azureData <- azureUser azureConf Creds{..} upsertUserAzureData <- azureUser upsertUserAzureConf Creds{..}
$logDebugS "AuthAzure" $ "Successful Azure lookup: " <> tshow azureData $logDebugS "AuthAzure" $ "Successful Azure lookup: " <> tshow upsertUserAzureData
Authenticated . entityKey <$> upsertAzureUser upsertMode' azureData Authenticated . entityKey <$> upsertUser upsertMode' UpsertUserDataAzure{..}
UserAuthConfSingleSource (AuthSourceConfLdap _) UserAuthConfSingleSource (AuthSourceConfLdap upsertUserLdapConf)
| Just upsertMode' <- upsertMode -> do | Just upsertMode' <- upsertMode -> do
ldapPool <- fmap (fromMaybe $ error "No LDAP Pool") . getsYesod $ view _appLdapPool ldapPool <- fmap (fromMaybe $ error "No LDAP Pool") . getsYesod $ view _appLdapPool
ldapData <- ldapUser ldapPool Creds{..} -- ldapUserWith withLdap ldapPool FailoverNone Creds{..} upsertUserLdapData <- ldapUser ldapPool Creds{..}
$logDebugS "AuthLDAP" $ "Successful LDAP lookup: " <> tshow ldapData $logDebugS "AuthLDAP" $ "Successful LDAP lookup: " <> tshow upsertUserLdapData
Authenticated . entityKey <$> upsertLdapUser upsertMode' ldapData Authenticated . entityKey <$> upsertUser upsertMode' UpsertUserDataLdap{..}
_other _other
-> acceptExisting -> acceptExisting
@ -136,17 +136,16 @@ data UserConversionException
deriving anyclass (Exception) deriving anyclass (Exception)
-- TODO: this is probably not a sane traversal anymore...
_upsertUserMode :: Traversal' (Creds UniWorX) UpsertUserMode _upsertUserMode :: Traversal' (Creds UniWorX) UpsertUserMode
_upsertUserMode mMode cs@Creds{..} _upsertUserMode mMode cs@Creds{..}
| credsPlugin == apDummy = setMode <$> mMode (UpsertUserLoginDummy $ CI.mk credsIdent) | credsPlugin == apDummy = setMode <$> mMode (UpsertUserLoginDummy $ CI.mk credsIdent)
| credsPlugin == apAzure = setMode <$> mMode UpsertUserLoginAzure | credsPlugin `elem` loginAPs
| credsPlugin == apLdap = setMode <$> mMode UpsertUserLoginLdap = setMode <$> mMode (UpsertUserLogin credsPlugin)
| otherwise = setMode <$> mMode (UpsertUserLoginOther $ CI.mk credsIdent) | otherwise = setMode <$> mMode (UpsertUserLoginOther $ CI.mk credsIdent)
where where
setMode UpsertUserLoginAzure{} -- TODO: stuff upsertUserSource into credsExtra? setMode UpsertUserLogin{..} | upsertUserSource `elem` loginAPs
= cs{ credsPlugin = apAzure } = cs{ credsPlugin = upsertUserSource }
setMode UpsertUserLoginLdap{} -- TODO: stuff upsertUserSource into credsExtra?
= cs{ credsPlugin = apLdap }
setMode UpsertUserLoginDummy{..} setMode UpsertUserLoginDummy{..}
= cs{ credsPlugin = apDummy = cs{ credsPlugin = apDummy
, credsIdent = CI.original upsertUserIdent , credsIdent = CI.original upsertUserIdent
@ -157,9 +156,11 @@ _upsertUserMode mMode cs@Creds{..}
} }
setMode _ = cs setMode _ = cs
loginAPs = [ apAzure, apLdap ]
defaultOther = apHash defaultOther = apHash
-- TODO: generalize
ldapLookupAndUpsert :: forall m. ldapLookupAndUpsert :: forall m.
( MonadHandler m ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
@ -167,64 +168,84 @@ ldapLookupAndUpsert :: forall m.
, MonadUnliftIO m , MonadUnliftIO m
) )
=> Text => Text
-> SqlPersistT m (Entity User) -> SqlPersistT m (Entity UserAuth)
ldapLookupAndUpsert ident = ldapLookupAndUpsert ident =
getsYesod (view _appLdapPool) >>= \case getsYesod (view _appLdapPool) >>= \case
Nothing -> throwM $ LdapUserLdapError $ LdapHostNotResolved "No LDAP configuration in Foundation." Nothing -> throwM $ LdapUserLdapError $ LdapHostNotResolved "No LDAP configuration in Foundation."
Just ldapPool -> Just ldapPool@(upsertUserLdapConf, _) ->
ldapUser'' ldapPool ident >>= \case ldapUser'' ldapPool ident >>= \case
Nothing -> throwM LdapUserNoResult Nothing -> throwM LdapUserNoResult
Just ldapResponse -> upsertLdapUser UpsertUserGuessUser ldapResponse Just upsertUserLdapData -> upsertUser UpsertUserGuessUser UpsertUserDataLdap{..}
-- | Upsert ExternalUser DB according to given external source data (does not query source itself) -- | Upsert User and related auth in DB according to given external source data (does not query source itself)
upsertUser :: forall m. upsertUser :: forall m.
( MonadHandler m ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
, MonadCatch m , MonadCatch m
) )
=> UpsertUserMode => UpsertUserMode
-> SqlPersistT m (Entity ExternalAuth) -> UpsertUserData
upsertUser upsertMode = do -> SqlPersistT m (Entity UserAuth)
upsertUser upsertMode upsertData = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
userDefaultConf <- getsYesod $ view _appUserDefaults userDefaultConf <- getsYesod $ view _appUserDefaults
(newUser,userUpdate) <- decodeLdapUser now userDefaultConf upsertMode ldapData (newUser,userUpdate) <- decodeUser now userDefaultConf upsertData
--TODO: newUser should be associated with a company and company supervisor through Handler.Utils.Company.upsertUserCompany, but this is called by upsertAvsUser already - conflict? --TODO: newUser should be associated with a company and company supervisor through Handler.Utils.Company.upsertUserCompany, but this is called by upsertAvsUser already - conflict?
oldUsers <- selectKeysList [ ExternalUserIdent ==. externalUserIdent newUser ] [] oldUsers <- selectKeysList [ UserIdent ==. userIdent newUser ] []
user@(Entity userId userRec) <- case oldUsers of _user@(Entity userId userRec) <- case oldUsers of
Just [oldUserId] -> updateGetEntity oldUserId userUpdate [oldUserId] -> updateGetEntity oldUserId userUpdate
_other -> upsertBy (UniqueAuthentication (newUser ^. _userIdent)) newUser userUpdate _other -> upsertBy (UniqueUser (newUser ^. _userIdent)) newUser userUpdate
unless (validDisplayName (newUser ^. _userTitle)
-- sets display name
-- TODO: use display name from external source, if possible
unless (validDisplayName (newUser ^. _userTitle)
(newUser ^. _userFirstName) (newUser ^. _userFirstName)
(newUser ^. _userSurname) (newUser ^. _userSurname)
(userRec ^. _userDisplayName)) $ (userRec ^. _userDisplayName)) $
update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ] update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ]
when (validEmail' (userRec ^. _userEmail)) $ do
let emUps = [ UserDisplayEmail =. (newUser ^. _userEmail) | not (validEmail' (userRec ^. _userDisplayEmail)) ] -- TODO updates ident with email - refactor and/or remove with Azure! (email /= ident in azure)
++ [ UserAuthentication =. AuthLDAP | is _AuthNoLogin (userRec ^. _userAuthentication) ] -- when (validEmail' (userRec ^. _userEmail)) $ do
unless (null emUps) $ update userId emUps -- let emUps = [ UserDisplayEmail =. (newUser ^. _userEmail) | not (validEmail' (userRec ^. _userDisplayEmail)) ]
-- Attempt to update ident, too: -- ++ [ UserAuthentication =. AuthLDAP | is _AuthNoLogin (userRec ^. _userAuthentication) ]
unless (validEmail' (userRec ^. _userIdent)) $ -- unless (null emUps) $ update userId emUps
void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ())) -- -- Attempt to update ident, too:
-- unless (validEmail' (userRec ^. _userIdent)) $
-- void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ()))
let let
userSystemFunctions = determineSystemFunctions . Set.fromList $ map CI.mk userSystemFunctions' userSystemFunctions = determineSystemFunctions . Set.fromList $ map CI.mk userSystemFunctions'
userSystemFunctions' = do userSystemFunctions' = case upsertData of
(k, v) <- ldapData UpsertUserDataAzure{..} -> do
guard $ k == ldapAffiliation (_k, v) <- upsertUserAzureData
v' <- v v' <- v
Right str <- return $ Text.decodeUtf8' v' Right str <- return $ Text.decodeUtf8' v'
assertM' (not . Text.null) $ Text.strip str assertM' (not . Text.null) $ Text.strip str
UpsertUserDataLdap{..} -> do
(k, v) <- upsertUserLdapData
guard $ k == ldapAffiliation
v' <- v
Right str <- return $ Text.decodeUtf8' v'
assertM' (not . Text.null) $ Text.strip str
iforM_ userSystemFunctions $ \func preset -> do iforM_ userSystemFunctions $ \func preset -> do
memcachedByInvalidate (AuthCacheSystemFunctionList func) $ Proxy @(Set UserId) memcachedByInvalidate (AuthCacheSystemFunctionList func) $ Proxy @(Set UserId)
if | preset -> void $ upsert (UserSystemFunction userId func False False) [] if | preset -> void $ upsert (UserSystemFunction userId func False False) []
| otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False] | otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False]
return user let (userAuthLastLogin, userAuthLastSync) = case upsertMode of
UpsertUserSync{} -> (Nothing , Just now)
UpsertUserGuessUser{} -> (Nothing , Nothing )
_other -> (Just now, Nothing )
userAuth <- upsertBy (UniqueAuthentication $ newUser ^. _userIdent) UserAuth{ userAuthIdent = newUser ^. _userIdent, ..} $
[ UserAuthLastLogin =. Just lastLogin | lastLogin <- maybeToList userAuthLastLogin ] ++
[ UserAuthLastSync =. Just lastSync | lastSync <- maybeToList userAuthLastSync ]
return userAuth
-- | Upsert User DB according to given Azure data (does not query Azure itself) -- | Upsert User DB according to given Azure data (does not query Azure itself)
-- upsertAzureUser :: forall m. -- upsertAzureUser :: forall m.
@ -275,68 +296,122 @@ upsertUser upsertMode = do
-- --
-- return user -- return user
decodeLdapUserTest :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m) decodeUserTest :: ( MonadHandler m
=> Maybe UserIdent -> Ldap.AttrList [] -> m (Either UserConversionException (User, [Update User])) , HandlerSite m ~ UniWorX
decodeLdapUserTest mbIdent ldapData = do , MonadCatch m
now <- liftIO getCurrentTime
userDefaultConf <- getsYesod $ view _appUserDefaults
let mode = maybe UpsertUserLoginLdap UpsertUserLoginDummy mbIdent
try $ decodeLdapUser now userDefaultConf mode ldapData
decodeAzureUserTest :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m)
=> Maybe UserIdent -> [(Text, [ByteString])] -> m (Either UserConversionException (User, [Update User]))
decodeAzureUserTest mbIdent azureData = do
now <- liftIO getCurrentTime
userDefaultConf <- getsYesod $ view _appUserDefaults
let mode = maybe UpsertUserLoginLdap UpsertUserLoginDummy mbIdent
try $ decodeAzureUser now userDefaultConf mode azureData
decodeLdapUser :: ( MonadThrow m
) )
=> UTCTime -- ^ Now => UpsertUserData
-> UpsertUserMode -> m (Either UserConversionException (User, [Update User]))
-> Ldap.AttrList [] -- ^ Raw LDAP data decodeUserTest decodeData = do
-> m (ExternalAuth,_) -- ^ Data for new ExternalUser entry and updating existing ExternalUser entry now <- liftIO getCurrentTime
decodeLdapUser now upsertMode ldapData = do userDefaultConf <- getsYesod $ view _appUserDefaults
externalAuthIdent <- if try $ decodeUser now userDefaultConf decodeData
| [bs] <- ldapMap !!! ldapPrimaryKey
, Right ldapPrimaryKey' <- Text.decodeUtf8' bs decodeUser :: ( MonadThrow m
)
=> UTCTime -- ^ Now
-> UserDefaultConf
-> UpsertUserData -- ^ Raw source data
-> m (User,_) -- ^ Data for new User entry and updating existing User entries
decodeUser now UserDefaultConf{..} upsertData = do
userIdent <- if
| Just azureData <- mbAzureData
, [(Text.decodeUtf8' -> Right azureUserPrincipalName')] <- azureData !!! azureUserPrincipalName
, Just azureUserPrincipalName'' <- assertM' (not . Text.null) $ Text.strip azureUserPrincipalName'
-> return $ CI.mk azureUserPrincipalName''
| Just ldapData <- mbLdapData
, [(Text.decodeUtf8' -> Right ldapPrimaryKey')] <- ldapData !!! ldapPrimaryKey
, Just ldapPrimaryKey'' <- assertM' (not . Text.null) $ Text.strip ldapPrimaryKey' , Just ldapPrimaryKey'' <- assertM' (not . Text.null) $ Text.strip ldapPrimaryKey'
-> return ldapPrimaryKey'' -> return $ CI.mk ldapPrimaryKey''
| otherwise | otherwise
-> throwM ExternalUserInvalidIdent -> throwM UserInvalidIdent
let externalAuthData = encode ldapData
externalAuthLastAuth <- if
| is _UpsertUserSync upsertMode || is _UpsertUserGuessUser upsertMode
-> Nothing
| otherwise
-> Just now
let let
newUser = ExternalAuth (userSurname, userFirstName, userDisplayName, userEmail, userTelephone, userMobile, userCompanyPersonalNumber, userCompanyDepartment, userLanguages)
{ externalAuthSource = ldapSourceIdent | Just azureData <- mbAzureData
, externalAuthLastSync = now = ( azureData `decodeAzure'` azureUserSurname
, azureData `decodeAzure'` azureUserGivenName
, azureData `decodeAzure'` azureUserDisplayName
, CI.mk $
azureData `decodeAzure'` azureUserMail
, azureData `decodeAzure` azureUserTelephone
, azureData `decodeAzure` azureUserMobile
, Nothing -- userCompanyPersonalNumber not contained in Azure response
, Nothing -- userCompanyDepartment not contained in Azure response
, Nothing -- azureData `decodeAzure` azureUserPreferredLanguage -- TODO: parse Languages from azureUserPreferredLanguage
)
| Just ldapData <- mbLdapData
= ( ldapData `decodeLdap'` ldapUserSurname
, ldapData `decodeLdap'` ldapUserFirstName
, ldapData `decodeLdap'` ldapUserDisplayName
, CI.mk $
ldapData `decodeLdap'` (Ldap.Attr "mail") -- TODO: use ldapUserEmail?
, ldapData `decodeLdap` ldapUserTelephone
, ldapData `decodeLdap` ldapUserMobile
, ldapData `decodeLdap` ldapUserFraportPersonalnummer
, ldapData `decodeLdap` ldapUserFraportAbteilung
, Nothing -- userLanguage not contained in LDAP response
)
| otherwise
= error "decodeUser: Both azureData and ldapData are empty, cannot decode basic fields from no data!"
let
newUser = User
{ userMaxFavourites = userDefaultMaxFavourites
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userSex = Nothing
, userBirthday = Nothing
, userTitle = Nothing
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userCsvOptions = def
, userTokensIssuedAfter = Nothing
, userCreated = now
, userDisplayEmail = userEmail
, 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
, userPostLastUpdate = Nothing
, userPinPassword = Nothing -- must be derived via AVS
, userPrefersPostal = userDefaultPrefersPostal
, .. , ..
} }
userUpdate = userUpdate =
[ ExternalAuthIdent =. externalAuthIdent [ UserSurname =. userSurname
, ExternalAuthData =. externalAuthData , UserFirstName =. userFirstName
, ExternalAuthLastSync =. now -- , UserDisplayName =. userDisplayName -- not updated, since users are allowed to change their DisplayName
, UserEmail =. userEmail
, UserTelephone =. userTelephone
, UserMobile =. userMobile
, UserCompanyPersonalNumber =. userCompanyPersonalNumber
, UserCompanyDepartment =. userCompanyDepartment
] ]
return (newUser, userUpdate) return (newUser, userUpdate)
where where
ldapMap :: Map.Map Ldap.Attr [Ldap.AttrValue] -- Recall: Ldap.AttrValue == ByteString mbAzureData :: Maybe (Map Text [ByteString])
ldapMap = Map.fromListWith (++) $ ldapData <&> second (filter (not . ByteString.null)) mbAzureData = Map.fromListWith (++) . fmap (second . filter $ not . ByteString.null) <$> preview _upsertUserAzureData upsertData
mbLdapData :: Maybe (Map Ldap.Attr [Ldap.AttrValue]) -- Recall: Ldap.AttrValue == ByteString
mbLdapData = Map.fromListWith (++) . fmap (second . filter $ not . ByteString.null) <$> preview _upsertUserLdapData upsertData
-- ldapData = fmap (Map.fromListWith (++)) $ upsertData ^? _upsertUserLdapData . over _2 (filter $ not . ByteString.null)
-- just returns Nothing on error, pure -- just returns Nothing on error, pure
decodeLdap :: Ldap.Attr -> Maybe Text decodeAzure :: Map Text [ByteString] -> Text -> Maybe Text
decodeLdap attr = listToMaybe . rights $ Text.decodeUtf8' <$> ldapMap !!! attr decodeAzure azureData k = listToMaybe . rights $ Text.decodeUtf8' <$> azureData !!! k
decodeLdap :: Map Ldap.Attr [Ldap.AttrValue] -> Ldap.Attr -> Maybe Text
decodeLdap ldapData attr = listToMaybe . rights $ Text.decodeUtf8' <$> ldapData !!! attr
decodeLdap' :: Ldap.Attr -> Text decodeAzure' :: Map Text [ByteString] -> Text -> Text
decodeLdap' = fromMaybe "" . decodeLdap decodeAzure' azureData = fromMaybe "" . decodeAzure azureData
decodeLdap' :: Map Ldap.Attr [Ldap.AttrValue] -> Ldap.Attr -> Text
decodeLdap' ldapData = fromMaybe "" . decodeLdap ldapData
-- accept the first successful decoding or empty; only throw an error if all decodings fail -- accept the first successful decoding or empty; only throw an error if all decodings fail
-- decodeLdap' :: (Exception e) => Ldap.Attr -> e -> m (Maybe Text) -- decodeLdap' :: (Exception e) => Ldap.Attr -> e -> m (Maybe Text)
-- decodeLdap' attr err -- decodeLdap' attr err
@ -348,11 +423,11 @@ decodeLdapUser now upsertMode ldapData = do
-- only accepts the first successful decoding, ignoring all others, but failing if there is none -- only accepts the first successful decoding, ignoring all others, but failing if there is none
-- decodeLdap1 :: (MonadThrow m, Exception e) => Ldap.Attr -> e -> m Text -- decodeLdap1 :: (MonadThrow m, Exception e) => Ldap.Attr -> e -> m Text
decodeLdap1 attr err -- decodeLdap1 ldapData attr err
| (h:_) <- rights vs = return h -- | (h:_) <- rights vs = return h
| otherwise = throwM err -- | otherwise = throwM err
where -- where
vs = Text.decodeUtf8' <$> (ldapMap !!! attr) -- vs = Text.decodeUtf8' <$> (ldapData !!! attr)
-- accept and merge one or more successful decodings, ignoring all others -- accept and merge one or more successful decodings, ignoring all others
-- decodeLdapN attr err -- decodeLdapN attr err
@ -489,121 +564,122 @@ decodeLdapUser now upsertMode ldapData = do
-- -- where -- -- where
-- -- vs = Text.decodeUtf8' <$> (ldapMap !!! attr) -- -- vs = Text.decodeUtf8' <$> (ldapMap !!! attr)
decodeAzureUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertUserMode -> [(Text, [ByteString])] -> m (User,_) -- decodeAzureUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertUserMode -> [(Text, [ByteString])] -> m (User,_)
decodeAzureUser now UserDefaultConf{..} upsertMode azureData = do -- decodeAzureUser now UserDefaultConf{..} upsertMode azureData = do
let -- let
userTelephone = decodeAzure azureUserTelephone -- userTelephone = decodeAzure azureUserTelephone
userMobile = decodeAzure azureUserMobile -- userMobile = decodeAzure azureUserMobile
userCompanyPersonalNumber = Nothing -- TODO decodeAzure azureUserFraportPersonalnummer -- userCompanyPersonalNumber = Nothing -- TODO decodeAzure azureUserFraportPersonalnummer
userCompanyDepartment = Nothing --TODO decodeAzure ldapUserFraportAbteilung -- userCompanyDepartment = Nothing --TODO decodeAzure ldapUserFraportAbteilung
--
userAuthentication -- userAuthentication
| is _UpsertUserLoginOther upsertMode -- | is _UpsertUserLoginOther upsertMode
= AuthNoLogin -- AuthPWHash (error "Non-LDAP logins should only work for users that are already known") -- = AuthPWHash (error "Non-LDAP logins should only work for users that are already known") -- TODO throwM instead?
| otherwise = AuthAzure -- | otherwise = AuthAzure
userLastAuthentication = guardOn isLogin now -- userLastAuthentication = guardOn isLogin now
isLogin = has (_UpsertUserLoginAzure <> _UpsertUserLoginOther . united) upsertMode -- isLogin = has (_UpsertUserLoginAzure <> _UpsertUserLoginOther . united) upsertMode
--
userTitle = Nothing -- TODO decodeAzure ldapUserTitle -- CampusUserInvalidTitle -- userTitle = Nothing -- TODO decodeAzure ldapUserTitle -- CampusUserInvalidTitle
userFirstName = decodeAzure' azureUserGivenName -- CampusUserInvalidGivenName -- userFirstName = decodeAzure' azureUserGivenName -- CampusUserInvalidGivenName
userSurname = decodeAzure' azureUserSurname -- CampusUserInvalidSurname -- userSurname = decodeAzure' azureUserSurname -- CampusUserInvalidSurname
userDisplayName <- decodeAzure1 azureUserDisplayName UserInvalidDisplayName <&> fixDisplayName -- do not check LDAP-given userDisplayName -- userDisplayName <- decodeAzure1 azureUserDisplayName UserInvalidDisplayName <&> fixDisplayName -- do not check LDAP-given userDisplayName
--
--userDisplayName <- decodeLdap1 ldapUserDisplayName CampusUserInvalidDisplayName >>= -- --userDisplayName <- decodeLdap1 ldapUserDisplayName CampusUserInvalidDisplayName >>=
-- (maybeThrow CampusUserInvalidDisplayName . checkDisplayName userTitle userFirstName userSurname) -- -- (maybeThrow CampusUserInvalidDisplayName . checkDisplayName userTitle userFirstName userSurname)
--
userIdent <- if -- userIdent <- if
| [bs] <- azureMap !!! azureUserPrincipalName -- | [bs] <- azureMap !!! azureUserPrincipalName
, Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs -- , Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs
, hasn't _upsertUserIdent upsertMode || has (_upsertUserIdent . only userIdent') upsertMode -- , hasn't _upsertUserIdent upsertMode || has (_upsertUserIdent . only userIdent') upsertMode
-> return userIdent' -- -> return userIdent'
| Just userIdent' <- upsertMode ^? _upsertUserIdent -- | Just userIdent' <- upsertMode ^? _upsertUserIdent
-> return userIdent' -- -> return userIdent'
| otherwise -- | otherwise
-> throwM UserInvalidIdent -- -> throwM UserInvalidIdent
--
userEmail <- if -- TODO: refactor! NOTE: LDAP doesnt know email for all users; we use userPrincialName instead; however validEmail refutes `E<number@fraport.de` here, too strong! Make Email-Field optional! -- userEmail <- if -- TODO: refactor! NOTE: LDAP doesnt know email for all users; we use userPrincialName instead; however validEmail refutes `E<number@fraport.de` here, too strong! Make Email-Field optional!
| userEmail : _ <- mapMaybe (assertM (elem '@') . either (const Nothing) Just . Text.decodeUtf8') (lookupSome azureMap [azureUserMail]) -- | userEmail : _ <- mapMaybe (assertM (elem '@') . either (const Nothing) Just . Text.decodeUtf8') (lookupSome azureMap [azureUserMail])
-> return $ CI.mk userEmail -- -> return $ CI.mk userEmail
-- -> return $ CI.mk userEmail -- -- -> return $ CI.mk userEmail
| otherwise -- | otherwise
-> throwM UserInvalidEmail -- -> throwM UserInvalidEmail
--
-- TODO: use fromASCIIBytes / fromByteString? -- -- TODO: use fromASCIIBytes / fromByteString?
userAzurePrimaryKey <- if -- userAzurePrimaryKey <- if
| [bs] <- azureMap !!! azurePrimaryKey -- | [bs] <- azureMap !!! azurePrimaryKey
, Right userAzurePrimaryKey'' <- Text.decodeUtf8' bs -- , Right userAzurePrimaryKey'' <- Text.decodeUtf8' bs
, Just userAzurePrimaryKey''' <- assertM' (not . Text.null) $ Text.strip userAzurePrimaryKey'' -- , Just userAzurePrimaryKey''' <- assertM' (not . Text.null) $ Text.strip userAzurePrimaryKey''
, Just userAzurePrimaryKey'''' <- UUID.fromText userAzurePrimaryKey''' -- , Just userAzurePrimaryKey'''' <- UUID.fromText userAzurePrimaryKey'''
-> return $ Just userAzurePrimaryKey'''' -- -> return $ Just userAzurePrimaryKey''''
| otherwise -- | otherwise
-> return Nothing -- -> return Nothing
--
let -- let
newUser = User -- newUser = User
{ userMaxFavourites = userDefaultMaxFavourites -- { userMaxFavourites = userDefaultMaxFavourites
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms -- , userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme -- , userTheme = userDefaultTheme
, userDateTimeFormat = userDefaultDateTimeFormat -- , userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat -- , userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat -- , userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles -- , userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays -- , userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex -- , userShowSex = userDefaultShowSex
, userSex = Nothing -- , userSex = Nothing
, userBirthday = Nothing -- , userBirthday = Nothing
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced -- , userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels -- , userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def -- , userNotificationSettings = def
, userLanguages = Nothing -- TODO: decode and parse preferredLanguages -- , userLanguages = Nothing -- TODO: decode and parse preferredLanguages
, userCsvOptions = def -- , userCsvOptions = def
, userTokensIssuedAfter = Nothing -- , userTokensIssuedAfter = Nothing
, userCreated = now -- , userCreated = now
, userDisplayName = userDisplayName -- , userDisplayName = userDisplayName
, userDisplayEmail = userEmail -- , userDisplayEmail = userEmail
, userMatrikelnummer = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO -- , userMatrikelnummer = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
, userPostAddress = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO -- , userPostAddress = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
, userPostLastUpdate = Nothing -- , userPostLastUpdate = Nothing
, userPinPassword = Nothing -- must be derived via AVS -- , userPinPassword = Nothing -- must be derived via AVS
, userPrefersPostal = userDefaultPrefersPostal -- , userPrefersPostal = userDefaultPrefersPostal
, .. -- , ..
} -- }
userUpdate = -- userUpdate =
[ UserLastAuthentication =. Just now | isLogin ] ++ -- --- [ UserLastAuthentication =. Just now | isLogin ] ++
[ UserEmail =. userEmail | validEmail' userEmail ] ++ -- [ UserEmail =. userEmail | validEmail' userEmail ] ++
[ -- [
-- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 272 -- -- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 272
UserFirstName =. userFirstName -- UserFirstName =. userFirstName
, UserSurname =. userSurname -- , UserSurname =. userSurname
, UserMobile =. userMobile -- , UserMobile =. userMobile
, UserTelephone =. userTelephone -- , UserTelephone =. userTelephone
, UserCompanyPersonalNumber =. userCompanyPersonalNumber -- , UserCompanyPersonalNumber =. userCompanyPersonalNumber
, UserCompanyDepartment =. userCompanyDepartment -- , UserCompanyDepartment =. userCompanyDepartment
] -- ]
return (newUser, userUpdate) -- return (newUser, userUpdate)
--
where -- where
azureMap :: Map.Map Text [ByteString] -- azureMap :: Map.Map Text [ByteString]
azureMap = Map.fromListWith (++) $ azureData <&> second (filter (not . ByteString.null)) -- azureMap = Map.fromListWith (++) $ azureData <&> second (filter (not . ByteString.null))
--
-- just returns Nothing on error, pure -- -- just returns Nothing on error, pure
decodeAzure :: Text -> Maybe Text -- decodeAzure :: Text -> Maybe Text
decodeAzure attr = listToMaybe . rights $ Text.decodeUtf8' <$> azureMap !!! attr -- decodeAzure attr = listToMaybe . rights $ Text.decodeUtf8' <$> azureMap !!! attr
--
decodeAzure' :: Text -> Text -- decodeAzure' :: Text -> Text
decodeAzure' = fromMaybe "" . decodeAzure -- decodeAzure' = fromMaybe "" . decodeAzure
--
-- only accepts the first successful decoding, ignoring all others, but failing if there is none -- -- only accepts the first successful decoding, ignoring all others, but failing if there is none
-- decodeLdap1 :: (MonadThrow m, Exception e) => Ldap.Attr -> e -> m Text -- -- decodeLdap1 :: (MonadThrow m, Exception e) => Ldap.Attr -> e -> m Text
decodeAzure1 attr err -- decodeAzure1 attr err
| (h:_) <- rights vs = return h -- | (h:_) <- rights vs = return h
| otherwise = throwM err -- | otherwise = throwM err
where -- where
vs = Text.decodeUtf8' <$> (azureMap !!! attr) -- vs = Text.decodeUtf8' <$> (azureMap !!! attr)
associateUserSchoolsByTerms :: MonadIO m => UserId -> SqlPersistT m () associateUserSchoolsByTerms :: MonadIO m => UserAuthId -> SqlPersistT m ()
associateUserSchoolsByTerms uid = do associateUserSchoolsByTerms uaid = do
uid <- join $ fmap (fromMaybe $ error "associateUserSchoolsByTerms: No User for given UserAuthId!") . getKeyBy . UniqueUser . userAuthIdent <$> getJust uaid
sfs <- selectList [StudyFeaturesUser ==. uid] [] sfs <- selectList [StudyFeaturesUser ==. uid] []
forM_ sfs $ \(Entity _ StudyFeatures{..}) -> do forM_ sfs $ \(Entity _ StudyFeatures{..}) -> do
@ -616,11 +692,13 @@ associateUserSchoolsByTerms uid = do
} }
updateUserLanguage :: ( MonadHandler m, HandlerSite m ~ UniWorX updateUserLanguage :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, YesodAuth UniWorX , YesodAuth UniWorX
, UserId ~ AuthId UniWorX , UserId ~ AuthId UniWorX
) )
=> Maybe Lang -> SqlPersistT m (Maybe Lang) => Maybe Lang
-> SqlPersistT m (Maybe Lang)
updateUserLanguage (Just lang) = do updateUserLanguage (Just lang) = do
unless (lang `elem` appLanguages) $ unless (lang `elem` appLanguages) $
invalidArgs ["Unsupported language"] invalidArgs ["Unsupported language"]