diff --git a/changelog.d/5-internal/servantification b/changelog.d/5-internal/servantification new file mode 100644 index 0000000000..90e81600ed --- /dev/null +++ b/changelog.d/5-internal/servantification @@ -0,0 +1 @@ +Servantify /self/* endpoints in brig. \ No newline at end of file diff --git a/libs/wire-api/package.yaml b/libs/wire-api/package.yaml index c616fb4d1c..ffb719e83a 100644 --- a/libs/wire-api/package.yaml +++ b/libs/wire-api/package.yaml @@ -41,6 +41,7 @@ library: - currency-codes >=2.0 - deriving-aeson >=0.2 - deriving-swagger2 + - either - email-validate >=2.0 - errors - extended diff --git a/libs/wire-api/src/Wire/API/ErrorDescription.hs b/libs/wire-api/src/Wire/API/ErrorDescription.hs index 4cd0b9be10..444e131a02 100644 --- a/libs/wire-api/src/Wire/API/ErrorDescription.hs +++ b/libs/wire-api/src/Wire/API/ErrorDescription.hs @@ -334,3 +334,23 @@ type AssetTooLarge = ErrorDescription 413 "client-error" "Asset too large" type InvalidLength = ErrorDescription 400 "invalid-length" "Invalid content length" type AssetNotFound = ErrorDescription 404 "not-found" "Asset not found" + +type NameManagedByScim = ErrorDescription 403 "managed-by-scim" "Updating name is not allowed, because it is managed by SCIM" + +type HandleManagedByScim = ErrorDescription 403 "managed-by-scim" "Updating handle is not allowed, because it is managed by SCIM" + +type InvalidPhone = ErrorDescription 400 "invalid-phone" "Invalid mobile phone number" + +type UserKeyExists = ErrorDescription 409 "key-exists" "The give e-mail address or phone number is in use." + +type BlacklistedPhone = ErrorDescription 403 "blacklisted-phone" "The given phone number has been blacklisted due to suspected abuse or a complaint." + +type LastIdentity = ErrorDescription 403 "last-identity" "The last user identity (email or phone number) cannot be removed." + +type NoPassword = ErrorDescription 403 "no-password" "The user has no password." + +type ChangePasswordMustDiffer = ErrorDescription 409 "password-must-differ" "For password change, new and old password must be different." + +type HandleExists = ErrorDescription 409 "handle-exists" "The given handle is already taken." + +type InvalidHandle = ErrorDescription 400 "invalid-handle" "The given handle is invalid." diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index 590c583e32..ff0498d6d5 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -31,6 +31,8 @@ module Wire.API.Routes.MultiVerb AsUnion (..), eitherToUnion, eitherFromUnion, + maybeToUnion, + maybeFromUnion, AsConstructor (..), GenericAsConstructor (..), GenericAsUnion (..), @@ -50,6 +52,7 @@ import Data.ByteString.Builder import qualified Data.ByteString.Lazy as LBS import qualified Data.CaseInsensitive as CI import Data.Containers.ListUtils +import Data.Either.Combinators (leftToMaybe) import Data.HashMap.Strict.InsOrd (InsOrdHashMap) import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap import Data.Metrics.Servant @@ -529,6 +532,21 @@ instance EitherFromUnion as bs => EitherFromUnion (a ': as) bs where eitherFromUnion f _ (Z x) = Left (f (Z x)) eitherFromUnion f g (S x) = eitherFromUnion @as @bs (f . S) g x +maybeToUnion :: + forall as a. + (InjectAfter as '[()], InjectBefore as '[()]) => + (a -> Union as) -> + (Maybe a -> Union (as .++ '[()])) +maybeToUnion f (Just a) = injectBefore @as @'[()] (f a) +maybeToUnion _ Nothing = injectAfter @as @'[()] (Z (I ())) + +maybeFromUnion :: + forall as a. + EitherFromUnion as '[()] => + (Union as -> a) -> + (Union (as .++ '[()]) -> Maybe a) +maybeFromUnion f = leftToMaybe . eitherFromUnion @as @'[()] f (const (Z (I ()))) + -- | This class can be instantiated to get automatic derivation of 'AsUnion' -- instances via 'GenericAsUnion'. The idea is that one has to make sure that for -- each response @r@ in a 'MultiVerb' endpoint, there is an instance of @@ -607,6 +625,23 @@ instance toUnion (GenericAsUnion x) = fromSOP @xss @rs (GSOP.from x) fromUnion = GenericAsUnion . GSOP.to . toSOP @xss @rs +-- | A handler for a pair of empty responses can be implemented simply by +-- returning a boolean value. The convention is that the "failure" case, normally +-- represented by 'False', corresponds to the /first/ response. +instance + AsUnion + '[ RespondEmpty s1 desc1, + RespondEmpty s2 desc2 + ] + Bool + where + toUnion False = Z (I ()) + toUnion True = S (Z (I ())) + + fromUnion (Z (I ())) = False + fromUnion (S (Z (I ()))) = True + fromUnion (S (S x)) = case x of + -- | A handler for a pair of responses where the first is empty can be -- implemented simply by returning a 'Maybe' value. The convention is that the -- "failure" case, normally represented by 'Nothing', corresponds to the /first/ diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index c800e77007..02adeba83d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -135,6 +135,90 @@ data Api routes = Api :> "self" :> ReqBody '[JSON] DeleteUser :> MultiVerb 'DELETE '[JSON] DeleteSelfResponses (Maybe Timeout), + -- This endpoint can lead to the following events being sent: + -- - UserUpdated event to contacts of self + putSelf :: + routes + :- Summary "Update your profile." + :> ZUser + :> ZConn + :> "self" + :> ReqBody '[JSON] UserUpdate + :> MultiVerb 'PUT '[JSON] PutSelfResponses (Maybe UpdateProfileError), + changePhone :: + routes + :- Summary "Change your phone number." + :> ZUser + :> ZConn + :> "self" + :> "phone" + :> ReqBody '[JSON] PhoneUpdate + :> MultiVerb 'PUT '[JSON] ChangePhoneResponses (Maybe ChangePhoneError), + -- This endpoint can lead to the following events being sent: + -- - UserIdentityRemoved event to self + removePhone :: + routes + :- Summary "Remove your phone number." + :> Description + "Your phone number can only be removed if you also have an \ + \email address and a password." + :> ZUser + :> ZConn + :> "self" + :> "phone" + :> MultiVerb 'DELETE '[JSON] RemoveIdentityResponses (Maybe RemoveIdentityError), + -- This endpoint can lead to the following events being sent: + -- - UserIdentityRemoved event to self + removeEmail :: + routes + :- Summary "Remove your email address." + :> Description + "Your email address can only be removed if you also have a \ + \phone number." + :> ZUser + :> ZConn + :> "self" + :> "email" + :> MultiVerb 'DELETE '[JSON] RemoveIdentityResponses (Maybe RemoveIdentityError), + checkPasswordExists :: + routes + :- Summary "Check that your password is set." + :> ZUser + :> "self" + :> "password" + :> MultiVerb + 'HEAD + '() + '[ RespondEmpty 404 "Password is not set", + RespondEmpty 200 "Password is set" + ] + Bool, + changePassword :: + routes + :- Summary "Change your password." + :> ZUser + :> "self" + :> "password" + :> ReqBody '[JSON] PasswordChange + :> MultiVerb 'PUT '[JSON] ChangePasswordResponses (Maybe ChangePasswordError), + changeLocale :: + routes + :- Summary "Change your locale." + :> ZUser + :> ZConn + :> "self" + :> "locale" + :> ReqBody '[JSON] LocaleUpdate + :> MultiVerb 'PUT '[JSON] '[RespondEmpty 200 "Local Changed"] (), + changeHandle :: + routes + :- Summary "Change your handle." + :> ZUser + :> ZConn + :> "self" + :> "handle" + :> ReqBody '[JSON] HandleUpdate + :> MultiVerb 'PUT '[JSON] ChangeHandleResponses (Maybe ChangeHandleError), updateUserEmail :: routes :- Summary "Resend email address validation email." diff --git a/libs/wire-api/src/Wire/API/Swagger.hs b/libs/wire-api/src/Wire/API/Swagger.hs index 19cdb227c6..1aa893026b 100644 --- a/libs/wire-api/src/Wire/API/Swagger.hs +++ b/libs/wire-api/src/Wire/API/Swagger.hs @@ -142,12 +142,7 @@ models = User.modelUserIdList, User.modelUser, User.modelNewUser, - User.modelUserUpdate, - User.modelChangePassword, - User.modelChangeLocale, User.modelEmailUpdate, - User.modelPhoneUpdate, - User.modelChangeHandle, User.modelDelete, User.modelVerifyDelete, User.Activation.modelActivate, diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 2ef7f9f715..5d11283717 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -56,11 +56,21 @@ module Wire.API.User -- * Profile Updates UserUpdate (..), + UpdateProfileError (..), + PutSelfResponses, PasswordChange (..), + ChangePasswordError (..), + ChangePasswordResponses, LocaleUpdate (..), EmailUpdate (..), PhoneUpdate (..), + ChangePhoneError (..), + ChangePhoneResponses, + RemoveIdentityError (..), + RemoveIdentityResponses, HandleUpdate (..), + ChangeHandleError (..), + ChangeHandleResponses, NameUpdate (..), -- * Account Deletion @@ -81,16 +91,11 @@ module Wire.API.User module Wire.API.User.Profile, -- * Swagger - modelChangeHandle, - modelChangeLocale, - modelChangePassword, modelDelete, modelEmailUpdate, modelNewUser, - modelPhoneUpdate, modelUser, modelUserIdList, - modelUserUpdate, modelVerifyDelete, ) where @@ -113,9 +118,9 @@ import Data.Json.Util (UTCTimeMillis, (#)) import Data.LegalHold (UserLegalHoldStatus) import qualified Data.List as List import Data.Misc (PlainTextPassword (..)) -import Data.Proxy (Proxy (..)) import Data.Qualified import Data.Range +import Data.SOP import Data.Schema import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc @@ -124,11 +129,15 @@ import Data.UUID (UUID, nil) import qualified Data.UUID as UUID import Deriving.Swagger import GHC.TypeLits (KnownNat, Nat) +import qualified Generics.SOP as GSOP import Imports import qualified SAML2.WebSSO as SAML +import Servant (type (.++)) import qualified Test.QuickCheck as QC import Wire.API.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) +import Wire.API.ErrorDescription import Wire.API.Provider.Service (ServiceRef, modelServiceRef) +import Wire.API.Routes.MultiVerb import Wire.API.Team (BindingNewTeam (BindingNewTeam), NewTeam (..), modelNewBindingTeam) import Wire.API.User.Activation (ActivationCode) import Wire.API.User.Auth (CookieLabel) @@ -830,37 +839,33 @@ data UserUpdate = UserUpdate uupAccentId :: Maybe ColourId } deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema UserUpdate) deriving (Arbitrary) via (GenericUniform UserUpdate) -modelUserUpdate :: Doc.Model -modelUserUpdate = Doc.defineModel "UserUpdate" $ do - Doc.description "User Update Data" - Doc.property "name" Doc.string' $ do - Doc.description "Name (1 - 128 characters)" - Doc.optional - Doc.property "assets" (Doc.array (Doc.ref modelAsset)) $ do - Doc.description "Profile assets" - Doc.optional - Doc.property "accent_id" Doc.int32' $ do - Doc.description "Accent colour ID" - Doc.optional +instance ToSchema UserUpdate where + schema = + object "UserUpdate" $ + UserUpdate + <$> uupName .= maybe_ (optField "name" schema) + <*> uupPict .= maybe_ (optField "picture" schema) + <*> uupAssets .= maybe_ (optField "assets" (array schema)) + <*> uupAccentId .= maybe_ (optField "accent_id" schema) -instance ToJSON UserUpdate where - toJSON u = - A.object $ - "name" A..= uupName u - # "picture" A..= uupPict u - # "assets" A..= uupAssets u - # "accent_id" A..= uupAccentId u - # [] +data UpdateProfileError + = DisplayNameManagedByScim + | ProfileNotFound + deriving (Generic) + deriving (AsUnion PutSelfErrorResponses) via GenericAsUnion PutSelfErrorResponses UpdateProfileError + +instance GSOP.Generic UpdateProfileError + +type PutSelfErrorResponses = '[NameManagedByScim, UserNotFound] + +type PutSelfResponses = PutSelfErrorResponses .++ '[RespondEmpty 200 "User updated"] -instance FromJSON UserUpdate where - parseJSON = A.withObject "UserUpdate" $ \o -> - UserUpdate - <$> o A..:? "name" - <*> o A..:? "picture" - <*> o A..:? "assets" - <*> o A..:? "accent_id" +instance (res ~ PutSelfResponses) => AsUnion res (Maybe UpdateProfileError) where + toUnion = maybeToUnion (toUnion @PutSelfErrorResponses) + fromUnion = maybeFromUnion (fromUnion @PutSelfErrorResponses) -- | The payload for setting or changing a password. data PasswordChange = PasswordChange @@ -869,47 +874,49 @@ data PasswordChange = PasswordChange } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform PasswordChange) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema PasswordChange) -modelChangePassword :: Doc.Model -modelChangePassword = Doc.defineModel "ChangePassword" $ do - Doc.description - "Data to change a password. The old password is required if \ - \a password already exists." - Doc.property "old_password" Doc.string' $ do - Doc.description "Old password" - Doc.optional - Doc.property "new_password" Doc.string' $ - Doc.description "New password (6 - 1024 characters)" - -instance ToJSON PasswordChange where - toJSON (PasswordChange old new) = - A.object - [ "old_password" A..= old, - "new_password" A..= new - ] - -instance FromJSON PasswordChange where - parseJSON = A.withObject "PasswordChange" $ \o -> - PasswordChange - <$> o A..:? "old_password" - <*> o A..: "new_password" +instance ToSchema PasswordChange where + schema = + over + doc + ( description + ?~ "Data to change a password. The old password is required if \ + \a password already exists." + ) + . object "PasswordChange" + $ PasswordChange + <$> cpOldPassword .= maybe_ (optField "old_password" schema) + <*> cpNewPassword .= field "new_password" schema + +data ChangePasswordError + = InvalidCurrentPassword + | ChangePasswordNoIdentity + | ChangePasswordMustDiffer + deriving (Generic) + deriving (AsUnion ChangePasswordErrorResponses) via GenericAsUnion ChangePasswordErrorResponses ChangePasswordError + +instance GSOP.Generic ChangePasswordError + +type ChangePasswordErrorResponses = [BadCredentials, NoIdentity, ChangePasswordMustDiffer] + +type ChangePasswordResponses = + ChangePasswordErrorResponses .++ '[RespondEmpty 200 "Password Changed"] + +instance (res ~ ChangePasswordResponses) => AsUnion res (Maybe ChangePasswordError) where + toUnion = maybeToUnion (toUnion @ChangePasswordErrorResponses) + fromUnion = maybeFromUnion (fromUnion @ChangePasswordErrorResponses) newtype LocaleUpdate = LocaleUpdate {luLocale :: Locale} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema LocaleUpdate) -modelChangeLocale :: Doc.Model -modelChangeLocale = Doc.defineModel "ChangeLocale" $ do - Doc.description "Data to change a locale." - Doc.property "locale" Doc.string' $ - Doc.description "Locale to be set" - -instance ToJSON LocaleUpdate where - toJSON l = A.object ["locale" A..= luLocale l] - -instance FromJSON LocaleUpdate where - parseJSON = A.withObject "locale-update" $ \o -> - LocaleUpdate <$> o A..: "locale" +instance ToSchema LocaleUpdate where + schema = + object "LocaleUpdate" $ + LocaleUpdate + <$> luLocale .= field "locale" schema newtype EmailUpdate = EmailUpdate {euEmail :: Email} deriving stock (Eq, Show, Generic) @@ -938,36 +945,78 @@ instance FromJSON EmailUpdate where newtype PhoneUpdate = PhoneUpdate {puPhone :: Phone} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema PhoneUpdate -modelPhoneUpdate :: Doc.Model -modelPhoneUpdate = Doc.defineModel "PhoneUpdate" $ do - Doc.description "Phone Update Data" - Doc.property "phone" Doc.string' $ - Doc.description "E.164 phone number" +instance ToSchema PhoneUpdate where + schema = + object "PhoneUpdate" $ + PhoneUpdate + <$> puPhone .= field "phone" schema + +data ChangePhoneError + = PhoneExists + | InvalidNewPhone + | BlacklistedNewPhone + deriving (Generic) + deriving (AsUnion ChangePhoneErrorResponses) via GenericAsUnion ChangePhoneErrorResponses ChangePhoneError + +instance GSOP.Generic ChangePhoneError + +type ChangePhoneErrorResponses = [UserKeyExists, InvalidPhone, BlacklistedPhone] -instance ToJSON PhoneUpdate where - toJSON p = A.object ["phone" A..= puPhone p] +type ChangePhoneResponses = + ChangePhoneErrorResponses .++ '[RespondEmpty 202 "Phone updated"] -instance FromJSON PhoneUpdate where - parseJSON = A.withObject "phone-update" $ \o -> - PhoneUpdate <$> o A..: "phone" +instance (res ~ ChangePhoneResponses) => AsUnion res (Maybe ChangePhoneError) where + toUnion = maybeToUnion (toUnion @ChangePhoneErrorResponses) + fromUnion = maybeFromUnion (fromUnion @ChangePhoneErrorResponses) + +data RemoveIdentityError + = LastIdentity + | NoPassword + | NoIdentity + deriving (Generic) + deriving (AsUnion RemoveIdentityErrorResponses) via GenericAsUnion RemoveIdentityErrorResponses RemoveIdentityError + +instance GSOP.Generic RemoveIdentityError + +type RemoveIdentityErrorResponses = [LastIdentity, NoPassword, NoIdentity] + +type RemoveIdentityResponses = + RemoveIdentityErrorResponses .++ '[RespondEmpty 200 "Identity Removed"] + +instance (res ~ RemoveIdentityResponses) => AsUnion res (Maybe RemoveIdentityError) where + toUnion = maybeToUnion (toUnion @RemoveIdentityErrorResponses) + fromUnion = maybeFromUnion (fromUnion @RemoveIdentityErrorResponses) newtype HandleUpdate = HandleUpdate {huHandle :: Text} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema HandleUpdate) + +instance ToSchema HandleUpdate where + schema = + object "HandleUpdate" $ + HandleUpdate <$> huHandle .= field "handle" schema + +data ChangeHandleError + = ChangeHandleNoIdentity + | ChangeHandleExists + | ChangeHandleInvalid + | ChangeHandleManagedByScim + deriving (Generic) + deriving (AsUnion ChangeHandleErrorResponses) via GenericAsUnion ChangeHandleErrorResponses ChangeHandleError + +instance GSOP.Generic ChangeHandleError -modelChangeHandle :: Doc.Model -modelChangeHandle = Doc.defineModel "ChangeHandle" $ do - Doc.description "Change the handle." - Doc.property "handle" Doc.string' $ - Doc.description "Handle to set" +type ChangeHandleErrorResponses = [NoIdentity, HandleExists, InvalidHandle, HandleManagedByScim] -instance ToJSON HandleUpdate where - toJSON h = A.object ["handle" A..= huHandle h] +type ChangeHandleResponses = + ChangeHandleErrorResponses .++ '[RespondEmpty 200 "Handle Changed"] -instance FromJSON HandleUpdate where - parseJSON = A.withObject "handle-update" $ \o -> - HandleUpdate <$> o A..: "handle" +instance (res ~ ChangeHandleResponses) => AsUnion res (Maybe ChangeHandleError) where + toUnion = maybeToUnion (toUnion @ChangeHandleErrorResponses) + fromUnion = maybeFromUnion (fromUnion @ChangeHandleErrorResponses) newtype NameUpdate = NameUpdate {nuHandle :: Text} deriving stock (Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index d9b819d9a3..07ca0f4bc9 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -50,7 +50,7 @@ module Wire.API.User.Identity where import Control.Applicative (optional) -import Control.Lens ((.~), (?~), (^.)) +import Control.Lens (over, (.~), (?~), (^.)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A import qualified Data.Aeson.Types as A @@ -242,13 +242,12 @@ validateEmail = newtype Phone = Phone {fromPhone :: Text} deriving stock (Eq, Ord, Show, Generic) - deriving newtype (ToJSON, S.ToSchema) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema Phone) -instance FromJSON Phone where - parseJSON (A.String s) = case parsePhone s of - Just p -> return p - Nothing -> fail "Invalid phone number. Expected E.164 format." - parseJSON _ = mempty +instance ToSchema Phone where + schema = + over doc (S.description ?~ "E.164 phone number") $ + fromPhone .= parsedText "PhoneNumber" (maybe (Left "Invalid phone number. Expected E.164 format.") Right . parsePhone) instance ToByteString Phone where builder = builder . fromPhone diff --git a/libs/wire-api/test/golden/testObject_PasswordChange_user_1.json b/libs/wire-api/test/golden/testObject_PasswordChange_user_1.json index efc8fa9efe..56d9750fae 100644 --- a/libs/wire-api/test/golden/testObject_PasswordChange_user_1.json +++ b/libs/wire-api/test/golden/testObject_PasswordChange_user_1.json @@ -1,4 +1,3 @@ { - "new_password": "\u0001f;0B+CKY󾃹W\u0005𫧻𐑹􇟚󽀰f\u0001&𨫳􀟘󹪊􊤍𣧚\tX𮥥\u001chl󹏂^A𥠮=𤻆\u0019_ow?\\h4𥼫𐬤\u0016⹋4O\u0015nJT\u0004r\u0018~\u0006\u0017R'󲏴sX󴤦mlCHvg􏥩ba󵂐R\\󼝬o󱢀􁹎*k\r)H𬂳􀧘\n#㠞~\\9Q|\u000b;\u001fd.􄙔0SHP%󸹆囇'!󾺴N\u001a!Kz@\u0000𒅟􉤛\u001aVp􌥏鞴\u00023#\u0014}}􋉝N𝙺`𩖂7󼽅\u0010𥱥^\u0002{`i:\rUT!\u0013􏚔𥏟\u0015WK\u0000􌋍􍅦eA𢚊\u0003𩿡󼣩t@?󷭺\u0001J􆔶7\u001eg{𓆲5R_\u0013u\u000f𥝛􈑉`}𐔔X\u0011𪱠D懷b𫋄6T𢨐𨳔p*7\n\\'\u000bO#\u001c𪫫(H\u0015n𫪢󷾡}2s𣀩8GA&󵏡\u0018􄱤d⍠\u001a􂤠t @􂀰I/𪻢痰\u00135烙c\u0004󿜉塂Uk\u0016\u0010􌕟8\u001d󼞚𗁬R-x󴇝󶁑󶏺\u0010O,Z\u0003􆌧f*\u000c^>\u0004D\r\u000f_AQPO33𗣃/F\u001e𭍙y𓀞|Fn󶬼E<󿩫9\u0003[y`e𩍈コL\u001a@4i/*󷂯􍋍⍮Ih\u000fC1󻴈\t%?kFt\u0006\u0010\u001f\u001dN𩰟\u000c􋆋:\u0007V\u0003j䙞\u0016\u0016𤨷\u0019K􈤚𧥃鸶Uez)􇹨\u001c)8vT;\u001d呭ay\u0003\u000f\u001d{C=\u0019,\u0001\u0003O𧰫\u0003&\u00012%<2s\u000c\u0016\r6ivo{󺿷WN􁓱R󽸖󻟱󳆅𘉋[ :\u001fu𬆺^f􉤮\u0018𡪧𬰥N\u000f𣝶\u0019@pK􇖌9\r@Ze𥐣f\u0012xM痽;j\u0016珥K_~:v&Dpx~_\u0002:b;bv\u0013=㧜6\u001aꄚ\u001by0Ho.B\"u*{󸪴Vw\u001aW𡰗𪞫rbY쬎I{q󾏞\u0005&_Pt𬪎'󠀷5\u000b𤵫谺觻Ue@YM𨌙)\n\u0019\u001dn\u0004Z\u000e\\\u000c`f3T+_\u001e@\u0007\u001e𭤦}", - "old_password": null + "new_password": "\u0001f;0B+CKY󾃹W\u0005𫧻𐑹􇟚󽀰f\u0001&𨫳􀟘󹪊􊤍𣧚\tX𮥥\u001chl󹏂^A𥠮=𤻆\u0019_ow?\\h4𥼫𐬤\u0016⹋4O\u0015nJT\u0004r\u0018~\u0006\u0017R'󲏴sX󴤦mlCHvg􏥩ba󵂐R\\󼝬o󱢀􁹎*k\r)H𬂳􀧘\n#㠞~\\9Q|\u000b;\u001fd.􄙔0SHP%󸹆囇'!󾺴N\u001a!Kz@\u0000𒅟􉤛\u001aVp􌥏鞴\u00023#\u0014}}􋉝N𝙺`𩖂7󼽅\u0010𥱥^\u0002{`i:\rUT!\u0013􏚔𥏟\u0015WK\u0000􌋍􍅦eA𢚊\u0003𩿡󼣩t@?󷭺\u0001J􆔶7\u001eg{𓆲5R_\u0013u\u000f𥝛􈑉`}𐔔X\u0011𪱠D懷b𫋄6T𢨐𨳔p*7\n\\'\u000bO#\u001c𪫫(H\u0015n𫪢󷾡}2s𣀩8GA&󵏡\u0018􄱤d⍠\u001a􂤠t @􂀰I/𪻢痰\u00135烙c\u0004󿜉塂Uk\u0016\u0010􌕟8\u001d󼞚𗁬R-x󴇝󶁑󶏺\u0010O,Z\u0003􆌧f*\u000c^>\u0004D\r\u000f_AQPO33𗣃/F\u001e𭍙y𓀞|Fn󶬼E<󿩫9\u0003[y`e𩍈コL\u001a@4i/*󷂯􍋍⍮Ih\u000fC1󻴈\t%?kFt\u0006\u0010\u001f\u001dN𩰟\u000c􋆋:\u0007V\u0003j䙞\u0016\u0016𤨷\u0019K􈤚𧥃鸶Uez)􇹨\u001c)8vT;\u001d呭ay\u0003\u000f\u001d{C=\u0019,\u0001\u0003O𧰫\u0003&\u00012%<2s\u000c\u0016\r6ivo{󺿷WN􁓱R󽸖󻟱󳆅𘉋[ :\u001fu𬆺^f􉤮\u0018𡪧𬰥N\u000f𣝶\u0019@pK􇖌9\r@Ze𥐣f\u0012xM痽;j\u0016珥K_~:v&Dpx~_\u0002:b;bv\u0013=㧜6\u001aꄚ\u001by0Ho.B\"u*{󸪴Vw\u001aW𡰗𪞫rbY쬎I{q󾏞\u0005&_Pt𬪎'󠀷5\u000b𤵫谺觻Ue@YM𨌙)\n\u0019\u001dn\u0004Z\u000e\\\u000c`f3T+_\u001e@\u0007\u001e𭤦}" } diff --git a/libs/wire-api/test/golden/testObject_PasswordChange_user_13.json b/libs/wire-api/test/golden/testObject_PasswordChange_user_13.json index 7ce0b18e5f..03878d7165 100644 --- a/libs/wire-api/test/golden/testObject_PasswordChange_user_13.json +++ b/libs/wire-api/test/golden/testObject_PasswordChange_user_13.json @@ -1,4 +1,3 @@ { - "new_password": "\tY6b􅟘\u0018J0\u001e\u0013\u000cg]Ⅼ^\u0012󻏡S<\u0003n\u0010𧔞\u0000󶜅\"\u0000𠾿\u000fU󻄡K;la\u000e𠜻`쎩$0#䷉\u000c~\u001e􀙓kyBx\u0007\u0005<bY%􎆪{𣖓\u0005󵪒\u001a\n􏛶=􈊯\nl󺱶쌴\t:Z\\덧7,\u0017\\󾱀󽼁m^\u0005+\u0003epC\u0018VO2:!𦂲\n\u00186g}􌪂q6􁵋6<𩺐\u0001%yP𪳿\u0002\u0006o𫵞g􅨘p󵮋T󸬔\u0005\u000e0'F#^IVd\n𦗔𢞫A,@􁊙C-𬒓g``𨤳\u0000ṹ􀎏𡱼5}q쑜\u0016􉿉𠴦u(P芷\u000eP뤘e\n\u0011𨃁6􈤲\u0016M𩂹7󺻩\u00149󽭑O9 ,Op󰂺UR\u0013\u0000뮽􀔭>(󺙦\u0019_Nn8\u001cb𖩅r[p􏹻za1\u0012w\u0011慧\u001a𩛅\u001c_\u0018\u0004N6Vyi&)󷋢^𡈴\u0004\u0018ꂦBuJ\u0012`𓈢)qq^@$*\u0016𝅘𝅥𝅮P\u000bể\u0006g𠸹Mg}\u001b\u0019󲤜󼾦w󰍴-e窓p&\u000ed󹭘𒄔a㮰󽼋􁸞\u001e𢾀􁵈'E𬌖𗽈9\u0014\u0014A$𬆴h/A`\u0011l]3Qv㧗MR3W\u001csn] a\u0000:3`𗐴{`罕\n\u001f\u0012.𪂺:?y`\u0014􈼒_%S𥻲:\u0000𩷛\u0019k\"\u000cWYu8-jr)¸?D〴c􎘍􋲹􉽙^x\u0001{b3Sl:&0xgT321𬄏FU􄵹N􏽊P*L𣣿ﱔi:뻜\u0016𨏇0#\u0006뺗0v􀐍n\u000e𦴧P:", - "old_password": null + "new_password": "\tY6b􅟘\u0018J0\u001e\u0013\u000cg]Ⅼ^\u0012󻏡S<\u0003n\u0010𧔞\u0000󶜅\"\u0000𠾿\u000fU󻄡K;la\u000e𠜻`쎩$0#䷉\u000c~\u001e􀙓kyBx\u0007\u0005<bY%􎆪{𣖓\u0005󵪒\u001a\n􏛶=􈊯\nl󺱶쌴\t:Z\\덧7,\u0017\\󾱀󽼁m^\u0005+\u0003epC\u0018VO2:!𦂲\n\u00186g}􌪂q6􁵋6<𩺐\u0001%yP𪳿\u0002\u0006o𫵞g􅨘p󵮋T󸬔\u0005\u000e0'F#^IVd\n𦗔𢞫A,@􁊙C-𬒓g``𨤳\u0000ṹ􀎏𡱼5}q쑜\u0016􉿉𠴦u(P芷\u000eP뤘e\n\u0011𨃁6􈤲\u0016M𩂹7󺻩\u00149󽭑O9 ,Op󰂺UR\u0013\u0000뮽􀔭>(󺙦\u0019_Nn8\u001cb𖩅r[p􏹻za1\u0012w\u0011慧\u001a𩛅\u001c_\u0018\u0004N6Vyi&)󷋢^𡈴\u0004\u0018ꂦBuJ\u0012`𓈢)qq^@$*\u0016𝅘𝅥𝅮P\u000bể\u0006g𠸹Mg}\u001b\u0019󲤜󼾦w󰍴-e窓p&\u000ed󹭘𒄔a㮰󽼋􁸞\u001e𢾀􁵈'E𬌖𗽈9\u0014\u0014A$𬆴h/A`\u0011l]3Qv㧗MR3W\u001csn] a\u0000:3`𗐴{`罕\n\u001f\u0012.𪂺:?y`\u0014􈼒_%S𥻲:\u0000𩷛\u0019k\"\u000cWYu8-jr)¸?D〴c􎘍􋲹􉽙^x\u0001{b3Sl:&0xgT321𬄏FU􄵹N􏽊P*L𣣿ﱔi:뻜\u0016𨏇0#\u0006뺗0v􀐍n\u000e𦴧P:" } diff --git a/libs/wire-api/test/golden/testObject_PasswordChange_user_14.json b/libs/wire-api/test/golden/testObject_PasswordChange_user_14.json index 9ecfcc2825..1685f45fe1 100644 --- a/libs/wire-api/test/golden/testObject_PasswordChange_user_14.json +++ b/libs/wire-api/test/golden/testObject_PasswordChange_user_14.json @@ -1,4 +1,3 @@ { - "new_password": "6go<􂵨fꃽY􈳇*KE󽤥])9󻆯􊕞K।􄉎1𘗢>qb2`\u00157*al\u000b3I*𦒁\u000e􊉪\u000c}\u0010𢁞~🩏@jjd\u0001O\u001a\u0001𗌗lv䃂B\u00083􄔙?Ih1bv\u000b\u001az\u0001\u001c😈0.\u0005b윮\u001cR<􏒫\u0004\u0011l\"E𞡊X6\u0015S󷂒?\u001b\u0004EKÒh팥䄰\u000c6󠁸>\u000b_Zd n<\u0012k􉖈s􅌻~\u0001󶹅\u0018r\u0010G󳶒\u0000`.)lj\u0010Rr/𤞸\\𫈘󴆴𭵝𗨦kI󾁓9\\.!\u0002pj&X?=r􅸤A5𩚏䤜\u0000ex`\u0001{ 𝩌\u0003󳽯^𘊏D􀩬𧵼𠐲m裠61JU\u0000tn@pCF3\u000cM𥸢ZrHM󺉄i𡔰zhn󶧼󰂧\u0005\u001b\u000b\u001f\u0003􇓚󺹼\u0014lU\u001d⥴[\u0011B\u0004 Y𧰏&cnLev俏awe\n𪵑+O𑀎N󽱴󰻦=x[7\"B\u001e\u0013\u0011\u0008Qy\u0010Nq~􌩔G*󾈲C𮨹Ho[𗜷9'󸤖6𭡑e#\u0017K샥W\u000e􎡥&kH7MQ\u0003\u0019,v2\u0001N/󾹍\tO𩑥\u000e󶩐b𭒦􈂫𤐕q󽖍􊙲ww}=$D\u0003w\u0010b􅀦\u0019~𢱜T\u001b\u0018𒑅Y~{\u000c{p𡱱*w􃑶Juo\n0𤵺sYXHT\u001fK\u0007󶡄\u0005\n%q iF𗙾\u001b\u001d.c\u001d󷪉􄞵\u001fW\u0019%x󵄢\u0015>\u0013􂟈", - "old_password": null + "new_password": "6go<􂵨fꃽY􈳇*KE󽤥])9󻆯􊕞K।􄉎1𘗢>qb2`\u00157*al\u000b3I*𦒁\u000e􊉪\u000c}\u0010𢁞~🩏@jjd\u0001O\u001a\u0001𗌗lv䃂B\u00083􄔙?Ih1bv\u000b\u001az\u0001\u001c😈0.\u0005b윮\u001cR<􏒫\u0004\u0011l\"E𞡊X6\u0015S󷂒?\u001b\u0004EKÒh팥䄰\u000c6󠁸>\u000b_Zd n<\u0012k􉖈s􅌻~\u0001󶹅\u0018r\u0010G󳶒\u0000`.)lj\u0010Rr/𤞸\\𫈘󴆴𭵝𗨦kI󾁓9\\.!\u0002pj&X?=r􅸤A5𩚏䤜\u0000ex`\u0001{ 𝩌\u0003󳽯^𘊏D􀩬𧵼𠐲m裠61JU\u0000tn@pCF3\u000cM𥸢ZrHM󺉄i𡔰zhn󶧼󰂧\u0005\u001b\u000b\u001f\u0003􇓚󺹼\u0014lU\u001d⥴[\u0011B\u0004 Y𧰏&cnLev俏awe\n𪵑+O𑀎N󽱴󰻦=x[7\"B\u001e\u0013\u0011\u0008Qy\u0010Nq~􌩔G*󾈲C𮨹Ho[𗜷9'󸤖6𭡑e#\u0017K샥W\u000e􎡥&kH7MQ\u0003\u0019,v2\u0001N/󾹍\tO𩑥\u000e󶩐b𭒦􈂫𤐕q󽖍􊙲ww}=$D\u0003w\u0010b􅀦\u0019~𢱜T\u001b\u0018𒑅Y~{\u000c{p𡱱*w􃑶Juo\n0𤵺sYXHT\u001fK\u0007󶡄\u0005\n%q iF𗙾\u001b\u001d.c\u001d󷪉􄞵\u001fW\u0019%x󵄢\u0015>\u0013􂟈" } diff --git a/libs/wire-api/test/golden/testObject_PasswordChange_user_17.json b/libs/wire-api/test/golden/testObject_PasswordChange_user_17.json index 079dbd79a9..8dc1a26d5b 100644 --- a/libs/wire-api/test/golden/testObject_PasswordChange_user_17.json +++ b/libs/wire-api/test/golden/testObject_PasswordChange_user_17.json @@ -1,4 +1,3 @@ { - "new_password": "󿞫X2cZf\tI\u0010.3樑ꊩ󱊝\u000e𨳮糽𦡈U峖𦿤\u0012󲪍xy􍘒DB􋣨\u001fP!≈3뒗\u000c㤈.1󹇚󾭏ji𡆁C𗍮䉇酖䪛\u001f\u0000~BYB\u001eUYc\u0003􊘨e🈨𭱐ᄻ\"cN&\t.9􌉽?\u001d^\u000b\u0001\u0008\u0018ꊛ&㔋􎭙a󷪠)z6Z𫴦􊱴\u001bX\u000f㗿xJ\u0016֔\u0019-\u0011I\nB=󾉏n!l𠆗~U􅅖;𛰔X𭱩\u0004𦹁󹷹uY鴇z􁚺D󹃹\u000cFbZtt\u0018:\u0018YQ\u0001h󵬙W􏄺\u0010𩌧!1}k\\􁭿z+\u0015􎉯\u0001􋇸%䑂?v􎡃h\u000cN\u000c\u0012\u0000\u001a𮒗󳏵P\u001dbP􎜘>ie􌫲Ỵi𩃮-癈]4i-\u0002/\u001dA\u0008&\u000b󾶽<􍍵􎋯M\\󲇎-pG\u001c𩕵\u0010HEJO\u0007|\t(⏹D=x<\u0017 aV󷏱O󳺅n|mdg󾯸@􏌿f\u0007󺒝W𮨌䵨\u0010h𨯽􊨀\u0017~⦜K~􅴪\u001b|\rdi\u001dﱽ𗈵􇾁;󺩗8e^𢬼T/D\u001f\u0015󰌧hTTe|N8􇅇1􊮋tJR\u0007𥺘iJ2󳩶}赛瀩扱\u000e􎱴0ુ!y\u00011W\u001fzX\u00010󲄬s𝒳h𓊖𡈵#􆒳𡯮SR󱖟,V𬃧v\u0015𨑊1g\u001a\u001b𗶱􇻦\u0002Q):\u001f/E'𠆔\u0010z!\u000e@󹋾Hy5*R󶩿\u001a󱳖󾼹󷬼􃰇𫢬􃒛`솖\u0012.M􏺪W𩴰\u0006󺋆Imf#dHF\u0017c娛\u0013QcG􅝄B\u0000W<\u001eC􂭸󱔉󹡃L7", - "old_password": null + "new_password": "󿞫X2cZf\tI\u0010.3樑ꊩ󱊝\u000e𨳮糽𦡈U峖𦿤\u0012󲪍xy􍘒DB􋣨\u001fP!≈3뒗\u000c㤈.1󹇚󾭏ji𡆁C𗍮䉇酖䪛\u001f\u0000~BYB\u001eUYc\u0003􊘨e🈨𭱐ᄻ\"cN&\t.9􌉽?\u001d^\u000b\u0001\u0008\u0018ꊛ&㔋􎭙a󷪠)z6Z𫴦􊱴\u001bX\u000f㗿xJ\u0016֔\u0019-\u0011I\nB=󾉏n!l𠆗~U􅅖;𛰔X𭱩\u0004𦹁󹷹uY鴇z􁚺D󹃹\u000cFbZtt\u0018:\u0018YQ\u0001h󵬙W􏄺\u0010𩌧!1}k\\􁭿z+\u0015􎉯\u0001􋇸%䑂?v􎡃h\u000cN\u000c\u0012\u0000\u001a𮒗󳏵P\u001dbP􎜘>ie􌫲Ỵi𩃮-癈]4i-\u0002/\u001dA\u0008&\u000b󾶽<􍍵􎋯M\\󲇎-pG\u001c𩕵\u0010HEJO\u0007|\t(⏹D=x<\u0017 aV󷏱O󳺅n|mdg󾯸@􏌿f\u0007󺒝W𮨌䵨\u0010h𨯽􊨀\u0017~⦜K~􅴪\u001b|\rdi\u001dﱽ𗈵􇾁;󺩗8e^𢬼T/D\u001f\u0015󰌧hTTe|N8􇅇1􊮋tJR\u0007𥺘iJ2󳩶}赛瀩扱\u000e􎱴0ુ!y\u00011W\u001fzX\u00010󲄬s𝒳h𓊖𡈵#􆒳𡯮SR󱖟,V𬃧v\u0015𨑊1g\u001a\u001b𗶱􇻦\u0002Q):\u001f/E'𠆔\u0010z!\u000e@󹋾Hy5*R󶩿\u001a󱳖󾼹󷬼􃰇𫢬􃒛`솖\u0012.M􏺪W𩴰\u0006󺋆Imf#dHF\u0017c娛\u0013QcG􅝄B\u0000W<\u001eC􂭸󱔉󹡃L7" } diff --git a/libs/wire-api/test/golden/testObject_PasswordChange_user_18.json b/libs/wire-api/test/golden/testObject_PasswordChange_user_18.json index 9b87cd015c..52c7a9d4b5 100644 --- a/libs/wire-api/test/golden/testObject_PasswordChange_user_18.json +++ b/libs/wire-api/test/golden/testObject_PasswordChange_user_18.json @@ -1,4 +1,3 @@ { - "new_password": "$󾮝izIhKE𥋌b\u000ci𨒼v=:\t9󻗨\r⣴!􀲃:󴽌&/#􊛋G𭿷$W󱲯>\u0019Do`F\nY\u0019L\u0004\t\u00005󳒈bC8ᑱBq󸢵$p\u0000\u000b┆R^\u0016GF&󷅀+]𦐧]壢鞈;:𠉵𦄍w􄉷\u0015\u0014\u0016􂾥󷺴gi\u000f\"\u000bqᢹvV󾃑\u0010Yya􍍕};,K3\u0010n\u0003\u0006\u0012+𭅵𢭯\u000e^q\u00134+Iby-\u0005􁎦𧮉_", - "old_password": null + "new_password": "$󾮝izIhKE𥋌b\u000ci𨒼v=:\t9󻗨\r⣴!􀲃:󴽌&/#􊛋G𭿷$W󱲯>\u0019Do`F\nY\u0019L\u0004\t\u00005󳒈bC8ᑱBq󸢵$p\u0000\u000b┆R^\u0016GF&󷅀+]𦐧]壢鞈;:𠉵𦄍w􄉷\u0015\u0014\u0016􂾥󷺴gi\u000f\"\u000bqᢹvV󾃑\u0010Yya􍍕};,K3\u0010n\u0003\u0006\u0012+𭅵𢭯\u000e^q\u00134+Iby-\u0005􁎦𧮉_" } diff --git a/libs/wire-api/test/golden/testObject_PasswordChange_user_7.json b/libs/wire-api/test/golden/testObject_PasswordChange_user_7.json index aeb51f98f6..19fe98e502 100644 --- a/libs/wire-api/test/golden/testObject_PasswordChange_user_7.json +++ b/libs/wire-api/test/golden/testObject_PasswordChange_user_7.json @@ -1,4 +1,3 @@ { - "new_password": "􇎃7ﻂ\u001eA󴇌k\u000b\u001b\u0015g&:\u00006K\u0000n>\u001a𝒦$\u001b8B'z\"Yc󻾅6\u001bU)\u0000\u0010􌵬Zj굡𔓻bJ-\u001f\"𠋜L\u0007􄫓y\u0010s-}𢨂}\u001d\u0018\u0006\rR}R\u0018L\u001cqkZ\n\t𮉈1|\u000c5󰏵L\u00178gL䝴5⨓cf\\V~\u0011Ⲕ\u0019𝠁󱮄1n\"*\u0017\u000faTxUcZ\r#5/\u0008k\u000b[\u0007`􎉒\u0000R;(\u0018\rFMN>󳆴\u001bt\u0006{'(𢣤\u0003\u0000T􂄷m\u000c6綠B\n󱋢", - "old_password": null + "new_password": "􇎃7ﻂ\u001eA󴇌k\u000b\u001b\u0015g&:\u00006K\u0000n>\u001a𝒦$\u001b8B'z\"Yc󻾅6\u001bU)\u0000\u0010􌵬Zj굡𔓻bJ-\u001f\"𠋜L\u0007􄫓y\u0010s-}𢨂}\u001d\u0018\u0006\rR}R\u0018L\u001cqkZ\n\t𮉈1|\u000c5󰏵L\u00178gL䝴5⨓cf\\V~\u0011Ⲕ\u0019𝠁󱮄1n\"*\u0017\u000faTxUcZ\r#5/\u0008k\u000b[\u0007`􎉒\u0000R;(\u0018\rFMN>󳆴\u001bt\u0006{'(𢣤\u0003\u0000T􂄷m\u000c6綠B\n󱋢" } diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index c26cfc4b43..dba6070fb5 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -161,6 +161,7 @@ library , currency-codes >=2.0 , deriving-aeson >=0.2 , deriving-swagger2 + , either , email-validate >=2.0 , errors , extended diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 58d9246315..b5830fe665 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -39,6 +39,7 @@ import qualified Network.Wai.Utilities.Error as Wai import Servant.API.Status import Wire.API.ErrorDescription import Wire.API.Federation.Error +import Wire.API.User (ChangeHandleError (..), UpdateProfileError (..)) errorDescriptionToWai :: forall (code :: Nat) (lbl :: Symbol) (desc :: Symbol). @@ -110,18 +111,18 @@ connError InvalidTransition {} = StdError (errorDescriptionTypeToWai @InvalidTra connError NotConnected {} = StdError (errorDescriptionTypeToWai @NotConnected) connError InvalidUser {} = StdError (errorDescriptionTypeToWai @InvalidUser) connError ConnectNoIdentity {} = StdError (errorDescriptionToWai (noIdentity 0)) -connError (ConnectBlacklistedUserKey k) = StdError $ foldKey (const blacklistedEmail) (const blacklistedPhone) k +connError (ConnectBlacklistedUserKey k) = StdError $ foldKey (const blacklistedEmail) (const (errorDescriptionTypeToWai @BlacklistedPhone)) k connError (ConnectInvalidEmail _ _) = StdError invalidEmail -connError ConnectInvalidPhone {} = StdError invalidPhone +connError ConnectInvalidPhone {} = StdError (errorDescriptionTypeToWai @InvalidPhone) connError ConnectSameBindingTeamUsers = StdError sameBindingTeamUsers connError ConnectMissingLegalholdConsent = StdError (errorDescriptionTypeToWai @MissingLegalholdConsent) connError (ConnectFederationError e) = fedError e actError :: ActivationError -> Error -actError (UserKeyExists _) = StdError userKeyExists +actError (UserKeyExists _) = StdError (errorDescriptionTypeToWai @UserKeyExists) actError (InvalidActivationCode e) = StdError (invalidActivationCode e) actError (InvalidActivationEmail _ _) = StdError invalidEmail -actError (InvalidActivationPhone _) = StdError invalidPhone +actError (InvalidActivationPhone _) = StdError (errorDescriptionTypeToWai @InvalidPhone) pwResetError :: PasswordResetError -> Error pwResetError InvalidPasswordResetKey = StdError invalidPwResetKey @@ -138,45 +139,35 @@ newUserError :: CreateUserError -> Error newUserError InvalidInvitationCode = StdError invalidInvitationCode newUserError MissingIdentity = StdError missingIdentity newUserError (InvalidEmail _ _) = StdError invalidEmail -newUserError (InvalidPhone _) = StdError invalidPhone -newUserError (DuplicateUserKey _) = StdError userKeyExists +newUserError (InvalidPhone _) = StdError (errorDescriptionTypeToWai @InvalidPhone) +newUserError (DuplicateUserKey _) = StdError (errorDescriptionTypeToWai @UserKeyExists) newUserError (EmailActivationError e) = actError e newUserError (PhoneActivationError e) = actError e -newUserError (BlacklistedUserKey k) = StdError $ foldKey (const blacklistedEmail) (const blacklistedPhone) k +newUserError (BlacklistedUserKey k) = StdError $ foldKey (const blacklistedEmail) (const (errorDescriptionTypeToWai @BlacklistedPhone)) k newUserError TooManyTeamMembers = StdError tooManyTeamMembers newUserError UserCreationRestricted = StdError userCreationRestricted newUserError (ExternalPreconditionFailed e) = StdError e sendLoginCodeError :: SendLoginCodeError -> Error -sendLoginCodeError (SendLoginInvalidPhone _) = StdError invalidPhone +sendLoginCodeError (SendLoginInvalidPhone _) = StdError (errorDescriptionTypeToWai @InvalidPhone) sendLoginCodeError SendLoginPasswordExists = StdError passwordExists sendActCodeError :: SendActivationCodeError -> Error -sendActCodeError (InvalidRecipient k) = StdError $ foldKey (const invalidEmail) (const invalidPhone) k -sendActCodeError (UserKeyInUse _) = StdError userKeyExists -sendActCodeError (ActivationBlacklistedUserKey k) = StdError $ foldKey (const blacklistedEmail) (const blacklistedPhone) k +sendActCodeError (InvalidRecipient k) = StdError $ foldKey (const invalidEmail) (const (errorDescriptionTypeToWai @InvalidPhone)) k +sendActCodeError (UserKeyInUse _) = StdError (errorDescriptionTypeToWai @UserKeyExists) +sendActCodeError (ActivationBlacklistedUserKey k) = StdError $ foldKey (const blacklistedEmail) (const (errorDescriptionTypeToWai @BlacklistedPhone)) k changeEmailError :: ChangeEmailError -> Error changeEmailError (InvalidNewEmail _ _) = StdError invalidEmail -changeEmailError (EmailExists _) = StdError userKeyExists +changeEmailError (EmailExists _) = StdError (errorDescriptionTypeToWai @UserKeyExists) changeEmailError (ChangeBlacklistedEmail _) = StdError blacklistedEmail changeEmailError EmailManagedByScim = StdError $ propertyManagedByScim "email" -changePhoneError :: ChangePhoneError -> Error -changePhoneError (InvalidNewPhone _) = StdError invalidPhone -changePhoneError (PhoneExists _) = StdError userKeyExists -changePhoneError (BlacklistedNewPhone _) = StdError blacklistedPhone - -changePwError :: ChangePasswordError -> Error -changePwError InvalidCurrentPassword = StdError (errorDescriptionTypeToWai @BadCredentials) -changePwError ChangePasswordNoIdentity = StdError (errorDescriptionToWai (noIdentity 1)) -changePwError ChangePasswordMustDiffer = StdError changePasswordMustDiffer - changeHandleError :: ChangeHandleError -> Error changeHandleError ChangeHandleNoIdentity = StdError (errorDescriptionToWai (noIdentity 2)) -changeHandleError ChangeHandleExists = StdError handleExists -changeHandleError ChangeHandleInvalid = StdError invalidHandle -changeHandleError ChangeHandleManagedByScim = StdError $ propertyManagedByScim "handle" +changeHandleError ChangeHandleExists = StdError (errorDescriptionToWai (mkErrorDescription :: HandleExists)) +changeHandleError ChangeHandleInvalid = StdError (errorDescriptionToWai (mkErrorDescription :: InvalidHandle)) +changeHandleError ChangeHandleManagedByScim = StdError (errorDescriptionToWai (mkErrorDescription :: HandleManagedByScim)) legalHoldLoginError :: LegalHoldLoginError -> Error legalHoldLoginError LegalHoldLoginNoBindingTeam = StdError noBindingTeam @@ -230,11 +221,6 @@ clientError ClientMissingLegalholdConsent = StdError (errorDescriptionTypeToWai fedError :: FederationError -> Error fedError = StdError . federationErrorToWai -idtError :: RemoveIdentityError -> Error -idtError LastIdentity = StdError lastIdentity -idtError NoPassword = StdError noPassword -idtError NoIdentity = StdError (errorDescriptionToWai (noIdentity 3)) - propDataError :: PropertiesDataError -> Error propDataError TooManyProperties = StdError tooManyProperties @@ -256,13 +242,13 @@ accountStatusError :: AccountStatusError -> Error accountStatusError InvalidAccountStatus = StdError invalidAccountStatus phoneError :: PhoneException -> Error -phoneError PhoneNumberUnreachable = StdError invalidPhone -phoneError PhoneNumberBarred = StdError blacklistedPhone +phoneError PhoneNumberUnreachable = StdError (errorDescriptionTypeToWai @InvalidPhone) +phoneError PhoneNumberBarred = StdError (errorDescriptionTypeToWai @BlacklistedPhone) phoneError (PhoneBudgetExhausted t) = RichError phoneBudgetExhausted (PhoneBudgetTimeout t) [] updateProfileError :: UpdateProfileError -> Error updateProfileError DisplayNameManagedByScim = StdError (propertyManagedByScim "name") -updateProfileError (ProfileNotFound _) = StdError (errorDescriptionTypeToWai @UserNotFound) +updateProfileError ProfileNotFound = StdError (errorDescriptionTypeToWai @UserNotFound) -- WAI Errors ----------------------------------------------------------------- @@ -281,12 +267,6 @@ clientCapabilitiesCannotBeRemoved = Wai.mkError status409 "client-capabilities-c noEmail :: Wai.Error noEmail = Wai.mkError status403 "no-email" "This operation requires the user to have a verified email address." -lastIdentity :: Wai.Error -lastIdentity = Wai.mkError status403 "last-identity" "The last user identity (email or phone number) cannot be removed." - -noPassword :: Wai.Error -noPassword = Wai.mkError status403 "no-password" "The user has no password." - invalidEmail :: Wai.Error invalidEmail = Wai.mkError status400 "invalid-email" "Invalid e-mail address." @@ -296,12 +276,6 @@ invalidPwResetKey = Wai.mkError status400 "invalid-key" "Invalid email or mobile resetPasswordMustDiffer :: Wai.Error resetPasswordMustDiffer = Wai.mkError status409 "password-must-differ" "For password reset, new and old password must be different." -changePasswordMustDiffer :: Wai.Error -changePasswordMustDiffer = Wai.mkError status409 "password-must-differ" "For password change, new and old password must be different." - -invalidPhone :: Wai.Error -invalidPhone = Wai.mkError status400 "invalid-phone" "Invalid mobile phone number." - invalidInvitationCode :: Wai.Error invalidInvitationCode = Wai.mkError status400 "invalid-invitation-code" "Invalid invitation code." @@ -314,21 +288,12 @@ invalidPwResetCode = Wai.mkError status400 "invalid-code" "Invalid password rese duplicatePwResetCode :: Wai.Error duplicatePwResetCode = Wai.mkError status409 "code-exists" "A password reset is already in progress." -userKeyExists :: Wai.Error -userKeyExists = Wai.mkError status409 "key-exists" "The given e-mail address or phone number is in use." - emailExists :: Wai.Error emailExists = Wai.mkError status409 "email-exists" "The given e-mail address is in use." phoneExists :: Wai.Error phoneExists = Wai.mkError status409 "phone-exists" "The given phone number is in use." -handleExists :: Wai.Error -handleExists = Wai.mkError status409 "handle-exists" "The given handle is already taken." - -invalidHandle :: Wai.Error -invalidHandle = Wai.mkError status400 "invalid-handle" "The given handle is invalid." - badRequest :: LText -> Wai.Error badRequest = Wai.mkError status400 "bad-request" @@ -379,14 +344,6 @@ blacklistedEmail = "The given e-mail address has been blacklisted due to a permanent bounce \ \or a complaint." -blacklistedPhone :: Wai.Error -blacklistedPhone = - Wai.mkError - status403 - "blacklisted-phone" - "The given phone number has been blacklisted due to suspected abuse \ - \or a complaint." - passwordExists :: Wai.Error passwordExists = Wai.mkError @@ -416,9 +373,6 @@ authMissingToken = Wai.mkError status403 "invalid-credentials" "Missing token" authMissingCookieAndToken :: Wai.Error authMissingCookieAndToken = Wai.mkError status403 "invalid-credentials" "Missing cookie and token" -invalidUserToken :: Wai.Error -invalidUserToken = Wai.mkError status403 "invalid-credentials" "Invalid user token" - invalidAccessToken :: Wai.Error invalidAccessToken = Wai.mkError status403 "invalid-credentials" "Invalid access token" @@ -437,9 +391,6 @@ authTokenInvalid = Wai.mkError status403 "invalid-credentials" "Invalid token" authTokenUnsupported :: Wai.Error authTokenUnsupported = Wai.mkError status403 "invalid-credentials" "Unsupported token operation for this token type" -incorrectPermissions :: Wai.Error -incorrectPermissions = Wai.mkError status403 "invalid-permissions" "Copy permissions must be a subset of self permissions" - -- | User's relation to the team is not what we expect it to be. Examples: -- -- * Requested action requires the user to be a team member, but the user doesn't belong to diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 0f9e108ceb..355e5ca1b9 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -593,7 +593,7 @@ updateUserName uid (NameUpdate nameUpd) = do checkHandleInternalH :: Text -> Handler Response checkHandleInternalH = API.checkHandle >=> \case - API.CheckHandleInvalid -> throwE (StdError invalidHandle) + API.CheckHandleInvalid -> throwE (StdError (errorDescriptionTypeToWai @InvalidHandle)) API.CheckHandleFound -> pure $ setStatus status200 empty API.CheckHandleNotFound -> pure $ setStatus status404 empty diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index d35ad9c5e8..850705a433 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -170,6 +170,14 @@ servantSitemap = BrigAPI.getUserQualified = getUser, BrigAPI.getSelf = getSelf, BrigAPI.deleteSelf = deleteUser, + BrigAPI.putSelf = updateUser, + BrigAPI.changePhone = changePhone, + BrigAPI.removePhone = removePhone, + BrigAPI.removeEmail = removeEmail, + BrigAPI.checkPasswordExists = checkPasswordExists, + BrigAPI.changePassword = changePassword, + BrigAPI.changeLocale = changeLocale, + BrigAPI.changeHandle = changeHandle, BrigAPI.updateUserEmail = updateUserEmail, BrigAPI.getHandleInfoUnqualified = getHandleInfoUnqualifiedH, BrigAPI.getUserByHandleQualified = Handle.getHandleInfo, @@ -235,7 +243,7 @@ sitemap = do Doc.parameter Doc.Path "handle" Doc.bytes' $ Doc.description "Handle to check" Doc.response 200 "Handle is taken" Doc.end - Doc.errorResponse invalidHandle + Doc.errorResponse (errorDescriptionTypeToWai @InvalidHandle) Doc.errorResponse (errorDescriptionTypeToWai @HandleNotFound) -- some APIs moved to servant @@ -255,18 +263,6 @@ sitemap = do -- User Self API ------------------------------------------------------ - -- This endpoint can lead to the following events being sent: - -- - UserUpdated event to contacts of self - put "/self" (continue updateUserH) $ - zauthUserId - .&. zauthConnId - .&. jsonRequest @Public.UserUpdate - document "PUT" "updateSelf" $ do - Doc.summary "Update your profile" - Doc.body (Doc.ref Public.modelUserUpdate) $ - Doc.description "JSON body" - Doc.response 200 "Update successful." Doc.end - get "/self/name" (continue getUserDisplayNameH) $ accept "application" "json" .&. zauthUserId @@ -275,88 +271,6 @@ sitemap = do Doc.returns (Doc.ref Public.modelUserDisplayName) Doc.response 200 "Profile name found." Doc.end - put "/self/phone" (continue changePhoneH) $ - zauthUserId - .&. zauthConnId - .&. jsonRequest @Public.PhoneUpdate - document "PUT" "changePhone" $ do - Doc.summary "Change your phone number" - Doc.body (Doc.ref Public.modelPhoneUpdate) $ - Doc.description "JSON body" - Doc.response 202 "Update accepted and pending activation of the new phone number." Doc.end - Doc.errorResponse userKeyExists - - head - "/self/password" - (continue checkPasswordExistsH) - zauthUserId - document "HEAD" "checkPassword" $ do - Doc.summary "Check that your password is set" - Doc.response 200 "Password is set." Doc.end - Doc.response 404 "Password is not set." Doc.end - - put "/self/password" (continue changePasswordH) $ - zauthUserId - .&. jsonRequest @Public.PasswordChange - document "PUT" "changePassword" $ do - Doc.summary "Change your password" - Doc.body (Doc.ref Public.modelChangePassword) $ - Doc.description "JSON body" - Doc.response 200 "Password changed." Doc.end - Doc.errorResponse (errorDescriptionTypeToWai @BadCredentials) - Doc.errorResponse (errorDescriptionToWai (noIdentity 4)) - - put "/self/locale" (continue changeLocaleH) $ - zauthUserId - .&. zauthConnId - .&. jsonRequest @Public.LocaleUpdate - document "PUT" "changeLocale" $ do - Doc.summary "Change your locale" - Doc.body (Doc.ref Public.modelChangeLocale) $ - Doc.description "JSON body" - Doc.response 200 "Locale changed." Doc.end - - -- This endpoint can lead to the following events being sent: - -- - UserUpdated event to contacts of self - put "/self/handle" (continue changeHandleH) $ - zauthUserId - .&. zauthConnId - .&. jsonRequest @Public.HandleUpdate - document "PUT" "changeHandle" $ do - Doc.summary "Change your handle" - Doc.body (Doc.ref Public.modelChangeHandle) $ - Doc.description "JSON body" - Doc.errorResponse handleExists - Doc.errorResponse invalidHandle - Doc.response 200 "Handle changed." Doc.end - - -- This endpoint can lead to the following events being sent: - -- - UserIdentityRemoved event to self - delete "/self/phone" (continue removePhoneH) $ - zauthUserId - .&. zauthConnId - document "DELETE" "removePhone" $ do - Doc.summary "Remove your phone number." - Doc.notes - "Your phone number can only be removed if you also have an \ - \email address and a password." - Doc.response 200 "Phone number removed." Doc.end - Doc.errorResponse lastIdentity - Doc.errorResponse noPassword - - -- This endpoint can lead to the following events being sent: - -- - UserIdentityRemoved event to self - delete "/self/email" (continue removeEmailH) $ - zauthUserId - .&. zauthConnId - document "DELETE" "removeEmail" $ do - Doc.summary "Remove your email address." - Doc.notes - "Your email address can only be removed if you also have a \ - \phone number." - Doc.response 200 "Email address removed." Doc.end - Doc.errorResponse lastIdentity - -- TODO put where? -- This endpoint can lead to the following events being sent: @@ -466,10 +380,10 @@ sitemap = do Doc.errorResponse whitelistError Doc.errorResponse invalidInvitationCode Doc.errorResponse missingIdentity - Doc.errorResponse userKeyExists + Doc.errorResponse (errorDescriptionTypeToWai @UserKeyExists) Doc.errorResponse activationCodeNotFound Doc.errorResponse blacklistedEmail - Doc.errorResponse blacklistedPhone + Doc.errorResponse (errorDescriptionTypeToWai @BlacklistedPhone) -- This endpoint can lead to the following events being sent: -- - UserActivated event to the user, if account gets activated @@ -518,10 +432,10 @@ sitemap = do Doc.description "JSON body" Doc.response 200 "Activation code sent." Doc.end Doc.errorResponse invalidEmail - Doc.errorResponse invalidPhone - Doc.errorResponse userKeyExists + Doc.errorResponse (errorDescriptionTypeToWai @InvalidPhone) + Doc.errorResponse (errorDescriptionTypeToWai @UserKeyExists) Doc.errorResponse blacklistedEmail - Doc.errorResponse blacklistedPhone + Doc.errorResponse (errorDescriptionTypeToWai @BlacklistedPhone) Doc.errorResponse (customerExtensionBlockedDomain (either undefined id $ mkDomain "example.com")) post "/password-reset" (continue beginPasswordResetH) $ @@ -894,56 +808,41 @@ newtype GetActivationCodeResp instance ToJSON GetActivationCodeResp where toJSON (GetActivationCodeResp (k, c)) = object ["key" .= k, "code" .= c] -updateUserH :: UserId ::: ConnId ::: JsonRequest Public.UserUpdate -> Handler Response -updateUserH (uid ::: conn ::: req) = do - uu <- parseJsonBody req - API.updateUser uid (Just conn) uu API.ForbidSCIMUpdates !>> updateProfileError - return empty - -changePhoneH :: UserId ::: ConnId ::: JsonRequest Public.PhoneUpdate -> Handler Response -changePhoneH (u ::: c ::: req) = - setStatus status202 empty <$ (changePhone u c =<< parseJsonBody req) +updateUser :: UserId -> ConnId -> Public.UserUpdate -> Handler (Maybe Public.UpdateProfileError) +updateUser uid conn uu = do + eithErr <- lift $ runExceptT $ API.updateUser uid (Just conn) uu API.ForbidSCIMUpdates + pure $ either Just (const Nothing) eithErr -changePhone :: UserId -> ConnId -> Public.PhoneUpdate -> Handler () -changePhone u _ (Public.puPhone -> phone) = do - (adata, pn) <- API.changePhone u phone !>> changePhoneError +changePhone :: UserId -> ConnId -> Public.PhoneUpdate -> Handler (Maybe Public.ChangePhoneError) +changePhone u _ (Public.puPhone -> phone) = lift . exceptTToMaybe $ do + (adata, pn) <- API.changePhone u phone loc <- lift $ API.lookupLocale u let apair = (activationKey adata, activationCode adata) lift $ sendActivationSms pn apair loc -removePhoneH :: UserId ::: ConnId -> Handler Response -removePhoneH (self ::: conn) = do - API.removePhone self conn !>> idtError - return empty +removePhone :: UserId -> ConnId -> Handler (Maybe Public.RemoveIdentityError) +removePhone self conn = + lift . exceptTToMaybe $ API.removePhone self conn -removeEmailH :: UserId ::: ConnId -> Handler Response -removeEmailH (self ::: conn) = do - API.removeEmail self conn !>> idtError - return empty +removeEmail :: UserId -> ConnId -> Handler (Maybe Public.RemoveIdentityError) +removeEmail self conn = + lift . exceptTToMaybe $ API.removeEmail self conn -checkPasswordExistsH :: UserId -> Handler Response -checkPasswordExistsH self = do - exists <- lift $ isJust <$> API.lookupPassword self - return $ if exists then empty else setStatus status404 empty +checkPasswordExists :: UserId -> Handler Bool +checkPasswordExists = fmap isJust . lift . API.lookupPassword -changePasswordH :: UserId ::: JsonRequest Public.PasswordChange -> Handler Response -changePasswordH (u ::: req) = do - cp <- parseJsonBody req - API.changePassword u cp !>> changePwError - return empty +changePassword :: UserId -> Public.PasswordChange -> Handler (Maybe Public.ChangePasswordError) +changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp -changeLocaleH :: UserId ::: ConnId ::: JsonRequest Public.LocaleUpdate -> Handler Response -changeLocaleH (u ::: conn ::: req) = do - l <- parseJsonBody req - lift $ API.changeLocale u conn l - return empty +changeLocale :: UserId -> ConnId -> Public.LocaleUpdate -> Handler () +changeLocale u conn l = lift $ API.changeLocale u conn l -- | (zusr is ignored by this handler, ie. checking handles is allowed as long as you have -- *any* account.) checkHandleH :: UserId ::: Text -> Handler Response checkHandleH (_uid ::: hndl) = API.checkHandle hndl >>= \case - API.CheckHandleInvalid -> throwE (StdError invalidHandle) + API.CheckHandleInvalid -> throwE (StdError (errorDescriptionTypeToWai @InvalidHandle)) API.CheckHandleFound -> pure $ setStatus status200 empty API.CheckHandleNotFound -> pure $ setStatus status404 empty @@ -964,15 +863,10 @@ getHandleInfoUnqualifiedH self handle = do Public.UserHandleInfo . Public.profileQualifiedId <$$> Handle.getHandleInfo self (Qualified handle domain) -changeHandleH :: UserId ::: ConnId ::: JsonRequest Public.HandleUpdate -> Handler Response -changeHandleH (u ::: conn ::: req) = - empty <$ (changeHandle u conn =<< parseJsonBody req) - -changeHandle :: UserId -> ConnId -> Public.HandleUpdate -> Handler () -changeHandle u conn (Public.HandleUpdate h) = do - handle <- API.validateHandle h - -- TODO check here - API.changeHandle u (Just conn) handle API.ForbidSCIMUpdates !>> changeHandleError +changeHandle :: UserId -> ConnId -> Public.HandleUpdate -> Handler (Maybe Public.ChangeHandleError) +changeHandle u conn (Public.HandleUpdate h) = lift . exceptTToMaybe $ do + handle <- maybe (throwError Public.ChangeHandleInvalid) pure $ parseHandle h + API.changeHandle u (Just conn) handle API.ForbidSCIMUpdates beginPasswordResetH :: JSON ::: JsonRequest Public.NewPasswordReset -> Handler Response beginPasswordResetH (_ ::: req) = diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 8d9359d29d..663411d793 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -101,10 +101,6 @@ data CreateUserError | -- | Some precondition on another Wire service failed. We propagate this error. ExternalPreconditionFailed Wai.Error -data UpdateProfileError - = DisplayNameManagedByScim - | ProfileNotFound UserId - data InvitationError = InviteeEmailExists UserId | InviteInvalidEmail Email @@ -156,28 +152,12 @@ data LoginError | LoginThrottled RetryAfter | LoginBlocked RetryAfter -data ChangePasswordError - = InvalidCurrentPassword - | ChangePasswordNoIdentity - | ChangePasswordMustDiffer - -data ChangePhoneError - = PhoneExists !Phone - | InvalidNewPhone !Phone - | BlacklistedNewPhone !Phone - data ChangeEmailError = InvalidNewEmail !Email !String | EmailExists !Email | ChangeBlacklistedEmail !Email | EmailManagedByScim -data ChangeHandleError - = ChangeHandleNoIdentity - | ChangeHandleExists - | ChangeHandleInvalid - | ChangeHandleManagedByScim - data SendActivationCodeError = InvalidRecipient UserKey | UserKeyInUse UserKey @@ -197,11 +177,6 @@ data ClientError | ClientCapabilitiesCannotBeRemoved | ClientMissingLegalholdConsent -data RemoveIdentityError - = LastIdentity - | NoPassword - | NoIdentity - data DeleteUserError = DeleteUserInvalid | DeleteUserInvalidCode diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 13e2169206..ccbb7df362 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -153,6 +153,7 @@ import UnliftIO.Async import Wire.API.Federation.Error import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Team.Member (legalHoldStatus) +import Wire.API.User data AllowSCIMUpdates = AllowSCIMUpdates @@ -454,7 +455,7 @@ updateUser :: UserId -> Maybe ConnId -> UserUpdate -> AllowSCIMUpdates -> Except updateUser uid mconn uu allowScim = do for_ (uupName uu) $ \newName -> do mbUser <- lift $ Data.lookupUser WithPendingInvitations uid - user <- maybe (throwE (ProfileNotFound uid)) pure mbUser + user <- maybe (throwE ProfileNotFound) pure mbUser unless ( userManagedBy user /= ManagedByScim || userDisplayName user == newName @@ -611,22 +612,21 @@ changePhone :: UserId -> Phone -> ExceptT ChangePhoneError AppIO (Activation, Ph changePhone u phone = do canonical <- maybe - (throwE $ InvalidNewPhone phone) + (throwE InvalidNewPhone) return =<< lift (validatePhone phone) let pk = userPhoneKey canonical available <- lift $ Data.keyAvailable pk (Just u) unless available $ - throwE $ - PhoneExists phone + throwE PhoneExists timeout <- setActivationTimeout <$> view settings blacklisted <- lift $ Blacklist.exists pk when blacklisted $ - throwE (BlacklistedNewPhone canonical) + throwE BlacklistedNewPhone -- check if any prefixes of this phone number are blocked prefixExcluded <- lift $ Blacklist.existsAnyPrefix canonical when prefixExcluded $ - throwE (BlacklistedNewPhone canonical) + throwE BlacklistedNewPhone act <- lift $ Data.newActivation pk timeout (Just u) return (act, canonical) diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index 3d4276d7f8..58d90d80c2 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -23,9 +23,11 @@ module Brig.API.Util validateHandle, logEmail, traverseConcurrentlyWithErrors, + exceptTToMaybe, ) where +import Brig.API.Error import qualified Brig.API.Error as Error import Brig.API.Handler import Brig.API.Types @@ -46,6 +48,7 @@ import qualified System.Logger as Log import UnliftIO.Async import UnliftIO.Exception (throwIO, try) import Util.Logging (sha256String) +import Wire.API.ErrorDescription lookupProfilesMaybeFilterSameTeamOnly :: UserId -> [UserProfile] -> Handler [UserProfile] lookupProfilesMaybeFilterSameTeamOnly self us = do @@ -68,7 +71,7 @@ lookupSelfProfile = fmap (fmap mk) . Data.lookupAccount mk a = SelfProfile (accountUser a) validateHandle :: Text -> Handler Handle -validateHandle = maybe (throwE (Error.StdError Error.invalidHandle)) return . parseHandle +validateHandle = maybe (throwE (Error.StdError (errorDescriptionTypeToWai @InvalidHandle))) return . parseHandle logEmail :: Email -> (Msg -> Msg) logEmail email = @@ -86,3 +89,6 @@ traverseConcurrentlyWithErrors :: traverseConcurrentlyWithErrors f = ExceptT . try . (traverse (either throwIO pure) =<<) . pooledMapConcurrentlyN 8 (runExceptT . f) + +exceptTToMaybe :: Monad m => ExceptT e m () -> m (Maybe e) +exceptTToMaybe = (pure . either Just (const Nothing)) <=< runExceptT diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index a55b9ffb59..5f770008b6 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -332,11 +332,11 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do -- Validate phone inviteePhone <- for (irInviteePhone body) $ \p -> do - validatedPhone <- maybe (throwStd invalidPhone) return =<< lift (Phone.validatePhone p) + validatedPhone <- maybe (throwStd (errorDescriptionTypeToWai @InvalidPhone)) return =<< lift (Phone.validatePhone p) let ukp = userPhoneKey validatedPhone blacklistedPh <- lift $ Blacklist.exists ukp when blacklistedPh $ - throwStd blacklistedPhone + throwStd (errorDescriptionTypeToWai @BlacklistedPhone) phoneTaken <- lift $ isJust <$> Data.lookupKey ukp when phoneTaken $ throwStd phoneExists diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index bd81bc54cf..dae3407e7e 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -95,7 +95,7 @@ routesPublic = do Doc.description "JSON body" Doc.returns (Doc.ref Public.modelLoginCodeResponse) Doc.response 200 "Login code sent." Doc.end - Doc.errorResponse invalidPhone + Doc.errorResponse (errorDescriptionTypeToWai @InvalidPhone) Doc.errorResponse passwordExists Doc.errorResponse' loginCodePending Doc.pendingLoginError @@ -155,9 +155,9 @@ routesPublic = do Doc.response 202 "Update accepted and pending activation of the new email." Doc.end Doc.response 204 "No update, current and new email address are the same." Doc.end Doc.errorResponse invalidEmail - Doc.errorResponse userKeyExists + Doc.errorResponse (errorDescriptionTypeToWai @UserKeyExists) Doc.errorResponse blacklistedEmail - Doc.errorResponse blacklistedPhone + Doc.errorResponse (errorDescriptionTypeToWai @BlacklistedPhone) Doc.errorResponse missingAccessToken Doc.errorResponse invalidAccessToken Doc.errorResponse (errorDescriptionTypeToWai @BadCredentials) diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 77074404f8..098c4b831e 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -137,6 +137,7 @@ tests _ at opts p b c ch g aws = test' aws p "put /access/self/email - 2xx" $ testEmailUpdate b aws, test' aws p "put /self/phone - 202" $ testPhoneUpdate b, test' aws p "put /self/phone - 403" $ testPhoneUpdateBlacklisted b, + test' aws p "put /self/phone - 409" $ testPhoneUpdateConflict b, test' aws p "head /self/password - 200/404" $ testPasswordSet b, test' aws p "put /self/password - 200" $ testPasswordChange b, test' aws p "put /self/locale - 200" $ testUserLocaleUpdate b aws, @@ -823,12 +824,13 @@ testUserUpdate brig cannon aws = do bobUser <- randomUser brig liftIO $ Util.assertUserJournalQueue "user create bob" aws (userActivateJournaled bobUser) let bob = userId bobUser + aliceNewName <- randomName connectUsers brig alice (singleton bob) let newColId = Just 5 newAssets = Just [ImageAsset "abc" (Just AssetComplete)] - newName = Just $ Name "dogbert" + mNewName = Just $ aliceNewName newPic = Nothing -- Legacy - userUpdate = UserUpdate newName newPic newAssets newColId + userUpdate = UserUpdate mNewName newPic newAssets newColId update = RequestBodyLBS . encode $ userUpdate -- Update profile & receive notification WS.bracketRN cannon [alice, bob] $ \[aliceWS, bobWS] -> do @@ -840,7 +842,7 @@ testUserUpdate brig cannon aws = do -- get the updated profile get (brig . path "/self" . zUser alice) !!! do const 200 === statusCode - const (newName, newColId, newAssets) + const (mNewName, newColId, newAssets) === ( \u -> ( fmap userDisplayName u, fmap userAccentId u, @@ -851,7 +853,7 @@ testUserUpdate brig cannon aws = do -- get only the new name get (brig . path "/self/name" . zUser alice) !!! do const 200 === statusCode - const (String . fromName <$> newName) + const (String . fromName <$> mNewName) === ( \r -> do b <- responseBody r b ^? key "name" @@ -859,7 +861,7 @@ testUserUpdate brig cannon aws = do -- should appear in search by 'newName' suid <- userId <$> randomUser brig Search.refreshIndex brig - Search.assertCanFind brig suid aliceQ "dogbert" + Search.assertCanFind brig suid aliceQ (fromName aliceNewName) -- This tests the behavior of `/i/self/email` instead of `/self/email` or -- `/access/self/email`. tests for session token handling under `/access/self/email` are in @@ -934,6 +936,17 @@ testPhoneUpdateBlacklisted brig = do -- cleanup to avoid other tests failing sporadically deletePrefix brig (phonePrefix prefix) +testPhoneUpdateConflict :: Brig -> Http () +testPhoneUpdateConflict brig = do + uid1 <- userId <$> randomUser brig + phn <- randomPhone + updatePhone brig uid1 phn + + uid2 <- userId <$> randomUser brig + let phoneUpdate = RequestBodyLBS . encode $ PhoneUpdate phn + put (brig . path "/self/phone" . contentJson . zUser uid2 . zConn "c" . body phoneUpdate) + !!! (const 409 === statusCode) + testCreateAccountPendingActivationKey :: Opt.Opts -> Brig -> Http () testCreateAccountPendingActivationKey (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () testCreateAccountPendingActivationKey _ brig = do