Overhaul authentication

Fixes #186
This commit is contained in:
Gregor Kleen 2018-09-28 21:27:19 +02:00
parent a6be152981
commit 857dc92ca2
24 changed files with 251 additions and 139 deletions

View File

@ -13,9 +13,12 @@ detailed-logging: "_env:DETAILED_LOGGING:false"
should-log-all: "_env:LOG_ALL:false" should-log-all: "_env:LOG_ALL:false"
minimum-log-level: "_env:LOGLEVEL:warn" minimum-log-level: "_env:LOGLEVEL:warn"
auth-dummy-login: "_env:DUMMY_LOGIN:false" auth-dummy-login: "_env:DUMMY_LOGIN:false"
auth-pwfile: "_env:PWFILE:"
allow-deprecated: "_env:ALLOW_DEPRECATED:false" allow-deprecated: "_env:ALLOW_DEPRECATED:false"
auth-pw-hash:
algorithm: "pbkdf2"
strength: 14
# Optional values with the following production defaults. # Optional values with the following production defaults.
# In development, they default to true. # In development, they default to true.
# reload-templates: false # reload-templates: false
@ -42,7 +45,7 @@ ldap:
timeout: "_env:LDAPTIMEOUT:5" timeout: "_env:LDAPTIMEOUT:5"
user-defaults: user-defaults:
favourites: 12 max-favourites: 12
theme: Default theme: Default
date-time-format: "%a %d %b %Y %R" date-time-format: "%a %d %b %Y %R"
date-format: "%d.%m.%Y" date-format: "%d.%m.%Y"

20
db.hs
View File

