diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index a7fae6f0b45..223a0444942 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -47,7 +47,7 @@ import Wire.EmailSubsystem import Wire.Events import Wire.HashPassword import Wire.PasswordResetCodeStore -import Wire.PasswordStore (PasswordStore, upsertHashedPassword) +import Wire.PasswordStore (PasswordStore) import Wire.PasswordStore qualified as PasswordStore import Wire.RateLimit import Wire.Sem.Now @@ -55,7 +55,8 @@ import Wire.Sem.Now qualified as Now import Wire.Sem.Random (Random) import Wire.SessionStore import Wire.UserKeyStore -import Wire.UserStore +import Wire.UserStore (UserStore) +import Wire.UserStore qualified as UserStore import Wire.UserSubsystem (UserSubsystem, getLocalAccountBy) import Wire.UserSubsystem qualified as User @@ -119,7 +120,6 @@ instance Exception PasswordResetError where authenticateEitherImpl :: ( Member UserStore r, Member HashPassword r, - Member PasswordStore r, Member RateLimit r ) => UserId -> @@ -127,7 +127,7 @@ authenticateEitherImpl :: Sem r (Either AuthError ()) authenticateEitherImpl uid plaintext = do runError $ - getUserAuthenticationInfo uid >>= \case + UserStore.getUserAuthenticationInfo uid >>= \case Nothing -> throw AuthInvalidUser Just (_, Deleted) -> throw AuthInvalidUser Just (_, Suspended) -> throw AuthSuspended @@ -144,7 +144,7 @@ authenticateEitherImpl uid plaintext = do hashAndUpdatePwd pwd = do tryHashPassword6 rateLimitKey pwd >>= \case Left _ -> pure () - Right hashed -> upsertHashedPassword uid hashed + Right hashed -> UserStore.upsertHashedPassword uid hashed -- | Password reauthentication. If the account has a password, reauthentication -- is mandatory. If @@ -161,7 +161,7 @@ reauthenticateEitherImpl :: Maybe (PlainTextPassword' t) -> Sem r (Either ReAuthError ()) reauthenticateEitherImpl user plaintextMaybe = - getUserAuthenticationInfo user + UserStore.getUserAuthenticationInfo user >>= runError . \case Nothing -> throw (ReAuthError AuthInvalidUser) @@ -296,8 +296,8 @@ resetPasswordImpl :: Member UserSubsystem r, Member HashPassword r, Member SessionStore r, - Member PasswordStore r, - Member RateLimit r + Member RateLimit r, + Member UserStore r ) => PasswordResetIdentity -> PasswordResetCode -> @@ -314,7 +314,7 @@ resetPasswordImpl ident code pw = do Log.debug $ field "user" (toByteString uid) . field "action" (val "User.completePasswordReset") checkNewIsDifferent uid pw hashedPw <- hashPassword8 rateLimitKey pw - PasswordStore.upsertHashedPassword uid hashedPw + UserStore.upsertHashedPassword uid hashedPw codeDelete key deleteAllCookies uid where @@ -330,7 +330,7 @@ resetPasswordImpl ident code pw = do checkNewIsDifferent :: UserId -> PlainTextPassword' t -> Sem r () checkNewIsDifferent uid newPassword = do - mCurrentPassword <- PasswordStore.lookupHashedPassword uid + mCurrentPassword <- UserStore.lookupHashedPassword uid case mCurrentPassword of Just currentPassword -> whenM (verifyPassword (RateLimitUser uid) newPassword currentPassword) $ @@ -369,25 +369,25 @@ verifyProviderPasswordImpl pid plaintext = do verifyPasswordWithStatus (RateLimitProvider pid) plaintext password verifyUserPasswordImpl :: - ( Member PasswordStore r, - Member (Error AuthenticationSubsystemError) r, + ( Member (Error AuthenticationSubsystemError) r, Member HashPassword r, - Member RateLimit r + Member RateLimit r, + Member UserStore r ) => UserId -> PlainTextPassword6 -> Sem r (Bool, PasswordStatus) verifyUserPasswordImpl uid plaintext = do password <- - PasswordStore.lookupHashedPassword uid + UserStore.lookupHashedPassword uid >>= maybe (throw AuthenticationSubsystemBadCredentials) pure verifyPasswordWithStatus (RateLimitUser uid) plaintext password verifyUserPasswordErrorImpl :: - ( Member PasswordStore r, - Member (Error AuthenticationSubsystemError) r, + ( Member (Error AuthenticationSubsystemError) r, Member HashPassword r, - Member RateLimit r + Member RateLimit r, + Member UserStore r ) => Local UserId -> PlainTextPassword6 -> diff --git a/libs/wire-subsystems/src/Wire/PasswordStore.hs b/libs/wire-subsystems/src/Wire/PasswordStore.hs index 0d01e4e7d43..d7598aeddf8 100644 --- a/libs/wire-subsystems/src/Wire/PasswordStore.hs +++ b/libs/wire-subsystems/src/Wire/PasswordStore.hs @@ -25,8 +25,7 @@ import Polysemy import Wire.API.Password data PasswordStore m a where - UpsertHashedPassword :: UserId -> Password -> PasswordStore m () - LookupHashedPassword :: UserId -> PasswordStore m (Maybe Password) + -- | FUTUREWORK: When we create ProviderStore, we should migrate this action to that. LookupHashedProviderPassword :: ProviderId -> PasswordStore m (Maybe Password) makeSem ''PasswordStore diff --git a/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs index 576ca6cceec..ec36c348b45 100644 --- a/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs @@ -31,8 +31,6 @@ interpretPasswordStore :: (Member (Embed IO) r) => ClientState -> InterpreterFor interpretPasswordStore casClient = interpret $ runEmbedded (runClient casClient) . \case - UpsertHashedPassword uid password -> embed $ updatePasswordImpl uid password - LookupHashedPassword uid -> embed $ lookupPasswordImpl uid LookupHashedProviderPassword pid -> embed $ lookupProviderPasswordImpl pid lookupProviderPasswordImpl :: (MonadClient m) => ProviderId -> m (Maybe Password) @@ -40,24 +38,9 @@ lookupProviderPasswordImpl u = (runIdentity =<<) <$> retry x1 (query1 providerPasswordSelect (params LocalQuorum (Identity u))) -lookupPasswordImpl :: (MonadClient m) => UserId -> m (Maybe Password) -lookupPasswordImpl u = - (runIdentity =<<) - <$> retry x1 (query1 passwordSelect (params LocalQuorum (Identity u))) - -updatePasswordImpl :: (MonadClient m) => UserId -> Password -> m () -updatePasswordImpl u p = do - retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) - ------------------------------------------------------------------------ -- Queries providerPasswordSelect :: PrepQuery R (Identity ProviderId) (Identity (Maybe Password)) providerPasswordSelect = "SELECT password FROM provider WHERE id = ?" - -passwordSelect :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) -passwordSelect = "SELECT password FROM user WHERE id = ?" - -userPasswordUpdate :: PrepQuery W (Password, UserId) () -userPasswordUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET password = ? WHERE id = ?" diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index b8c27f82e40..5c35eeae407 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -105,6 +105,8 @@ data UserStore m a where GetRichInfo :: UserId -> UserStore m (Maybe RichInfoAssocList) LookupRichInfos :: [UserId] -> UserStore m [(UserId, RichInfo)] UpdateRichInfo :: UserId -> RichInfoAssocList -> UserStore m () + UpsertHashedPassword :: UserId -> Password -> UserStore m () + LookupHashedPassword :: UserId -> UserStore m (Maybe Password) GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) SetUserSearchable :: UserId -> SetSearchable -> UserStore m () UpdateFeatureConferenceCalling :: UserId -> Maybe FeatureStatus -> UserStore m () diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 21d54bdc976..0fc3b94fb23 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -71,6 +71,8 @@ interpretUserStoreCassandra casClient = UpdateUserTeam uid tid -> updateUserTeamImpl uid tid GetRichInfo uid -> getRichInfoImpl uid LookupRichInfos uids -> lookupRichInfosImpl uids + UpsertHashedPassword uid pw -> upsertHashedPasswordImpl uid pw + LookupHashedPassword uid -> lookupHashedPasswordImpl uid GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid DeleteEmail uid -> deleteEmailImpl uid SetUserSearchable uid searchable -> setUserSearchableImpl uid searchable @@ -90,6 +92,21 @@ createUserImpl new mbConv = retry x5 . batch $ do for_ mbTid $ \tid -> addPrepQuery insertServiceTeam (pid, sid, BotId new.id, cid, tid) +upsertHashedPasswordImpl :: (MonadClient m) => UserId -> Password -> m () +upsertHashedPasswordImpl u p = do + retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) + where + userPasswordUpdate :: PrepQuery W (Password, UserId) () + userPasswordUpdate = "UPDATE user SET password = ? WHERE id = ?" + +lookupHashedPasswordImpl :: (MonadClient m) => UserId -> m (Maybe Password) +lookupHashedPasswordImpl u = + (runIdentity =<<) + <$> retry x1 (query1 selectPassword (params LocalQuorum (Identity u))) + where + selectPassword :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) + selectPassword = "SELECT password FROM user WHERE id = ?" + getUserAuthenticationInfoImpl :: UserId -> Client (Maybe (Maybe Password, AccountStatus)) getUserAuthenticationInfoImpl uid = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) where diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index 155225bff29..5c368928eea 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -21,6 +21,7 @@ module Wire.AuthenticationSubsystem.InterpreterSpec (spec) where import Data.Domain import Data.Id +import Data.Map qualified as Map import Data.Misc import Data.Qualified import Data.Range (rcast) @@ -39,6 +40,7 @@ import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck import Wire.API.Allowlists (AllowlistEmailDomains (AllowlistEmailDomains)) +import Wire.API.Password import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Password @@ -56,7 +58,6 @@ import Wire.MockInterpreters import Wire.MockInterpreters.Events as Event import Wire.PasswordResetCodeStore import Wire.PasswordStore -import Wire.PasswordStore qualified as PasswordStore import Wire.RateLimit import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) @@ -92,11 +93,11 @@ type AllEffects = State [StoredApp] ] -runAllEffects :: Domain -> [StoredUser] -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a -runAllEffects domain users emailDomains action = snd $ runAllEffectsWithEventState domain users emailDomains action +runAllEffects :: Domain -> [StoredUser] -> Map UserId Password -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a +runAllEffects domain users passwords emailDomains action = snd $ runAllEffectsWithEventState domain users passwords emailDomains action -runAllEffectsWithEventState :: Domain -> [StoredUser] -> Maybe [Text] -> Sem AllEffects a -> ([MiniEvent], Either AuthenticationSubsystemError a) -runAllEffectsWithEventState localDomain preexistingUsers mAllowedEmailDomains = +runAllEffectsWithEventState :: Domain -> [StoredUser] -> Map UserId Password -> Maybe [Text] -> Sem AllEffects a -> ([MiniEvent], Either AuthenticationSubsystemError a) +runAllEffectsWithEventState localDomain preexistingUsers preexistingPasswords mAllowedEmailDomains = let cfg = defaultAuthenticationSubsystemConfig { allowlistEmailDomains = AllowlistEmailDomains <$> mAllowedEmailDomains, @@ -107,7 +108,7 @@ runAllEffectsWithEventState localDomain preexistingUsers mAllowedEmailDomains = . evalState mempty . runState mempty . runInMemoryUserKeyStoreIntepreterWithStoredUsers preexistingUsers - . runInMemoryUserStoreInterpreter preexistingUsers + . runInMemoryUserStoreInterpreter preexistingUsers preexistingPasswords . inMemoryEmailSubsystemInterpreter . discardTinyLogs . evalState mempty @@ -143,9 +144,9 @@ spec = describe "AuthenticationSubsystem.Interpreter" do status = Just Active } uid = user.id + passwords = foldMap (Map.singleton uid . hashPassword) mPreviousPassword eithRes = - runAllEffects testDomain [user] Nothing $ do - forM_ mPreviousPassword (hashPassword >=> PasswordStore.upsertHashedPassword uid) + runAllEffects testDomain [user] passwords Nothing $ do mapM_ (uncurry (insertCookie uid)) cookiesWithTTL createPasswordResetCode (mkEmailKey email) @@ -171,9 +172,9 @@ spec = describe "AuthenticationSubsystem.Interpreter" do status = Just Active } uid = user.id + passwords = foldMap (Map.singleton uid . hashPassword) mPreviousPassword Right (newPasswordVerification, cookiesAfterReset) = - runAllEffects testDomain [user] Nothing $ do - forM_ mPreviousPassword (hashPassword >=> PasswordStore.upsertHashedPassword uid) + runAllEffects testDomain [user] passwords Nothing $ do mapM_ (uncurry (insertCookie uid)) cookiesWithTTL createPasswordResetCode (mkEmailKey email) @@ -188,7 +189,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "reset code is not generated when email is not in allow list" $ \email localDomain -> let createPasswordResetCodeResult = - runAllEffects localDomain [] (Just ["example.com"]) $ + runAllEffects localDomain [] mempty (Just ["example.com"]) $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in domainPart email /= "example.com" ==> @@ -203,7 +204,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do status = Just Active } createPasswordResetCodeResult = - runAllEffects testDomain [user] (Just [decodeUtf8 $ domainPart email]) $ + runAllEffects testDomain [user] mempty (Just [decodeUtf8 $ domainPart email]) $ createPasswordResetCode (mkEmailKey email) in counterexample ("expected Right, got: " <> show createPasswordResetCodeResult) $ isRight createPasswordResetCodeResult @@ -216,7 +217,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do emailUnvalidated = Nothing } createPasswordResetCodeResult = - runAllEffects testDomain [user] Nothing $ + runAllEffects testDomain [user] mempty Nothing $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in (isJust user.status && user.status /= Just Active) ==> @@ -225,7 +226,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "reset code is not generated for when there is no user for the email" $ \email localDomain -> let createPasswordResetCodeResult = - runAllEffects localDomain [] Nothing $ + runAllEffects localDomain [] mempty Nothing $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in createPasswordResetCodeResult === Right () @@ -240,7 +241,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do } uid = user.id Right (newPasswordVerification, mCaughtException) = - runAllEffects testDomain [user] Nothing $ do + runAllEffects testDomain [user] mempty Nothing $ do createPasswordResetCode (mkEmailKey email) (_, resetCode) <- expect1ResetPasswordEmail email @@ -262,9 +263,9 @@ spec = describe "AuthenticationSubsystem.Interpreter" do status = Just Active } uid = user.id + passwords = Map.singleton uid $ hashPassword oldPassword Right (oldPasswordVerification, newPasswordVerification, resetPasswordResult) = - runAllEffects testDomain [user] Nothing $ do - PasswordStore.upsertHashedPassword uid =<< hashPassword oldPassword + runAllEffects testDomain [user] passwords Nothing $ do createPasswordResetCode (mkEmailKey email) (_, resetCode) <- expect1ResetPasswordEmail email @@ -287,9 +288,9 @@ spec = describe "AuthenticationSubsystem.Interpreter" do status = Just Active } uid = user.id + passwords = Map.singleton uid $ hashPassword oldPassword Right (oldPasswordVerification, newPasswordVerification, resetPasswordResult) = - runAllEffects testDomain [user] Nothing $ do - PasswordStore.upsertHashedPassword uid =<< hashPassword oldPassword + runAllEffects testDomain [user] passwords Nothing $ do mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) resetCode newPassword (,,mCaughtExc) <$> verifyUserPassword uid (toInputPassword oldPassword) @@ -307,9 +308,9 @@ spec = describe "AuthenticationSubsystem.Interpreter" do status = Just Active } uid = user.id + passwords = Map.singleton uid $ hashPassword oldPassword Right (oldPasswordVerification, newPasswordVerification, resetPasswordResult) = - runAllEffects testDomain [user] Nothing $ do - hashAndUpsertPassword uid oldPassword + runAllEffects testDomain [user] passwords Nothing $ do mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity wrongEmail) resetCode newPassword (,,mCaughtExc) <$> verifyUserPassword uid (toInputPassword oldPassword) @@ -328,9 +329,9 @@ spec = describe "AuthenticationSubsystem.Interpreter" do status = Just Active } uid = user.id + passwords = Map.singleton uid $ hashPassword oldPassword Right (oldPasswordVerification, newPasswordVerification, correctResetCode, wrongResetErrors, resetPassworedWithCorectCodeResult) = - runAllEffects testDomain [user] Nothing $ do - PasswordStore.upsertHashedPassword uid =<< hashPassword oldPassword + runAllEffects testDomain [user] passwords Nothing $ do createPasswordResetCode (mkEmailKey email) (_, generatedResetCode) <- expect1ResetPasswordEmail email @@ -367,7 +368,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do } uid = user.id Right newPasswordVerification = - runAllEffects testDomain [user] Nothing $ do + runAllEffects testDomain [user] mempty Nothing $ do void $ createPasswordResetCode (mkEmailKey email) mLookupRes <- internalLookupPasswordResetCode (mkEmailKey email) for_ mLookupRes $ \(_, resetCode) -> resetPassword (PasswordResetEmailIdentity email) resetCode newPassword @@ -376,7 +377,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do describe "newCookie" $ do prop "trivial attributes: plain user cookie" $ \localDomain uid cid typ mLabel -> - let Right (plainCookie, lhCookie) = runAllEffects localDomain [] Nothing $ do + let Right (plainCookie, lhCookie) = runAllEffects localDomain [] mempty Nothing $ do plain <- newCookie @_ @ZAuth.U uid cid typ mLabel RevokeSameLabel lh <- newCookie @_ @ZAuth.U uid cid typ mLabel RevokeSameLabel pure (plain, lh) @@ -390,19 +391,19 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "persistent plain cookie expires at configured time" $ \localDomain uid cid mLabel -> - let Right cookie = runAllEffects localDomain [] Nothing $ do + let Right cookie = runAllEffects localDomain [] mempty Nothing $ do newCookie @_ @ZAuth.U uid cid PersistentCookie mLabel RevokeSameLabel in cookie.cookieExpires === addUTCTime (fromIntegral defaultZAuthSettings.userTokenTimeout.userTokenTimeoutSeconds) defaultTime prop "persistent LH cookie expires at configured time" $ \localDomain uid cid mLabel -> - let Right cookie = runAllEffects localDomain [] Nothing $ do + let Right cookie = runAllEffects localDomain [] mempty Nothing $ do newCookie @_ @ZAuth.LU uid cid PersistentCookie mLabel RevokeSameLabel in cookie.cookieExpires === addUTCTime (fromIntegral defaultZAuthSettings.legalHoldUserTokenTimeout.legalHoldUserTokenTimeoutSeconds) defaultTime modifyMaxSuccess (const 3) . prop "cookie is persisted" $ \localDomain uid cid mLabel -> do - let Right (cky, sto) = runAllEffects localDomain [] Nothing $ do + let Right (cky, sto) = runAllEffects localDomain [] mempty Nothing $ do c <- newCookie @_ @ZAuth.LU uid cid PersistentCookie mLabel RevokeSameLabel s <- listCookies uid pure (c, s) @@ -412,7 +413,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "old cookies with same label are revoked on insert" $ \localDomain uid cid typ mLabel otherLabel policy -> let (events, Right (cookie1, cookie2, cookie3, cookies)) = - runAllEffectsWithEventState localDomain [] Nothing $ + runAllEffectsWithEventState localDomain [] mempty Nothing $ (,,,) <$> newCookie @_ @ZAuth.U uid cid typ mLabel policy <*> newCookie @_ @ZAuth.U uid cid typ mLabel policy @@ -448,7 +449,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do \localDomain uidA uidB cid typ lab policy -> uidA /= uidB ==> let (events, Right (cookieA1, cookieB, cookieA2, cookiesA, cookiesB)) = - runAllEffectsWithEventState localDomain [] Nothing $ + runAllEffectsWithEventState localDomain [] mempty Nothing $ (,,,,) <$> newCookie @_ @ZAuth.U uidA cid typ (Just lab) policy <*> newCookie @_ @ZAuth.U uidB cid typ (Just lab) policy @@ -480,10 +481,6 @@ newtype Upto4 = Upto4 Int instance Arbitrary Upto4 where arbitrary = Upto4 <$> elements [0 .. 4] -hashAndUpsertPassword :: (Member PasswordStore r) => UserId -> PlainTextPassword8 -> Sem r () -hashAndUpsertPassword uid password = - upsertHashedPassword uid =<< hashPassword password - expect1ResetPasswordEmail :: (Member (State (Map EmailAddress [SentMail])) r) => EmailAddress -> Sem r PasswordResetPair expect1ResetPasswordEmail email = getEmailsSentTo email diff --git a/libs/wire-subsystems/test/unit/Wire/EnterpriseLoginSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/EnterpriseLoginSubsystem/InterpreterSpec.hs index 88874f171bd..1266cbc6434 100644 --- a/libs/wire-subsystems/test/unit/Wire/EnterpriseLoginSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/EnterpriseLoginSubsystem/InterpreterSpec.hs @@ -68,7 +68,7 @@ runDependencies :: Either EnterpriseLoginSubsystemError a runDependencies = run - . runInMemoryUserSubsystemInterpreter mempty + . runInMemoryUserSubsytemInterpreter mempty mempty . (evalState mempty . inMemoryUserKeyStoreInterpreter . raiseUnder) . fakeRpc . runRandomPure diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index cacc8f024b4..e1c4c204502 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -80,6 +80,7 @@ import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.API import Wire.API.Federation.Component import Wire.API.Federation.Error +import Wire.API.Password import Wire.API.Team.Collaborator import Wire.API.Team.Feature import Wire.API.Team.Member hiding (userId) @@ -325,6 +326,7 @@ type StateEffects = State (Map EmailKey (Maybe UserId, ActivationCode)), State [EmailKey], State [StoredUser], + State (Map UserId Password), State UserGroupInMemState, State [StoredApp], State UserIndex, @@ -347,6 +349,7 @@ stateEffectsInterpreters MiniBackendParams {..} = . liftIndexedUserStoreState . liftAppStoreState . liftUserGroupStoreState + . liftUserPasswordState . liftUserStoreState . liftBlockListStoreState . liftActivationCodeStoreState @@ -415,6 +418,7 @@ data MiniBackend = MkMiniBackend { -- | this is morally the same as the users stored in the actual backend -- invariant: for each key, the user.id and the key are the same users :: [StoredUser], + userPasswords :: Map UserId Password, apps :: [StoredApp], userIndex :: UserIndex, userKeys :: Map EmailKey UserId, @@ -428,12 +432,13 @@ data MiniBackend = MkMiniBackend pushNotifications :: [Push], userGroups :: UserGroupInMemState } - deriving stock (Eq, Show, Generic) + deriving stock (Show, Generic) instance Default MiniBackend where def = MkMiniBackend { users = mempty, + userPasswords = mempty, apps = mempty, userIndex = emptyIndex, userKeys = mempty, @@ -720,6 +725,11 @@ liftUserStoreState = interpret $ \case Polysemy.State.Get -> gets (.users) Put newUsers -> modify $ \b -> (b :: MiniBackend) {users = newUsers} +liftUserPasswordState :: (Member (State MiniBackend) r) => Sem (State (Map UserId Password) : r) a -> Sem r a +liftUserPasswordState = interpret $ \case + Polysemy.State.Get -> gets (.userPasswords) + Put newPasswords -> modify $ \b -> (b :: MiniBackend) {userPasswords = newPasswords} + liftAppStoreState :: (Member (State MiniBackend) r) => Sem (State [StoredApp] : r) a -> Sem r a liftAppStoreState = interpret $ \case Polysemy.State.Get -> gets (.apps) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs index 07dc9906328..6caf8390d18 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs @@ -30,13 +30,13 @@ import Wire.HashPassword.Interpreter staticHashPasswordInterpreter :: InterpreterFor HashPassword r staticHashPasswordInterpreter = interpret $ \case - HashPassword6 password -> hashPassword password - HashPassword8 password -> hashPassword password + HashPassword6 password -> pure $ hashPassword password + HashPassword8 password -> pure $ hashPassword password VerifyPasswordWithStatus plain hashed -> pure $ verifyPasswordWithStatusImpl PasswordHashingScrypt plain hashed -hashPassword :: (Monad m) => PlainTextPassword' t -> m Password +hashPassword :: PlainTextPassword' t -> Password hashPassword password = - pure . Argon2Password $ + Argon2Password $ hashPasswordArgon2idWithSalt fastArgon2IdOptions "9bytesalt" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs index 72ddd6277ff..33952587697 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs @@ -18,7 +18,6 @@ module Wire.MockInterpreters.PasswordStore where import Data.Id -import Data.Map qualified as Map import Imports import Polysemy import Polysemy.State @@ -28,8 +27,6 @@ import Wire.PasswordStore runInMemoryPasswordStoreInterpreter :: InterpreterFor PasswordStore r runInMemoryPasswordStoreInterpreter = evalState (mempty :: Map UserId Password) . inMemoryPasswordStoreInterpreter . raiseUnder -inMemoryPasswordStoreInterpreter :: (Member (State (Map UserId Password)) r) => InterpreterFor PasswordStore r +inMemoryPasswordStoreInterpreter :: InterpreterFor PasswordStore r inMemoryPasswordStoreInterpreter = interpret $ \case - UpsertHashedPassword uid password -> modify $ Map.insert uid password - LookupHashedPassword uid -> gets $ Map.lookup uid LookupHashedProviderPassword _uid -> error ("Implement as needed" :: String) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index e82a6afc355..e393dfdd7c5 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -19,14 +19,17 @@ module Wire.MockInterpreters.UserStore where +import Control.Monad.Trans.Maybe (MaybeT (..)) import Data.Handle import Data.Id +import Data.Map qualified as Map import Data.Time import Data.Time.Calendar.OrdinalDate import Imports import Polysemy import Polysemy.Error import Polysemy.State +import Wire.API.Password import Wire.API.User hiding (DeleteUser) import Wire.API.User qualified as User import Wire.API.User.Search (SetSearchable (SetSearchable)) @@ -34,20 +37,27 @@ import Wire.StoredUser import Wire.UserStore import Wire.UserStore.IndexUser -runInMemoryUserStoreInterpreter :: [StoredUser] -> InterpreterFor UserStore r -runInMemoryUserStoreInterpreter initialUsers = - evalState initialUsers +runInMemoryUserStoreInterpreter :: [StoredUser] -> Map UserId Password -> InterpreterFor UserStore r +runInMemoryUserStoreInterpreter users passwords = + evalState users + . evalState passwords . inMemoryUserStoreInterpreter . raiseUnder + . raiseUnder inMemoryUserStoreInterpreter :: forall r. - (Member (State [StoredUser]) r) => + ( Member (State [StoredUser]) r, + Member (State (Map UserId Password)) r + ) => InterpreterFor UserStore r inMemoryUserStoreInterpreter = interpret $ \case - CreateUser new _ -> modify (newStoredUserToStoredUser new :) - GetUsers uids -> gets $ filter (\user -> user.id `elem` uids) - DoesUserExist uid -> gets (any (\u -> u.id == uid)) + CreateUser new _ -> do + modify (newStoredUserToStoredUser new :) + forM_ new.password $ modify . Map.insert new.id + GetUsers uids -> do + gets $ filter (\user -> user.id `elem` uids) + DoesUserExist uid -> gets @[StoredUser] (any (\u -> u.id == uid)) UpdateUser uid update -> modify (map doUpdate) where doUpdate :: StoredUser -> StoredUser @@ -85,7 +95,7 @@ inMemoryUserStoreInterpreter = interpret $ \case else u UpdateSSOId uid ssoId -> do updateUserInStore uid (\u -> u {ssoId = ssoId}) - gets (any (\u -> u.id == uid)) + gets @[StoredUser] (any (\u -> u.id == uid)) UpdateManagedBy uid managedBy -> updateUserInStore uid (\u -> u {managedBy = Just managedBy}) UpdateAccountStatus uid accountStatus -> updateUserInStore uid (\u -> u {status = Just accountStatus}) ActivateUser uid identity -> updateUserInStore uid (\u -> u {activated = True, email = emailIdentity identity}) @@ -118,7 +128,7 @@ inMemoryUserStoreInterpreter = interpret $ \case us' <- f us put us' DeleteUser user -> modify @[StoredUser] $ filter (\u -> u.id /= User.userId user) - LookupName uid -> (.name) <$$> gets (find $ \u -> u.id == uid) + LookupName uid -> (.name) <$$> gets @[StoredUser] (find $ \u -> u.id == uid) LookupHandle h -> lookupHandleImpl h GlimpseHandle h -> lookupHandleImpl h LookupStatus uid -> lookupStatusImpl uid @@ -131,7 +141,14 @@ inMemoryUserStoreInterpreter = interpret $ \case GetRichInfo _ -> error "GetRichInfo: not implemented" LookupRichInfos _ -> error "LookupRichInfos: not implemented" UpdateRichInfo {} -> error "UpdateRichInfo: Not implemented" - GetUserAuthenticationInfo _uid -> error "Not implemented" + UpsertHashedPassword uid pw -> + modify $ Map.insert uid pw + LookupHashedPassword uid -> + gets $ Map.lookup uid + GetUserAuthenticationInfo uid -> runMaybeT $ do + status <- MaybeT $ lookupStatusImpl uid + pw <- lift $ gets @(Map UserId Password) $ Map.lookup uid + pure (pw, status) DeleteEmail uid -> modify (map doUpdate) where doUpdate :: StoredUser -> StoredUser diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs index bc31cfbb65a..fa2f9c3c8d3 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs @@ -19,11 +19,13 @@ module Wire.MockInterpreters.UserSubsystem where import Control.Monad.Trans.Maybe (MaybeT (..)) import Data.Domain +import Data.Id import Data.LanguageCodes import Data.LegalHold import Data.Qualified import Imports import Polysemy +import Wire.API.Password import Wire.API.User import Wire.MockInterpreters.UserKeyStore import Wire.MockInterpreters.UserStore @@ -34,9 +36,9 @@ import Wire.UserStore (UserStore) import Wire.UserStore qualified as UserStore import Wire.UserSubsystem -runInMemoryUserSubsystemInterpreter :: [StoredUser] -> InterpreterFor UserSubsystem r -runInMemoryUserSubsystemInterpreter initialUsers = - runInMemoryUserStoreInterpreter initialUsers +runInMemoryUserSubsytemInterpreter :: [StoredUser] -> Map UserId Password -> InterpreterFor UserSubsystem r +runInMemoryUserSubsytemInterpreter initialUsers passwords = + runInMemoryUserStoreInterpreter initialUsers passwords . runInMemoryUserKeyStoreIntepreterWithStoredUsers initialUsers . inMemoryUserSubsystemInterpreter . raiseUnder diff --git a/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs index 219a324ba87..dd2cf24f721 100644 --- a/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs @@ -93,7 +93,7 @@ runAllEffects :: RunAllEffectsArgs -> Sem AllEffects a -> Either TeamInvitationS runAllEffects args = run . runInMemoryUserKeyStoreIntepreterWithStoredUsers args.initialUsers - . runInMemoryUserStoreInterpreter args.initialUsers + . runInMemoryUserStoreInterpreter args.initialUsers mempty . inMemoryUserSubsystemInterpreter . evalState mempty . noopEmailSubsystemInterpreter diff --git a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs index 5bedfca85cf..c4288743fee 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs @@ -110,7 +110,7 @@ interpretDependencies initialUsers initialTeams = . runInMemoryUserGroupStore def . miniGalleyAPIAccess initialTeams def . interpretTeamSubsystemToGalleyAPI - . runInMemoryUserSubsystemInterpreter initialUsers + . runInMemoryUserSubsytemInterpreter initialUsers mempty runDependenciesWithReturnState :: [StoredUser] -> @@ -129,7 +129,7 @@ runDependenciesWithReturnState initialUsers initialTeams = . runInMemoryUserGroupStore def . miniGalleyAPIAccess initialTeams def . interpretTeamSubsystemToGalleyAPI - . runInMemoryUserSubsystemInterpreter initialUsers + . runInMemoryUserSubsytemInterpreter initialUsers mempty expectRight :: (Show err) => Either err Property -> Property expectRight = \case diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 09c7dece332..4625ac6191e 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -180,7 +180,6 @@ import Wire.IndexedUserStore (IndexedUserStore) import Wire.InvitationStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) -import Wire.PasswordStore (PasswordStore, lookupHashedPassword) import Wire.PropertySubsystem import Wire.RateLimit import Wire.Sem.Concurrency @@ -386,7 +385,6 @@ servantSitemap :: Member NotificationSubsystem r, Member Now r, Member PasswordResetCodeStore r, - Member PasswordStore r, Member PropertySubsystem r, Member PublicKeyBundle r, Member SFT r, @@ -1171,12 +1169,11 @@ removeEmail = lift . liftSem . User.removeEmailEither >=> reint Left e -> lift . liftSem . throw $ e Right () -> pure Nothing -checkPasswordExists :: (Member PasswordStore r) => UserId -> (Handler r) Bool -checkPasswordExists = fmap isJust . lift . liftSem . lookupHashedPassword +checkPasswordExists :: (Member UserStore r) => UserId -> (Handler r) Bool +checkPasswordExists = fmap isJust . lift . liftSem . UserStore.lookupHashedPassword changePassword :: - ( Member PasswordStore r, - Member UserStore r, + ( Member UserStore r, Member HashPassword r, Member RateLimit r, Member AuthenticationSubsystem r @@ -1438,7 +1435,6 @@ deleteSelfUser :: Member UserKeyStore r, Member NotificationSubsystem r, Member UserStore r, - Member PasswordStore r, Member EmailSubsystem r, Member UserSubsystem r, Member VerificationCodeSubsystem r, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index bc0c14f6200..4e5f2a27a52 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -137,7 +137,6 @@ import Wire.InvitationStore (InvitationStore, StoredInvitation) import Wire.InvitationStore qualified as InvitationStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) -import Wire.PasswordStore (PasswordStore, lookupHashedPassword, upsertHashedPassword) import Wire.PropertySubsystem as PropertySubsystem import Wire.RateLimit import Wire.Sem.Concurrency @@ -884,8 +883,7 @@ mkActivationKey (ActivateEmail e) = -- Password Management changePassword :: - ( Member PasswordStore r, - Member UserStore r, + ( Member UserStore r, Member HashPassword r, Member RateLimit r, Member AuthenticationSubsystem r @@ -897,12 +895,12 @@ changePassword uid cp = do activated <- lift $ liftSem $ UserStore.isActivated uid unless activated $ throwE ChangePasswordNoIdentity - currpw <- lift $ liftSem $ lookupHashedPassword uid + currpw <- lift $ liftSem $ UserStore.lookupHashedPassword uid let newpw = cp.newPassword rateLimitKey = RateLimitUser uid hashedNewPw <- lift . liftSem $ HashPassword.hashPassword8 rateLimitKey newpw case (currpw, cp.oldPassword) of - (Nothing, _) -> lift . liftSem $ upsertHashedPassword uid hashedNewPw + (Nothing, _) -> lift . liftSem $ UserStore.upsertHashedPassword uid hashedNewPw (Just _, Nothing) -> throwE InvalidCurrentPassword (Just pw, Just pw') -> do -- We are updating the pwd here anyway, so we don't care about the pwd status @@ -910,7 +908,7 @@ changePassword uid cp = do throwE InvalidCurrentPassword whenM (lift . liftSem $ HashPassword.verifyPassword rateLimitKey newpw pw) $ throwE ChangePasswordMustDiffer - lift $ liftSem (upsertHashedPassword uid hashedNewPw >> Auth.revokeAllCookies uid) + lift $ liftSem (UserStore.upsertHashedPassword uid hashedNewPw >> Auth.revokeAllCookies uid) ------------------------------------------------------------------------------- -- User Deletion @@ -933,7 +931,6 @@ deleteSelfUser :: Member (Embed HttpClientIO) r, Member UserKeyStore r, Member NotificationSubsystem r, - Member PasswordStore r, Member UserStore r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, @@ -977,7 +974,7 @@ deleteSelfUser luid@(tUnqualified -> uid) pwd = do lift . liftSem . Log.info $ field "user" (toByteString uid) . msg (val "Attempting account deletion with a password") - actual <- lift $ liftSem $ lookupHashedPassword uid + actual <- lift $ liftSem $ UserStore.lookupHashedPassword uid case actual of Nothing -> throwE DeleteUserInvalidPassword Just p -> do