Token revocation & Bugfixes
This commit is contained in:
parent
af6821c7c8
commit
6e29d8ed89
@ -10,6 +10,7 @@ BtnSave: Speichern
|
|||||||
BtnCandidatesInfer: Studienfachzuordnung automatisch lernen
|
BtnCandidatesInfer: Studienfachzuordnung automatisch lernen
|
||||||
BtnCandidatesDeleteConflicts: Konflikte löschen
|
BtnCandidatesDeleteConflicts: Konflikte löschen
|
||||||
BtnCandidatesDeleteAll: Alle Beobachtungen löschen
|
BtnCandidatesDeleteAll: Alle Beobachtungen löschen
|
||||||
|
BtnResetTokens: Authorisierungs-Tokens invalidieren
|
||||||
|
|
||||||
Aborted: Abgebrochen
|
Aborted: Abgebrochen
|
||||||
Remarks: Hinweise
|
Remarks: Hinweise
|
||||||
@ -279,6 +280,8 @@ DataProtHeading: Datenschutzerklärung
|
|||||||
SystemMessageHeading: Uni2work Statusmeldung
|
SystemMessageHeading: Uni2work Statusmeldung
|
||||||
SystemMessageListHeading: Uni2work Statusmeldungen
|
SystemMessageListHeading: Uni2work Statusmeldungen
|
||||||
NotificationSettingsHeading displayName@Text: Benachrichtigungs-Einstellungen für #{displayName}
|
NotificationSettingsHeading displayName@Text: Benachrichtigungs-Einstellungen für #{displayName}
|
||||||
|
TokensLastReset: Tokens zuletzt invalidiert
|
||||||
|
TokensResetSuccess: Authorisierungs-Tokens invalidiert
|
||||||
|
|
||||||
HomeOpenCourses: Kurse mit offener Registrierung
|
HomeOpenCourses: Kurse mit offener Registrierung
|
||||||
HomeUpcomingSheets: Anstehende Übungsblätter
|
HomeUpcomingSheets: Anstehende Übungsblätter
|
||||||
|
|||||||
@ -11,6 +11,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
|||||||
ident (CI Text) -- Case-insensitive user-identifier
|
ident (CI Text) -- Case-insensitive user-identifier
|
||||||
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
|
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
|
||||||
lastAuthentication UTCTime Maybe -- last login date
|
lastAuthentication UTCTime Maybe -- last login date
|
||||||
|
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
|
||||||
matrikelnummer Text Maybe -- optional immatriculation-string; usually a number, but not always (e.g. lecturers, pupils, guests,...)
|
matrikelnummer Text Maybe -- optional immatriculation-string; usually a number, but not always (e.g. lecturers, pupils, guests,...)
|
||||||
email (CI Text) -- Case-insensitive eMail address
|
email (CI Text) -- Case-insensitive eMail address
|
||||||
displayName Text -- we only show LDAP-DisplayName, and highlight LDAP-Surname within (appended if not contained)
|
displayName Text -- we only show LDAP-DisplayName, and highlight LDAP-Surname within (appended if not contained)
|
||||||
|
|||||||
@ -474,26 +474,26 @@ tagAccessPredicate AuthAdmin = APDB $ \mAuthId route _ -> case route of
|
|||||||
guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedSiteAdmin)
|
guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedSiteAdmin)
|
||||||
return Authorized
|
return Authorized
|
||||||
tagAccessPredicate AuthToken = APDB $ \mAuthId route isWrite -> exceptT return return $ do
|
tagAccessPredicate AuthToken = APDB $ \mAuthId route isWrite -> exceptT return return $ do
|
||||||
jwt <- maybeTMExceptT (unauthorizedI MsgUnauthorizedNoToken) $ asum
|
jwt <- maybeMExceptT (unauthorizedI MsgUnauthorizedNoToken) askJwt
|
||||||
[ MaybeT lookupBearerAuth >>= hoistMaybe . fromPathPiece
|
|
||||||
, MaybeT $ lookupGlobalPostParam PostToken
|
|
||||||
, MaybeT $ lookupGlobalGetParam GetToken
|
|
||||||
]
|
|
||||||
BearerToken{..} <- catch (decodeToken jwt) $ \case
|
BearerToken{..} <- catch (decodeToken jwt) $ \case
|
||||||
BearerTokenExpired -> throwError =<< unauthorizedI MsgUnauthorizedTokenExpired
|
BearerTokenExpired -> throwError =<< unauthorizedI MsgUnauthorizedTokenExpired
|
||||||
BearerTokenNotStarted -> throwError =<< unauthorizedI MsgUnauthorizedTokenNotStarted
|
BearerTokenNotStarted -> throwError =<< unauthorizedI MsgUnauthorizedTokenNotStarted
|
||||||
other -> do
|
other -> do
|
||||||
$logWarnS "AuthToken" $ tshow other
|
$logWarnS "AuthToken" $ tshow other
|
||||||
throwError =<< unauthorizedI MsgUnauthorizedTokenInvalid
|
throwError =<< unauthorizedI MsgUnauthorizedTokenInvalid
|
||||||
unless (maybe True (HashSet.member route) tokenRoutes) $
|
|
||||||
throwError =<< unauthorizedI MsgUnauthorizedTokenInvalidRoute
|
guardMExceptT (maybe True (HashSet.member route) tokenRoutes) (unauthorizedI MsgUnauthorizedTokenInvalidRoute)
|
||||||
|
|
||||||
|
User{userTokensIssuedAfter} <- lift $ get404 tokenAuthority
|
||||||
|
guardMExceptT (Just tokenIssuedAt >= userTokensIssuedAfter) (unauthorizedI MsgUnauthorizedTokenExpired)
|
||||||
|
|
||||||
authorityVal <- evalAccessFor (Just tokenAuthority) route isWrite
|
authorityVal <- evalAccessFor (Just tokenAuthority) route isWrite
|
||||||
unless (is _Authorized authorityVal) $
|
guardExceptT (is _Authorized authorityVal) authorityVal
|
||||||
throwError authorityVal
|
|
||||||
whenIsJust tokenAddAuth $ \addDNF -> do
|
whenIsJust tokenAddAuth $ \addDNF -> do
|
||||||
additionalVal <- fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) addDNF mAuthId route isWrite
|
additionalVal <- fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) addDNF mAuthId route isWrite
|
||||||
unless (is _Authorized additionalVal) $
|
guardExceptT (is _Authorized additionalVal) additionalVal
|
||||||
throwError additionalVal
|
|
||||||
return Authorized
|
return Authorized
|
||||||
tagAccessPredicate AuthNoEscalation = APDB $ \mAuthId route _ -> case route of
|
tagAccessPredicate AuthNoEscalation = APDB $ \mAuthId route _ -> case route of
|
||||||
AdminHijackUserR cID -> exceptT return return $ do
|
AdminHijackUserR cID -> exceptT return return $ do
|
||||||
@ -2206,6 +2206,7 @@ instance YesodAuth UniWorX where
|
|||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userMailLanguages = def
|
, userMailLanguages = def
|
||||||
|
, userTokensIssuedAfter = Nothing
|
||||||
, ..
|
, ..
|
||||||
}
|
}
|
||||||
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
|
userUpdate = [ UserMatrikelnummer =. userMatrikelnummer
|
||||||
|
|||||||
@ -79,7 +79,26 @@ notificationForm :: Maybe NotificationSettings -> AForm Handler NotificationSett
|
|||||||
notificationForm template = NotificationSettings <$> funcForm nsForm (fslI MsgNotificationSettings) True
|
notificationForm template = NotificationSettings <$> funcForm nsForm (fslI MsgNotificationSettings) True
|
||||||
where
|
where
|
||||||
nsForm nt = fromMaybe False <$> aopt checkBoxField (fslI nt) (Just $ flip notificationAllowed nt <$> template)
|
nsForm nt = fromMaybe False <$> aopt checkBoxField (fslI nt) (Just $ flip notificationAllowed nt <$> template)
|
||||||
|
|
||||||
|
|
||||||
|
data ButtonResetTokens = BtnResetTokens
|
||||||
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe ButtonResetTokens
|
||||||
|
instance Finite ButtonResetTokens
|
||||||
|
|
||||||
|
nullaryPathPiece ''ButtonResetTokens $ camelToPathPiece' 1
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''ButtonResetTokens id
|
||||||
|
instance Button UniWorX ButtonResetTokens where
|
||||||
|
btnClasses BtnResetTokens = [BCIsButton, BCDanger]
|
||||||
|
|
||||||
|
data ProfileAnchor = ProfileSettings | ProfileResetTokens
|
||||||
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe ProfileAnchor
|
||||||
|
instance Finite ProfileAnchor
|
||||||
|
|
||||||
|
nullaryPathPiece ''ProfileAnchor $ camelToPathPiece' 1
|
||||||
|
|
||||||
|
|
||||||
getProfileR, postProfileR :: Handler Html
|
getProfileR, postProfileR :: Handler Html
|
||||||
getProfileR = postProfileR
|
getProfileR = postProfileR
|
||||||
@ -94,37 +113,60 @@ postProfileR = do
|
|||||||
, stgDownloadFiles = userDownloadFiles
|
, stgDownloadFiles = userDownloadFiles
|
||||||
, stgNotificationSettings = userNotificationSettings
|
, stgNotificationSettings = userNotificationSettings
|
||||||
}
|
}
|
||||||
((res,formWidget), formEnctype) <- runFormPost $ makeSettingForm settingsTemplate
|
((res,formWidget), formEnctype) <- runFormPost . identifyForm ProfileSettings $ makeSettingForm settingsTemplate
|
||||||
case res of
|
|
||||||
(FormSuccess SettingsForm{..}) -> do
|
|
||||||
runDB $ do
|
|
||||||
update uid [ UserMaxFavourites =. stgMaxFavourties
|
|
||||||
, UserTheme =. stgTheme
|
|
||||||
, UserDateTimeFormat =. stgDateTime
|
|
||||||
, UserDateFormat =. stgDate
|
|
||||||
, UserTimeFormat =. stgTime
|
|
||||||
, UserDownloadFiles =. stgDownloadFiles
|
|
||||||
, UserNotificationSettings =. stgNotificationSettings
|
|
||||||
]
|
|
||||||
when (stgMaxFavourties < userMaxFavourites) $ do
|
|
||||||
-- prune Favourites to user-defined size
|
|
||||||
oldFavs <- selectKeysList [ CourseFavouriteUser ==. uid]
|
|
||||||
[ Desc CourseFavouriteTime
|
|
||||||
, OffsetBy stgMaxFavourties
|
|
||||||
]
|
|
||||||
mapM_ delete oldFavs
|
|
||||||
addMessageI Info MsgSettingsUpdate
|
|
||||||
redirect ProfileR -- TODO: them change does not happen without redirect
|
|
||||||
|
|
||||||
(FormFailure msgs) -> forM_ msgs $ addMessage Warning . toHtml
|
formResult res $ \SettingsForm{..} -> do
|
||||||
_ -> return ()
|
runDB $ do
|
||||||
|
update uid [ UserMaxFavourites =. stgMaxFavourties
|
||||||
|
, UserTheme =. stgTheme
|
||||||
|
, UserDateTimeFormat =. stgDateTime
|
||||||
|
, UserDateFormat =. stgDate
|
||||||
|
, UserTimeFormat =. stgTime
|
||||||
|
, UserDownloadFiles =. stgDownloadFiles
|
||||||
|
, UserNotificationSettings =. stgNotificationSettings
|
||||||
|
]
|
||||||
|
when (stgMaxFavourties < userMaxFavourites) $ do
|
||||||
|
-- prune Favourites to user-defined size
|
||||||
|
oldFavs <- selectKeysList [ CourseFavouriteUser ==. uid]
|
||||||
|
[ Desc CourseFavouriteTime
|
||||||
|
, OffsetBy stgMaxFavourties
|
||||||
|
]
|
||||||
|
mapM_ delete oldFavs
|
||||||
|
addMessageI Info MsgSettingsUpdate
|
||||||
|
redirect $ ProfileR :#: ProfileSettings
|
||||||
|
|
||||||
|
((tokenRes, tokenFormWidget), tokenEnctype) <- runFormPost . identifyForm ProfileResetTokens $ buttonForm
|
||||||
|
|
||||||
|
formResult tokenRes $ \BtnResetTokens -> do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
runDB $ update uid [ UserTokensIssuedAfter =. Just now ]
|
||||||
|
addMessageI Info MsgTokensResetSuccess
|
||||||
|
redirect $ ProfileR :#: ProfileResetTokens
|
||||||
|
|
||||||
|
tResetTime <- traverse (formatTime SelFormatDateTime) userTokensIssuedAfter
|
||||||
|
|
||||||
siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do
|
siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do
|
||||||
setTitle . toHtml $ "Profil " <> userIdent
|
setTitle . toHtml $ "Profil " <> userIdent
|
||||||
wrapForm formWidget def
|
let settingsForm =
|
||||||
{ formAction = Just $ SomeRoute ProfileR
|
wrapForm formWidget FormSettings
|
||||||
, formEncoding = formEnctype
|
{ formMethod = POST
|
||||||
}
|
, formAction = Just . SomeRoute $ ProfileR :#: ProfileSettings
|
||||||
|
, formEncoding = formEnctype
|
||||||
|
, formAttrs = []
|
||||||
|
, formSubmit = FormSubmit
|
||||||
|
, formAnchor = Just ProfileSettings
|
||||||
|
}
|
||||||
|
tokenForm =
|
||||||
|
wrapForm tokenFormWidget FormSettings
|
||||||
|
{ formMethod = POST
|
||||||
|
, formAction = Just . SomeRoute $ ProfileR :#: ProfileResetTokens
|
||||||
|
, formEncoding = tokenEnctype
|
||||||
|
, formAttrs = []
|
||||||
|
, formSubmit = FormNoSubmit
|
||||||
|
, formAnchor = Just ProfileResetTokens
|
||||||
|
}
|
||||||
|
tokenExplanation = $(i18nWidgetFile "profile/tokenExplanation")
|
||||||
|
$(widgetFile "profile/profile")
|
||||||
|
|
||||||
|
|
||||||
getProfileDataR :: Handler Html
|
getProfileDataR :: Handler Html
|
||||||
@ -544,18 +586,27 @@ getUserNotificationR = postUserNotificationR
|
|||||||
postUserNotificationR cID = do
|
postUserNotificationR cID = do
|
||||||
uid <- decrypt cID
|
uid <- decrypt cID
|
||||||
User{userNotificationSettings, userDisplayName} <- runDB $ get404 uid
|
User{userNotificationSettings, userDisplayName} <- runDB $ get404 uid
|
||||||
|
|
||||||
((nsRes, nsInnerWdgt), nsEnc) <- runFormPost . renderAForm FormStandard . notificationForm $ Just userNotificationSettings
|
((nsRes, nsInnerWdgt), nsEnc) <- runFormPost . renderAForm FormStandard . notificationForm $ Just userNotificationSettings
|
||||||
|
mJwt <- askJwt
|
||||||
isModal <- hasCustomHeader HeaderIsModal
|
isModal <- hasCustomHeader HeaderIsModal
|
||||||
let formWidget = wrapForm nsInnerWdgt def
|
let formWidget = wrapForm nsInnerWdgt' def
|
||||||
{ formAction = Just . SomeRoute $ UserNotificationR cID
|
{ formAction = Just . SomeRoute $ UserNotificationR cID
|
||||||
, formEncoding = nsEnc
|
, formEncoding = nsEnc
|
||||||
, formAttrs = [ ("data-ajax-submit", "") | isModal ]
|
, formAttrs = [ ("data-ajax-submit", "") | isModal ]
|
||||||
}
|
}
|
||||||
|
nsInnerWdgt'
|
||||||
|
= [whamlet|
|
||||||
|
$newline never
|
||||||
|
$maybe jwt <- mJwt
|
||||||
|
<input type=hidden name=#{toPathPiece PostToken} value=#{toPathPiece jwt}>
|
||||||
|
^{nsInnerWdgt}
|
||||||
|
|]
|
||||||
|
|
||||||
formResultModal nsRes (UserNotificationR cID) $ \ns -> do
|
formResultModal nsRes (UserNotificationR cID, [ (toPathPiece GetToken, toPathPiece jwt) | Just jwt <- pure mJwt ]) $ \ns -> do
|
||||||
lift . runDB $ update uid [ UserNotificationSettings =. ns ]
|
lift . runDB $ update uid [ UserNotificationSettings =. ns ]
|
||||||
tell . pure =<< messageI Success MsgNotificationSettingsUpdate
|
tell . pure =<< messageI Success MsgNotificationSettingsUpdate
|
||||||
|
|
||||||
siteLayoutMsg (MsgNotificationSettingsHeading userDisplayName) $
|
siteLayoutMsg (MsgNotificationSettingsHeading userDisplayName) $ do
|
||||||
|
setTitleI $ MsgNotificationSettingsHeading userDisplayName
|
||||||
formWidget
|
formWidget
|
||||||
|
|||||||
@ -5,13 +5,15 @@ module Model.Token
|
|||||||
, bearerToken
|
, bearerToken
|
||||||
, encodeToken, BearerTokenException(..), decodeToken
|
, encodeToken, BearerTokenException(..), decodeToken
|
||||||
, tokenToJSON, tokenParseJSON, tokenParseJSON'
|
, tokenToJSON, tokenParseJSON, tokenParseJSON'
|
||||||
|
, askJwt
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod
|
import ClassyPrelude.Yesod
|
||||||
import Model
|
import Model
|
||||||
import Settings
|
import Settings
|
||||||
import Utils (NTop(..))
|
import Utils (NTop(..), hoistMaybe)
|
||||||
import Utils.Lens hiding ((.=))
|
import Utils.Lens hiding ((.=))
|
||||||
|
import Utils.Parameters
|
||||||
|
|
||||||
import Yesod.Auth (AuthId)
|
import Yesod.Auth (AuthId)
|
||||||
|
|
||||||
@ -40,6 +42,7 @@ import Data.Time.Clock
|
|||||||
import Data.Time.Clock.POSIX
|
import Data.Time.Clock.POSIX
|
||||||
|
|
||||||
import Control.Monad.Random (MonadRandom(..))
|
import Control.Monad.Random (MonadRandom(..))
|
||||||
|
import Control.Monad.Trans.Maybe (MaybeT(..))
|
||||||
|
|
||||||
|
|
||||||
data BearerToken site = BearerToken
|
data BearerToken site = BearerToken
|
||||||
@ -203,3 +206,15 @@ decodeToken (Jwt bs) = do
|
|||||||
unless (tokenStartsAt <= Just now) $
|
unless (tokenStartsAt <= Just now) $
|
||||||
throwM BearerTokenNotStarted
|
throwM BearerTokenNotStarted
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
askJwt :: forall m.
|
||||||
|
( MonadHandler m
|
||||||
|
)
|
||||||
|
=> m (Maybe Jwt)
|
||||||
|
-- | Retrieve current `Jwt` from HTTP-Header, POST-Parameter, or GET-Parameter
|
||||||
|
askJwt = runMaybeT $ asum
|
||||||
|
[ MaybeT lookupBearerAuth >>= hoistMaybe . fromPathPiece
|
||||||
|
, MaybeT $ lookupGlobalPostParam PostToken
|
||||||
|
, MaybeT $ lookupGlobalGetParam GetToken
|
||||||
|
]
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
<div .profile>
|
|
||||||
^{settingsForm}
|
|
||||||
13
templates/profile/profile.hamlet
Normal file
13
templates/profile/profile.hamlet
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
$newline never
|
||||||
|
<section>
|
||||||
|
^{settingsForm}
|
||||||
|
<section>
|
||||||
|
^{tokenExplanation}
|
||||||
|
<p>
|
||||||
|
_{MsgTokensLastReset}:
|
||||||
|
$maybe tResetTime' <- tResetTime
|
||||||
|
\ #{tResetTime'}
|
||||||
|
$nothing
|
||||||
|
\ _{MsgNever}
|
||||||
|
<br />
|
||||||
|
^{tokenForm}
|
||||||
13
templates/profile/tokenExplanation/de.hamlet
Normal file
13
templates/profile/tokenExplanation/de.hamlet
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<p>
|
||||||
|
Das System stellt gelegentlich Benutzer-bezogene <i>Authorisierungs-Tokens</i> aus.
|
||||||
|
Diese Tokens erlauben es jedem, der in Besitz dieses Tokens ist, bestimmte Ihrer Benutzer-Rechte anzunehmen.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Dies ist insbesondere notwendig um verschickten Emails einen Link beifügen zu können, der das Deabonnieren von Benachrichtigungen erlaubt.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Mit dem untigen Knopf können Sie alle Authorisierungs-Tokens, die bisher für Sie ausgestellt wurden, als ungültig markieren.
|
||||||
|
Dies ist zum Beispiel dann notwendig, wenn Sie Grund haben zu vermuten, dass Dritte Zugriff auf eines Ihrer Tokens gehabt haben könnten.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Für die sichere Verwahrung Ihnen ausgehändigter Tokens sind immer Sie selbst verantwortlich.
|
||||||
Loading…
Reference in New Issue
Block a user