@ -66,8 +66,8 @@ fillDb = do
winter2017 = TermIdentifier 2017 Winter winter2017 = TermIdentifier 2017 Winter
summer2018 = TermIdentifier 2018 Summer summer2018 = TermIdentifier 2018 Summer
gkleen <- insert User gkleen <- insert User
{ userPlugin = "LDAP" { userIdent = "G.Kleen@campus.lmu.de"
, userIdent = "G.Kleen@campus.lmu.de" , userAuthentication = AuthLDAP
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "G.Kleen@campus.lmu.de" , userEmail = "G.Kleen@campus.lmu.de"
, userDisplayName = "Gregor Kleen" , userDisplayName = "Gregor Kleen"
@ -80,8 +80,8 @@ fillDb = do
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
} }
fhamann <- insert User fhamann <- insert User
{ userPlugin = "LDAP" { userIdent = "felix.hamann@campus.lmu.de"
, userIdent = "felix.hamann@campus.lmu.de" , userAuthentication = AuthLDAP
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "felix.hamann@campus.lmu.de" , userEmail = "felix.hamann@campus.lmu.de"
, userDisplayName = "Felix Hamann" , userDisplayName = "Felix Hamann"
@ -94,8 +94,8 @@ fillDb = do
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
} }
jost <- insert User jost <- insert User
{ userPlugin = "LDAP" { userIdent = "jost@tcs.ifi.lmu.de"
, userIdent = "jost@tcs.ifi.lmu.de" , userAuthentication = AuthLDAP
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "jost@tcs.ifi.lmu.de" , userEmail = "jost@tcs.ifi.lmu.de"
, userDisplayName = "Steffen Jost" , userDisplayName = "Steffen Jost"
@ -108,8 +108,8 @@ fillDb = do
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
} }
void . insert $ User void . insert $ User
{ userPlugin = "LDAP" { userIdent = "max@campus.lmu.de"
, userIdent = "max@campus.lmu.de" , userAuthentication = AuthLDAP
, userMatrikelnummer = Nothing , userMatrikelnummer = Nothing
, userEmail = "max@campus.lmu.de" , userEmail = "max@campus.lmu.de"
, userDisplayName = "Max Musterstudent" , userDisplayName = "Max Musterstudent"
@ -122,8 +122,8 @@ fillDb = do
, userDownloadFiles = userDefaultDownloadFiles , userDownloadFiles = userDefaultDownloadFiles
} }
void . insert $ User void . insert $ User
{ userPlugin = "LDAP" { userIdent = "tester@campus.lmu.de"
, userIdent = "tester@campus.lmu.de" , userAuthentication = AuthLDAP
, userMatrikelnummer = Just "999" , userMatrikelnummer = Just "999"
, userEmail = "tester@campus.lmu.de" , userEmail = "tester@campus.lmu.de"
, userDisplayName = "Tina Tester" , userDisplayName = "Tina Tester"

2
messages/pw-hash/de.msg Normal file
View File

@ -0,0 +1,2 @@
PWHashIdent: Identifikation
PWHashPassword: Passwort

View File

@ -201,7 +201,7 @@ MatrikelNr: Matrikelnummer
Theme: Oberflächen Design Theme: Oberflächen Design
Favoriten: Anzahl gespeicherter Favoriten Favoriten: Anzahl gespeicherter Favoriten
Plugin: Plugin Plugin: Plugin
Ident: Identifizierung Ident: Identifikation
Settings: Individuelle Benutzereinstellungen Settings: Individuelle Benutzereinstellungen
SettingsUpdate: Einstellungen wurden gespeichert. SettingsUpdate: Einstellungen wurden gespeichert.
@ -285,6 +285,8 @@ SubmissionFilesIgnored: Es wurden Dateien in der hochgeladenen Abgabe ignoriert:
SubmissionDoesNotExist smid@CryptoFileNameSubmission: Es existiert keine Abgabe mit Nummer #{toPathPiece smid}. SubmissionDoesNotExist smid@CryptoFileNameSubmission: Es existiert keine Abgabe mit Nummer #{toPathPiece smid}.
LDAPLoginTitle: Campus-Login LDAPLoginTitle: Campus-Login
PWHashLoginTitle: Uni2Work-Login
PWHashLoginNote: Dieses Formular ist zu verwenden, wenn Sie vom Uni2Work-Team spezielle Logindaten erhalten haben. Normale Nutzer melden sich bitte via Campus-Login an!
DummyLoginTitle: Development-Login DummyLoginTitle: Development-Login
CorrectorNormal: Normal CorrectorNormal: Normal

6
models
View File

@ -1,6 +1,6 @@
User json User json
plugin Text ident (CI Text)
ident Text authentication AuthenticationMode
matrikelnummer Text Maybe matrikelnummer Text Maybe
email (CI Text) email (CI Text)
displayName Text displayName Text
@ -11,7 +11,7 @@ User json
dateFormat DateTimeFormat "default='%d.%m.%Y'" dateFormat DateTimeFormat "default='%d.%m.%Y'"
timeFormat DateTimeFormat "default='%R'" timeFormat DateTimeFormat "default='%R'"
downloadFiles Bool default=false downloadFiles Bool default=false
UniqueAuthentication plugin ident UniqueAuthentication ident
UniqueEmail email UniqueEmail email
deriving Show deriving Show
UserAdmin UserAdmin

View File

@ -206,13 +206,10 @@ handler h = getAppDevSettings >>= makeFoundation >>= flip unsafeHandler h
db :: ReaderT SqlBackend (HandlerT UniWorX IO) a -> IO a db :: ReaderT SqlBackend (HandlerT UniWorX IO) a -> IO a
db = handler . runDB db = handler . runDB
addPWEntry :: FilePath {-^ Password file -} addPWEntry :: User
-> User
-> Text {-^ Password -} -> Text {-^ Password -}
-> IO () -> IO ()
addPWEntry pwFile User{..} (Text.encodeUtf8 -> pw) = do addPWEntry User{..} (Text.encodeUtf8 -> pw) = db $ do
(Text.decodeUtf8 -> pwHash) <- makePassword pw 14 PWHashConf{..} <- getsYesod $ appAuthPWHash . appSettings
let pwEntry = PWEntry{ pwUser = User{ userPlugin = "PWFile", .. }, .. } (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength
newUser = userIdent void $ insert User{..}
c <- either (const []) id <$> Yaml.decodeFileEither pwFile
Yaml.encodeFile pwFile $ pwEntry : [ c' | c'@(PWEntry{pwUser=User{..}}) <- c, userIdent /= newUser ]

View File

@ -16,6 +16,9 @@ import Database.Persist.Sql (SqlBackendCanRead)
import Utils.Form import Utils.Form
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
data DummyMessage = MsgDummyIdent data DummyMessage = MsgDummyIdent
@ -26,12 +29,12 @@ dummyForm :: ( RenderMessage site FormMessage
, SqlBackendCanRead (YesodPersistBackend site) , SqlBackendCanRead (YesodPersistBackend site)
, Button site SubmitButton , Button site SubmitButton
, Show (ButtonCssClass site) , Show (ButtonCssClass site)
) => AForm (HandlerT site IO) Text ) => AForm (HandlerT site IO) (CI Text)
dummyForm = areq userField (fslpI MsgDummyIdent "plugin:ident") Nothing dummyForm = areq (selectField userList) (fslI MsgDummyIdent) Nothing
<* submitButton <* submitButton
where where
userField = textField `addDatalist` liftHandlerT (runDB userList) userList = fmap mkOptionList . runDB $ map toOption <$> selectList [] [Asc UserIdent]
userList = map (\(Entity _ User{..}) -> userPlugin <> ":" <> userIdent) <$> selectList [] [Asc UserPlugin, Asc UserIdent] toOption (Entity _ User{..}) = Option (CI.original userIdent) userIdent (CI.original userIdent)
dummyLogin :: ( YesodAuth site dummyLogin :: ( YesodAuth site
, YesodPersist site , YesodPersist site
@ -53,7 +56,7 @@ dummyLogin = AuthPlugin{..}
redirect LoginR redirect LoginR
FormMissing -> redirect LoginR FormMissing -> redirect LoginR
FormSuccess ident -> FormSuccess ident ->
lift . setCredsRedirect $ Creds "dummy" ident [] lift . setCredsRedirect $ Creds "dummy" (CI.original ident) []
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm

View File

@ -1,6 +1,7 @@
{-# LANGUAGE RecordWildCards {-# LANGUAGE RecordWildCards
, OverloadedStrings , OverloadedStrings
, TemplateHaskell , TemplateHaskell
, ViewPatterns
, TypeFamilies , TypeFamilies
, FlexibleContexts , FlexibleContexts
, FlexibleInstances , FlexibleInstances
@ -20,6 +21,9 @@ import Import.NoFoundation
import Control.Lens import Control.Lens
import Network.Connection import Network.Connection
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import qualified Control.Monad.Catch as Exc import qualified Control.Monad.Catch as Exc
import Utils.Form import Utils.Form
@ -31,7 +35,10 @@ import qualified Data.Text.Encoding as Text
import qualified Yesod.Auth.Message as Msg import qualified Yesod.Auth.Message as Msg
data CampusLogin = CampusLogin { campusIdent, campusPassword :: Text } data CampusLogin = CampusLogin
{ campusIdent :: CI Text
, campusPassword :: Text
}
data CampusMessage = MsgCampusIdentNote data CampusMessage = MsgCampusIdentNote
| MsgCampusIdent | MsgCampusIdent
@ -60,7 +67,7 @@ campusForm :: ( RenderMessage site FormMessage
, Show (ButtonCssClass site) , Show (ButtonCssClass site)
) => AForm (HandlerT site IO) CampusLogin ) => AForm (HandlerT site IO) CampusLogin
campusForm = CampusLogin campusForm = CampusLogin
<$> areq textField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote) Nothing <$> areq ciField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote) Nothing
<*> areq passwordField (fslI MsgCampusPassword) Nothing <*> areq passwordField (fslI MsgCampusPassword) Nothing
<* submitButton <* submitButton
@ -82,7 +89,7 @@ campusLogin conf@LdapConf{..} = AuthPlugin{..}
forM_ errs $ addMessage "error" . toHtml forM_ errs $ addMessage "error" . toHtml
redirect LoginR redirect LoginR
FormMissing -> redirect LoginR FormMissing -> redirect LoginR
FormSuccess CampusLogin{..} -> do FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do
ldapResult <- liftIO . Ldap.with ldapHost ldapPort $ \ldap -> do ldapResult <- liftIO . Ldap.with ldapHost ldapPort $ \ldap -> do
Ldap.bind ldap (Ldap.Dn campusIdent) (Ldap.Password $ Text.encodeUtf8 campusPassword) Ldap.bind ldap (Ldap.Dn campusIdent) (Ldap.Password $ Text.encodeUtf8 campusPassword)
Ldap.bind ldap ldapDn ldapPassword Ldap.bind ldap ldapDn ldapPassword

View File

@ -1,60 +0,0 @@
{-# LANGUAGE NoImplicitPrelude
, QuasiQuotes
, TemplateHaskell
, ViewPatterns
, RecordWildCards
, OverloadedStrings
, FlexibleContexts
, TypeFamilies
#-}
module Auth.PWFile
( maintenanceLogin
) where
import Import.NoFoundation
import Database.Persist.Sql (IsSqlBackend)
import qualified Data.Yaml as Yaml
import qualified Data.Text.Encoding as Text
import Yesod.Auth.Util.PasswordStore (verifyPassword)
maintenanceLogin :: ( YesodAuth site
, YesodPersist site
, IsSqlBackend (YesodPersistBackend site)
, PersistUniqueWrite (YesodPersistBackend site)
) => FilePath -> AuthPlugin site
maintenanceLogin fp = AuthPlugin{..}
where
apName = "PWFile"
apLogin = mempty
apDispatch "GET" [] = do
authData <- lookupBasicAuth
pwdata <- liftIO $ Yaml.decodeFileEither fp
addHeader "WWW-Authenticate" [st|Basic realm="uni2work maintenance auth" charset="UTF-8"|]
case pwdata of
Left err -> $logDebugS "Auth" $ tshow err
Right pws -> $logDebugS "Auth" $ tshow (length pws) <> " pw entries"
case (authData, pwdata) of
(Nothing, _) -> do
notAuthenticated
(Just (usr, (Text.encodeUtf8 -> pw)), Right pwdata')
| [ PWEntry{ pwUser = pwUser@(User{..}), pwHash = (Text.encodeUtf8 -> pwHash) } ]
<- [ pwe | pwe@PWEntry{..} <- pwdata'
, let User{..} = pwUser
, userIdent == usr
, userPlugin == apName
]
, verifyPassword pw pwHash
-> lift $ do
runDB . void $ insertUnique pwUser
setCredsRedirect $ Creds apName userIdent []
_ -> permissionDenied "Invalid auth"
apDispatch _ _ = notFound

105
src/Auth/PWHash.hs Normal file
View File

@ -0,0 +1,105 @@
{-# LANGUAGE NoImplicitPrelude
, QuasiQuotes
, TemplateHaskell
, ViewPatterns
, RecordWildCards
, OverloadedStrings
, FlexibleContexts
, TypeFamilies
#-}
module Auth.PWHash
( hashLogin
, PWHashMessage(..)
) where
import Import.NoFoundation
import Database.Persist.Sql (SqlBackendCanRead)
import Utils.Form
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Yesod.Auth.Util.PasswordStore (verifyPasswordWith)
import qualified Yesod.Auth.Message as Msg
data HashLogin = HashLogin
{ hashIdent :: CI Text
, hashPassword :: Text
}
data PWHashMessage = MsgPWHashIdent
| MsgPWHashPassword
hashForm :: ( RenderMessage site FormMessage
, RenderMessage site PWHashMessage
, Button site SubmitButton
, Show (ButtonCssClass site)
) => AForm (HandlerT site IO) HashLogin
hashForm = HashLogin
<$> areq ciField (fslpI MsgPWHashIdent "Identifikation") Nothing
<*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing
<* submitButton
hashLogin :: ( YesodAuth site
, YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site FormMessage
, RenderMessage site PWHashMessage
, Button site SubmitButton
, Show (ButtonCssClass site)
) => PWHashAlgorithm -> AuthPlugin site
hashLogin pwHashAlgo = AuthPlugin{..}
where
apName = "PWHash"
apDispatch "POST" [] = do
((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard hashForm
case loginRes of
FormFailure errs -> do
forM_ errs $ addMessage "error" . toHtml
redirect LoginR
FormMissing -> redirect LoginR
FormSuccess HashLogin{..} -> do
user <- lift . runDB . getBy $ UniqueAuthentication hashIdent
case user of
Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent })
| verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> -- (2^) is magic.
lift . setCredsRedirect $ Creds apName userIdent []
other -> do
$logDebugS "PWHash" $ tshow other
loginErrorMessageI LoginR Msg.InvalidLogin
-- apDispatch "GET" [] = do
-- authData <- lookupBasicAuth
-- pwdata <- liftIO $ Yaml.decodeFileEither fp
-- addHeader "WWW-Authenticate" [st|Basic realm="uni2work maintenance auth" charset="UTF-8"|]
-- case pwdata of
-- Left err -> $logDebugS "Auth" $ tshow err
-- Right pws -> $logDebugS "Auth" $ tshow (length pws) <> " pw entries"
-- case (authData, pwdata) of
-- (Nothing, _) -> do
-- notAuthenticated
-- (Just (usr, (Text.encodeUtf8 -> pw)), Right pwdata')
-- | [ PWEntry{ pwUser = pwUser@(User{..}), pwHash = (Text.encodeUtf8 -> pwHash) } ]
-- <- [ pwe | pwe@PWEntry{..} <- pwdata'
-- , let User{..} = pwUser
-- , userIdent == usr
-- , userPlugin == apName
-- ]
-- , verifyPassword pw pwHash
-- -> lift $ do
-- runDB . void $ insertUnique pwUser
-- setCredsRedirect $ Creds apName userIdent []
-- _ -> permissionDenied "Invalid auth"
apDispatch _ _ = notFound
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm
$(widgetFile "widgets/hash-login-form")

View File

@ -24,7 +24,7 @@ import Text.Jasmine (minifym)
import Yesod.Auth.Message import Yesod.Auth.Message
import Yesod.Auth.Dummy import Yesod.Auth.Dummy
import Auth.LDAP import Auth.LDAP
import Auth.PWFile import Auth.PWHash
import Auth.Dummy import Auth.Dummy
import qualified Network.Wai as W (requestMethod, pathInfo) import qualified Network.Wai as W (requestMethod, pathInfo)
@ -168,6 +168,7 @@ data MenuTypes -- Semantische Rolle:
mkMessage "UniWorX" "messages/uniworx" "de" mkMessage "UniWorX" "messages/uniworx" "de"
mkMessageVariant "UniWorX" "Campus" "messages/campus" "de" mkMessageVariant "UniWorX" "Campus" "messages/campus" "de"
mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de" mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de"
mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de"
-- This instance is required to use forms. You can modify renderMessage to -- This instance is required to use forms. You can modify renderMessage to
-- achieve customized and internationalized form validation messages. -- achieve customized and internationalized form validation messages.
@ -1141,18 +1142,14 @@ instance YesodAuth UniWorX where
authenticate Creds{..} = runDB $ do authenticate Creds{..} = runDB $ do
let let
(userPlugin, userIdent) userIdent = CI.mk credsIdent
| isDummy uAuth = UniqueAuthentication userIdent
, [dummyPlugin, dummyIdent] <- Text.splitOn ":" credsIdent
= (dummyPlugin, dummyIdent)
| otherwise
= (credsPlugin, credsIdent)
isDummy = credsPlugin == "dummy" isDummy = credsPlugin == "dummy"
isPWFile = credsPlugin == "PWFile" isPWHash = credsPlugin == "PWHash"
uAuth = UniqueAuthentication userPlugin userIdent
excHandlers excHandlers
| isDummy || isPWFile | isDummy || isPWHash
= [ C.Handler $ \err -> do = [ C.Handler $ \err -> do
addMessage "error" (toHtml $ tshow (err :: CampusUserException)) addMessage "error" (toHtml $ tshow (err :: CampusUserException))
$logErrorS "LDAP" $ tshow err $logErrorS "LDAP" $ tshow err
@ -1178,7 +1175,7 @@ instance YesodAuth UniWorX where
flip catches excHandlers $ case appLdapConf of flip catches excHandlers $ case appLdapConf of
Just ldapConf -> fmap (either id id) . runExceptT $ do Just ldapConf -> fmap (either id id) . runExceptT $ do
ldapData <- campusUser ldapConf $ Creds userPlugin userIdent credsExtra ldapData <- campusUser ldapConf $ Creds credsPlugin (CI.original userIdent) credsExtra
$logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData $logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData
let let
@ -1187,6 +1184,10 @@ instance YesodAuth UniWorX where
userDisplayName' = lookup (Attr "displayName") ldapData userDisplayName' = lookup (Attr "displayName") ldapData
userSurname' = lookup (Attr "sn") ldapData userSurname' = lookup (Attr "sn") ldapData
userAuthentication
| isPWHash = error "PWHash should only work for users that are already known"
| otherwise = AuthLDAP
userEmail <- if userEmail <- if
| Just [bs] <- userEmail' | Just [bs] <- userEmail'
, Right userEmail <- Text.decodeUtf8' bs , Right userEmail <- Text.decodeUtf8' bs
@ -1258,7 +1259,7 @@ instance YesodAuth UniWorX where
authPlugins (appSettings -> AppSettings{..}) = catMaybes authPlugins (appSettings -> AppSettings{..}) = catMaybes
[ campusLogin <$> appLdapConf [ campusLogin <$> appLdapConf
, maintenanceLogin <$> appAuthPWFile , Just . hashLogin $ pwHashAlgorithm appAuthPWHash
, dummyLogin <$ guard appAuthDummyLogin , dummyLogin <$ guard appAuthDummyLogin
] ]

View File

@ -317,9 +317,9 @@ getCourseNewR :: Handler Html
getCourseNewR = do getCourseNewR = do
uid <- requireAuthId uid <- requireAuthId
params <- runInputGetResult $ (,,) params <- runInputGetResult $ (,,)
<$> ireq ciTextField "csh" <$> ireq ciField "csh"
<*> iopt textField "tid" <*> iopt textField "tid"
<*> iopt ciTextField "ssh" <*> iopt ciField "ssh"
let noTemplateAction = courseEditHandler True Nothing let noTemplateAction = courseEditHandler True Nothing
case params of case params of
FormMissing -> noTemplateAction FormMissing -> noTemplateAction
@ -530,11 +530,11 @@ newCourseForm template = identForm FIDcourse $ \html -> do
_allOtherCases -> termsActiveField _allOtherCases -> termsActiveField
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm (result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
<$> pure (cfCourseId =<< template) <$> pure (cfCourseId =<< template)
<*> areq (ciField textField) (fslI MsgCourseName) (cfName <$> template) <*> areq ciField (fslI MsgCourseName) (cfName <$> template)
<*> aopt htmlField (fslI MsgCourseDescription <*> aopt htmlField (fslI MsgCourseDescription
& setTooltip MsgCourseDescriptionTip) (cfDesc <$> template) & setTooltip MsgCourseDescriptionTip) (cfDesc <$> template)
<*> aopt urlField (fslI MsgCourseHomepage) (cfLink <$> template) <*> aopt urlField (fslI MsgCourseHomepage) (cfLink <$> template)
<*> areq (ciField textField) (fslI MsgCourseShorthand <*> areq ciField (fslI MsgCourseShorthand
-- & addAttr "disabled" "disabled" -- & addAttr "disabled" "disabled"
& setTooltip MsgCourseShorthandUnique) & setTooltip MsgCourseShorthandUnique)
(cfShort <$> template) (cfShort <$> template)

View File

@ -108,7 +108,7 @@ makeSheetForm msId template = identForm FIDsheet $ \html -> do
mr <- getMsgRenderer mr <- getMsgRenderer
ctime <- liftIO $ getCurrentTime ctime <- liftIO $ getCurrentTime
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm (result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
<$> areq (ciField textField) (fslI MsgSheetName) (sfName <$> template) <$> areq ciField (fslI MsgSheetName) (sfName <$> template)
<*> aopt htmlField (fslI MsgSheetDescription) (sfDescription <$> template) <*> aopt htmlField (fslI MsgSheetDescription) (sfDescription <$> template)
<*> sheetTypeAFormReq (fslI MsgSheetType) (sfType <$> template) <*> sheetTypeAFormReq (fslI MsgSheetType) (sfType <$> template)
<*> sheetGroupAFormReq (fslI MsgSheetGroup) (sfGrouping <$> template) <*> sheetGroupAFormReq (fslI MsgSheetGroup) (sfGrouping <$> template)

View File

@ -71,7 +71,7 @@ makeSubmissionForm msmid uploadMode grouping buddies = identForm FIDsubmission $
(Upload unpackZips) -> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt $ isJust msmid) (zipFileField unpackZips) (fsm $ bool MsgSubmissionFile MsgSubmissionArchive unpackZips) Nothing (Upload unpackZips) -> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt $ isJust msmid) (zipFileField unpackZips) (fsm $ bool MsgSubmissionFile MsgSubmissionArchive unpackZips) Nothing
flip (renderAForm FormStandard) html $ (,) flip (renderAForm FormStandard) html $ (,)
<$> fileUpload <$> fileUpload
<*> (catMaybes <$> sequenceA [bool aforced' aopt editableBuddies (ciField textField) (fslpI (MsgSubmissionMember g) "user@campus.lmu.de" ) buddy <*> (catMaybes <$> sequenceA [bool aforced' aopt editableBuddies ciField (fslpI (MsgSubmissionMember g) "user@campus.lmu.de" ) buddy
| g <- [1..(max groupNr $ length buddies)] -- groupNr might have decreased meanwhile | g <- [1..(max groupNr $ length buddies)] -- groupNr might have decreased meanwhile
| buddy <- map (Just . Just) buddies ++ repeat Nothing -- show current buddies | buddy <- map (Just . Just) buddies ++ repeat Nothing -- show current buddies
]) ])

View File

@ -14,6 +14,8 @@ import Handler.Utils
import Utils.Lens import Utils.Lens
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
@ -116,7 +118,7 @@ postAdminHijackUserR cID = do
permissionDenied "Cannot escalate admin status to additional schools" permissionDenied "Cannot escalate admin status to additional schools"
get404 uid get404 uid
setCredsRedirect $ Creds "dummy" (userPlugin <> ":" <> userIdent) [] setCredsRedirect $ Creds "dummy" (CI.original userIdent) []
| otherwise -> error "This should be impossible by definition of `hijackUserForm`" | otherwise -> error "This should be impossible by definition of `hijackUserForm`"
FormFailure errs -> toTypedContent <$> mapM_ (addMessage "error" . toHtml) errs FormFailure errs -> toTypedContent <$> mapM_ (addMessage "error" . toHtml) errs
FormMissing -> return $ toTypedContent () FormMissing -> return $ toTypedContent ()

View File

@ -183,11 +183,7 @@ buttonForm csrf = do
-- Fields -- -- Fields --
------------ ------------
ciField :: (Functor m, CI.FoldCase a) => Field m a -> Field m (CI a) -- ciField moved to Utils.Form
ciField = convertField CI.mk CI.original
ciTextField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m (CI Text)
ciTextField = ciField textField
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
natFieldI msg = checkBool (>= 0) msg intField natFieldI msg = checkBool (>= 0) msg intField

View File

@ -180,6 +180,15 @@ customMigrations = Map.fromListWith (>>)
ALTER TABLE "sheet" ADD COLUMN "upload_mode" json DEFAULT '{ "tag": "Upload", "unpackZips": true }'; ALTER TABLE "sheet" ADD COLUMN "upload_mode" json DEFAULT '{ "tag": "Upload", "unpackZips": true }';
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|3.2.0|] [version|4.0.0|]
, whenM (tableExists "user") $ do
-- <> is standard sql for /=
[executeQQ|
DELETE FROM "user" WHERE "plugin" <> 'LDAP';
ALTER TABLE "user" DROP COLUMN "plugin";
ALTER TABLE "user" ADD COLUMN "authentication" json DEFAULT '"ldap"';
|]
)
] ]

View File

@ -46,12 +46,14 @@ import Data.CaseInsensitive.Instances ()
import Yesod.Core.Dispatch (PathPiece(..)) import Yesod.Core.Dispatch (PathPiece(..))
import Data.Aeson (FromJSON(..), ToJSON(..), withText, Value(..)) import Data.Aeson (FromJSON(..), ToJSON(..), withText, Value(..))
import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..)) import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..), SumEncoding(..))
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
import qualified Yesod.Auth.Util.PasswordStore as PWStore
instance PathPiece UUID where instance PathPiece UUID where
fromPathPiece = Data.UUID.Types.fromString . unpack fromPathPiece = Data.UUID.Types.fromString . unpack
@ -388,7 +390,7 @@ instance PathPiece obj => PathPiece (ZIPArchiveName obj) where
toPathPiece = (<> ".zip") . toPathPiece . unZIPArchiveName toPathPiece = (<> ".zip") . toPathPiece . unZIPArchiveName
newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String } newtype DateTimeFormat = DateTimeFormat { unDateTimeFormat :: String }
deriving (Eq, Ord, Read, Show, ToJSON, FromJSON, PersistField, PersistFieldSql) deriving (Eq, Ord, Read, Show, ToJSON, FromJSON, PersistField, PersistFieldSql, IsString)
data SelDateTimeFormat = SelFormatDateTime | SelFormatDate | SelFormatTime data SelDateTimeFormat = SelFormatDateTime | SelFormatDate | SelFormatTime
deriving (Eq, Ord, Read, Show, Enum, Bounded) deriving (Eq, Ord, Read, Show, Enum, Bounded)
@ -405,12 +407,25 @@ instance Universe CorrectorState where universe = universeDef
instance Finite CorrectorState instance Finite CorrectorState
instance PathPiece CorrectorState where instance PathPiece CorrectorState where
toPathPiece = $(nullaryToPathPiece ''CorrectorState [Text.intercalate "-" . map Text.toLower . unsafeTail . splitCamel]) toPathPiece = $(nullaryToPathPiece ''CorrectorState [Text.intercalate "-" . map toLower . unsafeTail . splitCamel])
fromPathPiece = finiteFromPathPiece fromPathPiece = finiteFromPathPiece
derivePersistField "CorrectorState" derivePersistField "CorrectorState"
data AuthenticationMode = AuthLDAP
| AuthPWHash { authPWHash :: Text }
deriving (Eq, Ord, Read, Show)
deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, sumEncoding = UntaggedValue
} ''AuthenticationMode
derivePersistFieldJSON ''AuthenticationMode
-- Type synonyms -- Type synonyms
type SchoolName = CI Text type SchoolName = CI Text
@ -419,3 +434,5 @@ type CourseName = CI Text
type CourseShorthand = CI Text type CourseShorthand = CI Text
type SheetName = CI Text type SheetName = CI Text
type UserEmail = CI Text type UserEmail = CI Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString

View File

@ -16,6 +16,7 @@ import ClassyPrelude.Yesod
import qualified Control.Exception as Exception import qualified Control.Exception as Exception
import Data.Aeson (Result (..), fromJSON, withObject, import Data.Aeson (Result (..), fromJSON, withObject,
(.!=), (.:?)) (.!=), (.:?))
import qualified Data.Aeson.Types as Aeson
import Data.Aeson.TH import Data.Aeson.TH
import Data.FileEmbed (embedFile) import Data.FileEmbed (embedFile)
import Data.Yaml (decodeEither') import Data.Yaml (decodeEither')
@ -26,6 +27,7 @@ import Yesod.Default.Config2 (applyEnvValue, configSettingsYml)
import Yesod.Default.Util (WidgetFileSettings, import Yesod.Default.Util (WidgetFileSettings,
widgetFileNoReload, widgetFileNoReload,
widgetFileReload) widgetFileReload)
import qualified Yesod.Auth.Util.PasswordStore as PWStore
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
@ -74,11 +76,10 @@ data AppSettings = AppSettings
-- ^ Indicate if auth dummy login should be enabled. -- ^ Indicate if auth dummy login should be enabled.
, appAllowDeprecated :: Bool , appAllowDeprecated :: Bool
-- ^ Indicate if deprecated routes are accessible for everyone -- ^ Indicate if deprecated routes are accessible for everyone
, appAuthPWFile :: Maybe FilePath
-- ^ If set authenticate against a local password file
, appMinimumLogLevel :: LogLevel , appMinimumLogLevel :: LogLevel
, appUserDefaults :: UserDefaultConf , appUserDefaults :: UserDefaultConf
, appAuthPWHash :: PWHashConf
, appCryptoIDKeyFile :: FilePath , appCryptoIDKeyFile :: FilePath
} }
@ -90,16 +91,21 @@ data UserDefaultConf = UserDefaultConf
, userDefaultDownloadFiles :: Bool , userDefaultDownloadFiles :: Bool
} }
instance FromJSON UserDefaultConf where data PWHashConf = PWHashConf
parseJSON = withObject "UserDefaultConf" $ \o -> do { pwHashAlgorithm :: PWHashAlgorithm
userDefaultTheme <- o .: "theme" , pwHashStrength :: Int
userDefaultMaxFavourites <- o .: "favourites" }
userDefaultDateTimeFormat <- o .: "date-time-format"
userDefaultDateFormat <- o .: "date-format"
userDefaultTimeFormat <- o .: "time-format"
userDefaultDownloadFiles <- o .: "download-files"
return UserDefaultConf{..} instance FromJSON PWHashConf where
parseJSON = withObject "PWHashConf" $ \o -> do
pwHashAlgorithm' <- (o .: "algorithm" :: Aeson.Parser Text)
pwHashAlgorithm <- if
| pwHashAlgorithm' == "pbkdf1" -> return PWStore.pbkdf1
| pwHashAlgorithm' == "pbkdf2" -> return PWStore.pbkdf2
| otherwise -> fail "Unsupported hash algorithm"
pwHashStrength <- o .: "strength"
return PWHashConf{..}
data LdapConf = LdapConf data LdapConf = LdapConf
{ ldapHost :: Ldap.Host, ldapPort :: Ldap.PortNumber { ldapHost :: Ldap.Host, ldapPort :: Ldap.PortNumber
@ -110,6 +116,9 @@ data LdapConf = LdapConf
} }
deriveFromJSON defaultOptions ''Ldap.Scope deriveFromJSON defaultOptions ''Ldap.Scope
deriveFromJSON defaultOptions
{ fieldLabelModifier = intercalate "-" . map toLower . drop 2 . splitCamel
} ''UserDefaultConf
instance FromJSON LdapConf where instance FromJSON LdapConf where
parseJSON = withObject "LdapConf" $ \o -> do parseJSON = withObject "LdapConf" $ \o -> do
@ -164,9 +173,9 @@ instance FromJSON AppSettings where
appSkipCombining <- o .:? "skip-combining" .!= defaultDev appSkipCombining <- o .:? "skip-combining" .!= defaultDev
appAuthDummyLogin <- o .:? "auth-dummy-login" .!= defaultDev appAuthDummyLogin <- o .:? "auth-dummy-login" .!= defaultDev
appAllowDeprecated <- o .:? "allow-deprecated" .!= defaultDev appAllowDeprecated <- o .:? "allow-deprecated" .!= defaultDev
appAuthPWFile <- assertM (not . null) <$> o .:? "auth-pwfile"
appUserDefaults <- o .: "user-defaults" appUserDefaults <- o .: "user-defaults"
appAuthPWHash <- o .: "auth-pw-hash"
appCryptoIDKeyFile <- o .: "cryptoid-keyfile" appCryptoIDKeyFile <- o .: "cryptoid-keyfile"

View File

@ -20,6 +20,9 @@ import qualified Text.Blaze.Internal as Blaze (null)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Char as Char import qualified Data.Char as Char
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Web.PathPieces import Web.PathPieces
------------------- -------------------
@ -202,3 +205,14 @@ combinedButtonField btns = traverse b2f btns
submitButton :: (Button site SubmitButton, Show (ButtonCssClass site)) => AForm (HandlerT site IO) () submitButton :: (Button site SubmitButton, Show (ButtonCssClass site)) => AForm (HandlerT site IO) ()
submitButton = void $ combinedButtonField [BtnSubmit] submitButton = void $ combinedButtonField [BtnSubmit]
-------------------
-- Custom Fields --
-------------------
ciField :: ( Textual t
, CI.FoldCase t
, Monad m
, RenderMessage (HandlerSite m) FormMessage
) => Field m (CI t)
ciField = convertField (CI.mk . fromList . unpack) (pack . toList . CI.original) textField

View File

@ -35,14 +35,14 @@ nullaryToPathPiece nullaryType manglers = do
where where
mangle = appEndo (foldMap Endo manglers) . Text.pack mangle = appEndo (foldMap Endo manglers) . Text.pack
splitCamel :: Text -> [Text] splitCamel :: Textual t => t -> [t]
splitCamel = map Text.pack . reverse . helper (error "hasChange undefined at start of string") [] "" . Text.unpack splitCamel = map fromList . reverse . helper (error "hasChange undefined at start of string") [] "" . otoList
where where
helper _hadChange items thisWord [] = reverse thisWord : items helper _hadChange items thisWord [] = reverse thisWord : items
helper _hadChange items [] (c:cs) = helper True items [c] cs helper _hadChange items [] (c:cs) = helper True items [c] cs
helper hadChange items ws@(w:ws') (c:cs) helper hadChange items ws@(w:ws') (c:cs)
| sameCategory w c | sameCategory w c
, null ws' = helper False items (c:ws) cs , null ws' = helper (Char.isLower w) items (c:ws) cs
| sameCategory w c = helper hadChange items (c:ws) cs | sameCategory w c = helper hadChange items (c:ws) cs
| null ws' = helper True items (c:ws) cs | null ws' = helper True items (c:ws) cs
| not hadChange = helper True (reverse ws':items) [c,w] cs | not hadChange = helper True (reverse ws':items) [c,w] cs

View File

@ -3,6 +3,11 @@ $forall AuthPlugin{..} <- plugins
<section> <section>
<h2>_{MsgLDAPLoginTitle} <h2>_{MsgLDAPLoginTitle}
^{apLogin toParent} ^{apLogin toParent}
$elseif apName == "PWHash"
<section>
<h2>_{MsgPWHashLoginTitle}
<p>_{MsgPWHashLoginNote}
^{apLogin toParent}
$elseif apName == "dummy" $elseif apName == "dummy"
<section> <section>
<h2>_{MsgDummyLoginTitle} <h2>_{MsgDummyLoginTitle}

View File

@ -10,8 +10,6 @@
<dd .deflist__dd> #{display userEmail} <dd .deflist__dd> #{display userEmail}
<dt .deflist__dt> _{MsgIdent} <dt .deflist__dt> _{MsgIdent}
<dd .deflist__dd> #{display userIdent} <dd .deflist__dd> #{display userIdent}
<dt .deflist__dt> _{MsgPlugin}
<dd .deflist__dd> #{display userPlugin}
$if not $ null admin_rights $if not $ null admin_rights
<dt .deflist__dt> Administrator <dt .deflist__dt> Administrator
<dd .deflist__dd> <dd .deflist__dd>

View File

@ -0,0 +1,2 @@
<form method=POST action=@{toMaster $ PluginR "PWHash" []} enctype=#{loginEnctype}>
^{login}