diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be2efc067..a103065f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +# [2022-03-07] + +## Release notes + + +* For wire.com operators: make sure that nginz is deployed (#2166) + + +## API changes + + +* Add qualified broadcast endpoint (#2166) + + +## Bug fixes and other updates + + +* Always create spar credentials during SCIM provisioning when applicable (#2174) + + +## Internal changes + + +* Add tests for additional information returned by `GET /api-version` (#2159) + +* Clean up `Base64ByteString` implementation (#2170) + +* The `Event` record type does not contain a `type` field anymore (#2160) + +* Add MLS message types and corresponding deserialisers (#2145) + +* Servantify `POST /register` and `POST /i/users` endpoints (#2121) + + # [2022-03-01] ## Release notes diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index bbaa7171af..3885ccdf8c 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -204,7 +204,9 @@ data: suspendTimeout: {{ .setSuspendInactiveUsers.suspendTimeout }} {{- end }} setRichInfoLimit: {{ .setRichInfoLimit }} + {{- if .setDefaultUserLocale }} setDefaultUserLocale: {{ .setDefaultUserLocale }} + {{- end }} setMaxTeamSize: {{ .setMaxTeamSize }} setMaxConvSize: {{ .setMaxConvSize }} setEmailVisibility: {{ .setEmailVisibility }} diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 68306c486a..e24b6989d3 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -316,7 +316,7 @@ nginx_conf: - all max_body_size: 40m body_buffer_size: 256k - - path: /broadcast/otr/messages + - path: /broadcast envs: - all max_body_size: 40m diff --git a/deploy/services-demo/conf/nginz/nginx.conf b/deploy/services-demo/conf/nginz/nginx.conf index d7a3a6f413..3900df1863 100644 --- a/deploy/services-demo/conf/nginz/nginx.conf +++ b/deploy/services-demo/conf/nginz/nginx.conf @@ -336,7 +336,7 @@ http { proxy_pass http://galley; } - location /broadcast/otr/messages { + location /broadcast { include common_response_with_zauth.conf; proxy_pass http://galley; } diff --git a/docs/reference/config-options.md b/docs/reference/config-options.md index 649c9c90d1..3594f5ae3d 100644 --- a/docs/reference/config-options.md +++ b/docs/reference/config-options.md @@ -396,7 +396,7 @@ Additionally if `setSftListAllServers` is set to `enabled` (disabled by default) #### setDefaultLocale (deprecated / ignored) -The brig server config option `setDefaultLocale` has been replaced by `setDefaultUserLocale` and `setDefaultTemplateLocale`. Both settings are optional and `setDefaultTemplateLocale` defaults to `EN` and `setDefaultLocale` defaults to `setDefaultTemplateLocale`. If `setDefaultLocale` was not set or set to `EN` before this change, nothing needs to be done. If `setDefaultLocale` was set to any other language other than `EN` the name of the setting should be changed to `setDefaultTemplateLocale`. +The brig server config option `setDefaultLocale` has been replaced by `setDefaultUserLocale` and `setDefaultTemplateLocale`. Both settings are optional and `setDefaultTemplateLocale` defaults to `EN` and `setDefaultLocale` defaults to `setDefaultTemplateLocale`. If `setDefaultLocale` was not set or set to `EN` before this change, nothing needs to be done. If `setDefaultLocale` was set to any other language other than `EN` the name of the setting should be changed to `setDefaultTemplateLocale`. #### `setDefaultTemplateLocale` @@ -410,7 +410,7 @@ optSettings: #### `setDefaultUserLocale` -This option is the default user locale to be used if it is not set in the user profile. This can be the case if the users are provisioned by SCIM e.g. This option determines which language to use for email communication. If not set the default is the value that is configured for `setDefaultTemplateLocale`. +This option determines which language to use for email communication. It is the default value if none is given in the user profile, or if no user profile exists (eg., if user is being provisioned via SCIM or manual team invitation via the team management app). If not set, `setDefaultTemplateLocale` is used instead. ``` # [brig.yaml] @@ -433,7 +433,7 @@ optSettings: any key package whose expiry date is set further than 15 days after upload time will be rejected. -### Federated domain specific configuration settings +### Federated domain specific configuration settings #### Restrict user search The lookup and search of users on a wire instance can be configured. This can be done per federated domain. diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index c62f90bff4..38b76486f0 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE NumDecimals #-} {-# LANGUAGE TypeApplications #-} @@ -38,10 +39,12 @@ module Data.Json.Util -- * Base64 Base64ByteString (..), + base64Schema, + Base64ByteStringL (..), + base64SchemaL, fromBase64TextLenient, fromBase64Text, toBase64Text, - base64Schema, ) where @@ -52,12 +55,12 @@ import qualified Data.Aeson as A import qualified Data.Aeson.Types as A import qualified Data.Attoparsec.Text as Atto import qualified Data.Attoparsec.Time as Atto +import Data.Bifunctor import qualified Data.ByteString.Base64 as B64 -import qualified Data.ByteString.Base64.Lazy as B64L +import qualified Data.ByteString.Base64.URL as B64U import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Conversion as BS import qualified Data.ByteString.Lazy as L -import qualified Data.ByteString.Lazy.Char8 as L8 import Data.Fixed import Data.Schema import Data.String.Conversions (cs) @@ -70,6 +73,7 @@ import Data.Time.Format (formatTime, parseTimeM) import qualified Data.Time.Lens as TL import Data.Time.Locale.Compat (defaultTimeLocale) import Imports +import Servant import Test.QuickCheck (Arbitrary (arbitrary)) -- for UTCTime import Test.QuickCheck.Instances () @@ -174,39 +178,71 @@ toJSONFieldName = A.defaultOptions {A.fieldLabelModifier = A.camelTo2 '_' . drop dropPrefix = dropWhile (not . isUpper) -------------------------------------------------------------------------------- --- base64-encoded lazy bytestrings --- | Lazy 'ByteString' with base64 json encoding. Relevant discussion: --- . See test suite for more details. -newtype Base64ByteString = Base64ByteString {fromBase64ByteString :: L.ByteString} +-- | Base64-encoded strict 'ByteString'. +-- +-- For proper Swagger generation, avoid using this type directly in APIs. Instead, +-- use a plain 'ByteString' (or a more specific newtype wrapper), and construct +-- instances using @deriving via@. +-- +-- For URLs or HTTP headers, the base64url encoding is used. +-- +-- Some related discussion: . +newtype Base64ByteString = Base64ByteString {fromBase64ByteString :: ByteString} + deriving stock (Eq, Ord, Show) + deriving (FromJSON, ToJSON) via Schema Base64ByteString + deriving newtype (Arbitrary, IsString) + +instance ToSchema Base64ByteString where + schema = fromBase64ByteString .= fmap Base64ByteString base64SchemaN + +instance FromHttpApiData Base64ByteString where + parseUrlPiece = bimap Text.pack Base64ByteString . B64U.decode . Text.encodeUtf8 + +instance ToHttpApiData Base64ByteString where + toUrlPiece = Text.decodeUtf8With Text.lenientDecode . B64U.encode . fromBase64ByteString + +instance S.ToParamSchema Base64ByteString where + toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + +base64SchemaN :: ValueSchema NamedSwaggerDoc ByteString +base64SchemaN = toBase64Text .= parsedText "Base64ByteString" fromBase64Text + +base64Schema :: ValueSchema SwaggerDoc ByteString +base64Schema = unnamed base64SchemaN + +-------------------------------------------------------------------------------- + +-- | Base64-encoded lazy 'ByteString'. +-- Similar to 'Base64ByteString', but based on 'LByteString'. +newtype Base64ByteStringL = Base64ByteStringL {fromBase64ByteStringL :: LByteString} deriving (Eq, Show, Generic) + deriving (FromJSON, ToJSON) via Schema Base64ByteStringL + deriving newtype (Arbitrary, IsString) + +base64FromStrict :: Base64ByteString -> Base64ByteStringL +base64FromStrict = Base64ByteStringL . L.fromStrict . fromBase64ByteString + +base64ToStrict :: Base64ByteStringL -> Base64ByteString +base64ToStrict = Base64ByteString . L.toStrict . fromBase64ByteStringL + +instance ToSchema Base64ByteStringL where + schema = fromBase64ByteStringL .= fmap Base64ByteStringL base64SchemaLN + +instance FromHttpApiData Base64ByteStringL where + parseUrlPiece = fmap base64FromStrict . parseUrlPiece + +instance ToHttpApiData Base64ByteStringL where + toUrlPiece = toUrlPiece . base64ToStrict + +instance S.ToParamSchema Base64ByteStringL where + toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString + +base64SchemaLN :: ValueSchema NamedSwaggerDoc LByteString +base64SchemaLN = L.toStrict .= fmap L.fromStrict base64SchemaN -instance FromJSON Base64ByteString where - parseJSON (A.String st) = handleError . B64L.decode . stToLbs $ st - where - stToLbs = L.fromChunks . pure . Text.encodeUtf8 - handleError = - either - (const $ fail "parse Base64ByteString: invalid base64 encoding") - (pure . Base64ByteString) - parseJSON _ = fail "parse Base64ByteString: not a string" - -instance ToJSON Base64ByteString where - toJSON (Base64ByteString lbs) = A.String . lbsToSt . B64L.encode $ lbs - where - lbsToSt = - Text.decodeUtf8With Text.lenientDecode - . mconcat - . L.toChunks - -instance IsString Base64ByteString where - fromString = Base64ByteString . L8.pack - -instance Arbitrary Base64ByteString where - arbitrary = Base64ByteString <$> arbitrary - -base64Schema :: ValueSchema SwaggerDoc Base64ByteString -base64Schema = mkSchema mempty A.parseJSON (pure . A.toJSON) +base64SchemaL :: ValueSchema SwaggerDoc LByteString +base64SchemaL = unnamed base64SchemaLN -------------------------------------------------------------------------------- -- Utilities diff --git a/libs/types-common/test/Test/Properties.hs b/libs/types-common/test/Test/Properties.hs index cecb0794dd..9b5ba7880e 100644 --- a/libs/types-common/test/Test/Properties.hs +++ b/libs/types-common/test/Test/Properties.hs @@ -107,15 +107,15 @@ tests = \(c :: Char) -> Ascii.contains Ascii.Base64Url c ==> Ascii.contains Ascii.Standard c ], testGroup - "Base64ByteString" + "Base64ByteStringL" [ testProperty "validate (Aeson.decode . Aeson.encode) == pure . id" $ - \(Util.Base64ByteString . L.pack -> s) -> + \(Util.Base64ByteStringL . L.pack -> s) -> (Aeson.eitherDecode . Aeson.encode) s == Right s, -- the property only considers valid 'String's, and it does not document the encoding very -- well, so here are some unit tests (see -- http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt for more). testCase "examples" $ do - let go :: Util.Base64ByteString -> L.ByteString -> Assertion + let go :: Util.Base64ByteStringL -> L.ByteString -> Assertion go b uu = do Aeson.encode b @=? uu (Aeson.eitherDecode . Aeson.encode) b @=? Right b diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Event.hs b/libs/wire-api-federation/src/Wire/API/Federation/Event.hs deleted file mode 100644 index 2e62292524..0000000000 --- a/libs/wire-api-federation/src/Wire/API/Federation/Event.hs +++ /dev/null @@ -1,100 +0,0 @@ -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE StrictData #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Wire.API.Federation.Event - ( AnyEvent (..), - ConversationEvent (..), - - -- * MemberJoin - MemberJoin (..), - SimpleMember (..), - ConversationRole (..), - ) -where - -import Data.Aeson (FromJSON, ToJSON) -import Data.Id -import Data.Qualified (Qualified) -import Data.Time -import Imports -import Test.QuickCheck (Arbitrary (arbitrary)) -import qualified Test.QuickCheck as QC -import Wire.API.Util.Aeson (CustomEncoded (CustomEncoded)) - -data AnyEvent - = EventMemberJoin (ConversationEvent MemberJoin) - deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON) via (CustomEncoded AnyEvent) - --- | Similar to 'Wire.API.Event.ConversationEvent', but all IDs are qualified to allow --- this representation to be sent across backends. --- --- Also, instead of having a sum type in 'eventData', it allows specifying which type --- of event it is, e.g. @ConversationEvent MemberJoin@. --- To represent possiblity of multiple different event types, use a sum type around it. -data ConversationEvent a = ConversationEvent - { eventConversation :: Qualified ConvId, - eventFrom :: Qualified UserId, - eventTime :: UTCTime, - eventData :: a - } - deriving stock (Eq, Show, Generic, Foldable, Functor, Traversable) - deriving (ToJSON, FromJSON) via (CustomEncoded (ConversationEvent a)) - -newtype MemberJoin = MemberJoin - { smUsers :: [SimpleMember] - } - deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON) via (CustomEncoded MemberJoin) - -data SimpleMember = SimpleMember - { smId :: Qualified UserId, - smConversationRole :: ConversationRole - } - deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON) via (CustomEncoded SimpleMember) - -data ConversationRole - = ConversationRoleAdmin - | ConversationRoleMember - deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON) via (CustomEncoded ConversationRole) - --- Arbitrary - -instance Arbitrary AnyEvent where - arbitrary = - QC.oneof - [ EventMemberJoin <$> arbitrary - ] - -instance Arbitrary a => Arbitrary (ConversationEvent a) where - arbitrary = ConversationEvent <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary - -instance Arbitrary MemberJoin where - arbitrary = MemberJoin <$> arbitrary - -instance Arbitrary SimpleMember where - arbitrary = SimpleMember <$> arbitrary <*> arbitrary - -instance Arbitrary ConversationRole where - arbitrary = QC.elements [ConversationRoleAdmin, ConversationRoleMember] diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 0c2cc1db7f..20d673c962 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -28,7 +28,6 @@ library Wire.API.Federation.Domain Wire.API.Federation.Endpoint Wire.API.Federation.Error - Wire.API.Federation.Event other-modules: Paths_wire_api_federation hs-source-dirs: diff --git a/libs/wire-api/package.yaml b/libs/wire-api/package.yaml index 02215683ab..f3d687cb89 100644 --- a/libs/wire-api/package.yaml +++ b/libs/wire-api/package.yaml @@ -114,6 +114,7 @@ tests: - cassava - currency-codes - directory + - either - hex - iso3166-country-codes - iso639 @@ -123,6 +124,7 @@ tests: - pretty - proto-lens - QuickCheck + - schema-profunctor - string-conversions - swagger2 - tasty diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index cb8b5890cd..c89fb2db27 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -55,24 +55,24 @@ conversationActionToEvent :: ConversationAction -> Event conversationActionToEvent now quid qcnv (ConversationActionAddMembers newMembers role) = - Event MemberJoin qcnv quid now $ + Event qcnv quid now $ EdMembersJoin $ SimpleMembers (map (`SimpleMember` role) (toList newMembers)) conversationActionToEvent now quid qcnv (ConversationActionRemoveMembers removedMembers) = - Event MemberLeave qcnv quid now $ + Event qcnv quid now $ EdMembersLeave (QualifiedUserIdList (toList removedMembers)) conversationActionToEvent now quid qcnv (ConversationActionRename rename) = - Event ConvRename qcnv quid now (EdConvRename rename) + Event qcnv quid now (EdConvRename rename) conversationActionToEvent now quid qcnv (ConversationActionMessageTimerUpdate update) = - Event ConvMessageTimerUpdate qcnv quid now (EdConvMessageTimerUpdate update) + Event qcnv quid now (EdConvMessageTimerUpdate update) conversationActionToEvent now quid qcnv (ConversationActionReceiptModeUpdate update) = - Event ConvReceiptModeUpdate qcnv quid now (EdConvReceiptModeUpdate update) + Event qcnv quid now (EdConvReceiptModeUpdate update) conversationActionToEvent now quid qcnv (ConversationActionMemberUpdate target (OtherMemberUpdate role)) = let update = MemberUpdateData target Nothing Nothing Nothing Nothing Nothing Nothing role - in Event MemberStateUpdate qcnv quid now (EdMemberUpdate update) + in Event qcnv quid now (EdMemberUpdate update) conversationActionToEvent now quid qcnv (ConversationActionAccessUpdate update) = - Event ConvAccessUpdate qcnv quid now (EdConvAccessUpdate update) + Event qcnv quid now (EdConvAccessUpdate update) conversationActionToEvent now quid qcnv ConversationActionDelete = - Event ConvDelete qcnv quid now EdConvDelete + Event qcnv quid now EdConvDelete conversationActionTag :: Qualified UserId -> ConversationAction -> Action conversationActionTag _ (ConversationActionAddMembers _ _) = AddConversationMember diff --git a/libs/wire-api/src/Wire/API/ErrorDescription.hs b/libs/wire-api/src/Wire/API/ErrorDescription.hs index 49870a480f..73301bda8e 100644 --- a/libs/wire-api/src/Wire/API/ErrorDescription.hs +++ b/libs/wire-api/src/Wire/API/ErrorDescription.hs @@ -387,3 +387,29 @@ type MLSIdentityMismatch = 403 "mls-identity-mismatch" "Prekey credential does not match qualified client ID" + +type WhitelistError = ErrorDescription 403 "unauthorized" "Unauthorized e-mail address or phone number." + +type InvalidInvitationCode = ErrorDescription 400 "invalid-invitation-code" "Invalid invitation code." + +type MissingIdentity = ErrorDescription 403 "missing-identity" "Using an invitation code requires registering the given email and/or phone." + +type BlacklistedEmail = + ErrorDescription + 403 + "blacklisted-email" + "The given e-mail address has been blacklisted due to a permanent bounce \ + \or a complaint." + +type InvalidEmail = ErrorDescription 400 "invalid-email" "Invalid e-mail address." + +type InvalidActivationCode msg = ErrorDescription 404 "invalid-code" msg + +type InvalidActivationCodeWrongUser = InvalidActivationCode "User does not exist." + +type InvalidActivationCodeWrongCode = InvalidActivationCode "Invalid activation code" + +type TooManyTeamMembers = ErrorDescription 403 "too-many-team-members" "Too many members in this team." + +-- | docs/reference/user/registration.md {#RefRestrictRegistration}. +type UserCreationRestricted = ErrorDescription 403 "user-creation-restricted" "This instance does not allow creation of personal users or teams." diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 66a1b7e946..b39ea7ed9d 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -21,6 +21,7 @@ module Wire.API.Event.Conversation ( -- * Event Event (..), + evtType, EventType (..), EventData (..), AddCodeResult (..), @@ -107,14 +108,16 @@ import Wire.API.User (QualifiedUserIdList (..)) -- Event data Event = Event - { evtType :: EventType, - evtConv :: Qualified ConvId, + { evtConv :: Qualified ConvId, evtFrom :: Qualified UserId, evtTime :: UTCTime, evtData :: EventData } deriving stock (Eq, Show, Generic) +evtType :: Event -> EventType +evtType = eventDataType . evtData + modelEvent :: Doc.Model modelEvent = Doc.defineModel "Event" $ do Doc.description "Event data" @@ -146,7 +149,7 @@ modelEvent = Doc.defineModel "Event" $ do instance Arbitrary Event where arbitrary = do typ <- arbitrary - Event typ + Event <$> arbitrary <*> arbitrary <*> (milli <$> arbitrary) @@ -302,6 +305,22 @@ genEventData = \case OtrMessageAdd -> EdOtrMessage <$> arbitrary ConvDelete -> pure EdConvDelete +eventDataType :: EventData -> EventType +eventDataType (EdMembersJoin _) = MemberJoin +eventDataType (EdMembersLeave _) = MemberLeave +eventDataType (EdMemberUpdate _) = MemberStateUpdate +eventDataType (EdConvRename _) = ConvRename +eventDataType (EdConvAccessUpdate _) = ConvAccessUpdate +eventDataType (EdConvMessageTimerUpdate _) = ConvMessageTimerUpdate +eventDataType (EdConvCodeUpdate _) = ConvCodeUpdate +eventDataType EdConvCodeDelete = ConvCodeDelete +eventDataType (EdConnect _) = ConvConnect +eventDataType (EdConversation _) = ConvCreate +eventDataType (EdConvReceiptModeUpdate _) = ConvReceiptModeUpdate +eventDataType (EdTyping _) = Typing +eventDataType (EdOtrMessage _) = OtrMessageAdd +eventDataType EdConvDelete = ConvDelete + -------------------------------------------------------------------------------- -- Event data helpers @@ -547,7 +566,7 @@ eventObjectSchema = <*> evtFrom .= field "qualified_from" schema <*> (toUTCTimeMillis . evtTime) .= field "time" (fromUTCTimeMillis <$> schema) where - mk (ty, d) cid uid tm = Event ty cid uid tm d + mk (_, d) cid uid tm = Event cid uid tm d instance ToJSONObject Event where toJSONObject = diff --git a/libs/wire-api/src/Wire/API/Event/Team.hs b/libs/wire-api/src/Wire/API/Event/Team.hs index e18cde53a0..94c692703c 100644 --- a/libs/wire-api/src/Wire/API/Event/Team.hs +++ b/libs/wire-api/src/Wire/API/Event/Team.hs @@ -62,15 +62,17 @@ import Wire.API.Team.Permission (Permissions) -- Event data Event = Event - { _eventType :: EventType, - _eventTeam :: TeamId, + { _eventTeam :: TeamId, _eventTime :: UTCTime, - _eventData :: Maybe EventData + _eventData :: EventData } deriving stock (Eq, Show, Generic) -newEvent :: EventType -> TeamId -> UTCTime -> Event -newEvent typ tid tme = Event typ tid tme Nothing +eventType :: Event -> EventType +eventType = eventDataType . _eventData + +newEvent :: TeamId -> UTCTime -> EventData -> Event +newEvent = Event modelEvent :: Doc.Model modelEvent = Doc.defineModel "TeamEvent" $ do @@ -123,7 +125,7 @@ instance ToJSON Event where instance ToJSONObject Event where toJSONObject e = KeyMap.fromList - [ "type" .= _eventType e, + [ "type" .= eventType e, "team" .= _eventTeam e, "time" .= _eventTime e, "data" .= _eventData e @@ -133,7 +135,7 @@ instance FromJSON Event where parseJSON = withObject "event" $ \o -> do ty <- o .: "type" dt <- o .:? "data" - Event ty + Event <$> o .: "team" <*> o .: "time" <*> parseEventData ty dt @@ -141,7 +143,7 @@ instance FromJSON Event where instance Arbitrary Event where arbitrary = do typ <- arbitrary - Event typ + Event <$> arbitrary <*> arbitrary <*> genEventData typ @@ -200,6 +202,7 @@ instance FromJSON EventType where data EventData = EdTeamCreate Team + | EdTeamDelete | EdTeamUpdate TeamUpdateData | EdMemberJoin UserId | EdMemberLeave UserId @@ -210,6 +213,7 @@ data EventData instance ToJSON EventData where toJSON (EdTeamCreate tem) = toJSON tem + toJSON EdTeamDelete = Null toJSON (EdMemberJoin usr) = object ["user" .= usr] toJSON (EdMemberUpdate usr mPerm) = object $ @@ -221,43 +225,53 @@ instance ToJSON EventData where toJSON (EdConvDelete cnv) = object ["conv" .= cnv] toJSON (EdTeamUpdate upd) = toJSON upd -parseEventData :: EventType -> Maybe Value -> Parser (Maybe EventData) +eventDataType :: EventData -> EventType +eventDataType (EdTeamCreate _) = TeamCreate +eventDataType EdTeamDelete = TeamDelete +eventDataType (EdTeamUpdate _) = TeamUpdate +eventDataType (EdMemberJoin _) = MemberJoin +eventDataType (EdMemberLeave _) = MemberLeave +eventDataType (EdMemberUpdate _ _) = MemberUpdate +eventDataType (EdConvCreate _) = ConvCreate +eventDataType (EdConvDelete _) = ConvDelete + +parseEventData :: EventType -> Maybe Value -> Parser (EventData) parseEventData MemberJoin Nothing = fail "missing event data for type 'team.member-join'" parseEventData MemberJoin (Just j) = do - let f o = Just . EdMemberJoin <$> o .: "user" + let f o = EdMemberJoin <$> o .: "user" withObject "member join data" f j parseEventData MemberUpdate Nothing = fail "missing event data for type 'team.member-update" parseEventData MemberUpdate (Just j) = do - let f o = Just <$> (EdMemberUpdate <$> o .: "user" <*> o .:? "permissions") + let f o = EdMemberUpdate <$> o .: "user" <*> o .:? "permissions" withObject "member update data" f j parseEventData MemberLeave Nothing = fail "missing event data for type 'team.member-leave'" parseEventData MemberLeave (Just j) = do - let f o = Just . EdMemberLeave <$> o .: "user" + let f o = EdMemberLeave <$> o .: "user" withObject "member leave data" f j parseEventData ConvCreate Nothing = fail "missing event data for type 'team.conversation-create" parseEventData ConvCreate (Just j) = do - let f o = Just . EdConvCreate <$> o .: "conv" + let f o = EdConvCreate <$> o .: "conv" withObject "conversation create data" f j parseEventData ConvDelete Nothing = fail "missing event data for type 'team.conversation-delete" parseEventData ConvDelete (Just j) = do - let f o = Just . EdConvDelete <$> o .: "conv" + let f o = EdConvDelete <$> o .: "conv" withObject "conversation delete data" f j parseEventData TeamCreate Nothing = fail "missing event data for type 'team.create'" -parseEventData TeamCreate (Just j) = Just . EdTeamCreate <$> parseJSON j +parseEventData TeamCreate (Just j) = EdTeamCreate <$> parseJSON j parseEventData TeamUpdate Nothing = fail "missing event data for type 'team.update'" -parseEventData TeamUpdate (Just j) = Just . EdTeamUpdate <$> parseJSON j -parseEventData _ Nothing = pure Nothing +parseEventData TeamUpdate (Just j) = EdTeamUpdate <$> parseJSON j +parseEventData _ Nothing = pure EdTeamDelete parseEventData t (Just _) = fail $ "unexpected event data for type " <> show t -genEventData :: EventType -> QC.Gen (Maybe EventData) +genEventData :: EventType -> QC.Gen (EventData) genEventData = \case - TeamCreate -> Just . EdTeamCreate <$> arbitrary - TeamDelete -> pure Nothing - TeamUpdate -> Just . EdTeamUpdate <$> arbitrary - MemberJoin -> Just . EdMemberJoin <$> arbitrary - MemberLeave -> Just . EdMemberLeave <$> arbitrary - MemberUpdate -> Just <$> (EdMemberUpdate <$> arbitrary <*> arbitrary) - ConvCreate -> Just . EdConvCreate <$> arbitrary - ConvDelete -> Just . EdConvDelete <$> arbitrary + TeamCreate -> EdTeamCreate <$> arbitrary + TeamDelete -> pure EdTeamDelete + TeamUpdate -> EdTeamUpdate <$> arbitrary + MemberJoin -> EdMemberJoin <$> arbitrary + MemberLeave -> EdMemberLeave <$> arbitrary + MemberUpdate -> EdMemberUpdate <$> arbitrary <*> arbitrary + ConvCreate -> EdConvCreate <$> arbitrary + ConvDelete -> EdConvDelete <$> arbitrary makeLenses ''Event diff --git a/libs/wire-api/src/Wire/API/MLS/Commit.hs b/libs/wire-api/src/Wire/API/MLS/Commit.hs new file mode 100644 index 0000000000..22c14dda9d --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Commit.hs @@ -0,0 +1,55 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.MLS.Commit where + +import Imports +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Proposal +import Wire.API.MLS.Serialisation + +data Commit = Commit + { cProposals :: [ProposalOrRef], + cPath :: Maybe UpdatePath + } + +instance ParseMLS Commit where + parseMLS = Commit <$> parseMLSVector @Word32 parseMLS <*> parseMLSOptional parseMLS + +data UpdatePath = UpdatePath + { upLeaf :: KeyPackage, + upNodes :: [UpdatePathNode] + } + +instance ParseMLS UpdatePath where + parseMLS = UpdatePath <$> parseMLS <*> parseMLSVector @Word32 parseMLS + +data UpdatePathNode = UpdatePathNode + { upnPublicKey :: ByteString, + upnSecret :: [HPKECiphertext] + } + +instance ParseMLS UpdatePathNode where + parseMLS = UpdatePathNode <$> parseMLSBytes @Word16 <*> parseMLSVector @Word32 parseMLS + +data HPKECiphertext = HPKECiphertext + { hcOutput :: ByteString, + hcCiphertext :: ByteString + } + +instance ParseMLS HPKECiphertext where + parseMLS = HPKECiphertext <$> parseMLSBytes @Word16 <*> parseMLSBytes @Word16 diff --git a/libs/wire-api/src/Wire/API/MLS/Credential.hs b/libs/wire-api/src/Wire/API/MLS/Credential.hs index 2922ca76e4..2db0616336 100644 --- a/libs/wire-api/src/Wire/API/MLS/Credential.hs +++ b/libs/wire-api/src/Wire/API/MLS/Credential.hs @@ -43,21 +43,20 @@ data Credential = BasicCredential deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform Credential -data CredentialTag = ReservedCredentialTag | BasicCredentialTag - deriving stock (Enum, Bounded, Show) - deriving (ParseMLS) via (EnumMLS Word16 CredentialTag) +data CredentialTag = BasicCredentialTag + deriving stock (Enum, Bounded, Eq, Show) + +instance ParseMLS CredentialTag where + parseMLS = parseMLSEnum @Word16 "credential type" instance ParseMLS Credential where - parseMLS = do - tag <- parseMLS - case tag of + parseMLS = + parseMLS >>= \case BasicCredentialTag -> BasicCredential <$> parseMLSBytes @Word16 <*> parseMLS <*> parseMLSBytes @Word16 - ReservedCredentialTag -> - fail "Unexpected credential type" credentialTag :: Credential -> CredentialTag credentialTag (BasicCredential _ _ _) = BasicCredentialTag diff --git a/libs/wire-api/src/Wire/API/MLS/Group.hs b/libs/wire-api/src/Wire/API/MLS/Group.hs new file mode 100644 index 0000000000..f7a5d9d824 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Group.hs @@ -0,0 +1,30 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.MLS.Group where + +import Imports +import Wire.API.MLS.Serialisation + +newtype GroupId = GroupId {unGroupId :: ByteString} + deriving (Eq, Show) + +instance IsString GroupId where + fromString = GroupId . fromString + +instance ParseMLS GroupId where + parseMLS = GroupId <$> parseMLSBytes @Word8 diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index 6c1958f281..c8e7a88347 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -40,7 +40,6 @@ module Wire.API.MLS.KeyPackage decodeExtension, parseExtension, ExtensionTag (..), - ReservedExtensionTagSym0, CapabilitiesExtensionTagSym0, LifetimeExtensionTagSym0, SExtensionTag (..), @@ -56,7 +55,6 @@ module Wire.API.MLS.KeyPackage where import Control.Applicative -import Control.Error.Util import Control.Lens hiding (set, (.=)) import Data.Aeson (FromJSON, ToJSON) import Data.Binary @@ -93,7 +91,7 @@ instance ToSchema KeyPackageData where schema = (S.schema . S.example ?~ "a2V5IHBhY2thZ2UgZGF0YQo=") ( KeyPackageData <$> kpData - .= named "KeyPackage" (Base64ByteString .= fmap fromBase64ByteString base64Schema) + .= named "KeyPackage" base64SchemaL ) data KeyPackageBundleEntry = KeyPackageBundleEntry @@ -156,20 +154,17 @@ instance ParseMLS Extension where parseMLS = Extension <$> parseMLS <*> parseMLSBytes @Word32 data ExtensionTag - = ReservedExtensionTag - | CapabilitiesExtensionTag + = CapabilitiesExtensionTag | LifetimeExtensionTag deriving (Bounded, Enum) $(genSingletons [''ExtensionTag]) type family ExtensionType (t :: ExtensionTag) :: * where - ExtensionType 'ReservedExtensionTag = () ExtensionType 'CapabilitiesExtensionTag = Capabilities ExtensionType 'LifetimeExtensionTag = Lifetime parseExtension :: Sing t -> Get (ExtensionType t) -parseExtension SReservedExtensionTag = pure () parseExtension SCapabilitiesExtensionTag = parseMLS parseExtension SLifetimeExtensionTag = parseMLS @@ -182,16 +177,16 @@ instance Eq SomeExtension where _ == _ = False instance Show SomeExtension where - show (SomeExtension SReservedExtensionTag _) = show () show (SomeExtension SCapabilitiesExtensionTag caps) = show caps show (SomeExtension SLifetimeExtensionTag lt) = show lt -decodeExtension :: Extension -> Maybe SomeExtension +decodeExtension :: Extension -> Either Text (Maybe SomeExtension) decodeExtension e = do - t <- safeToEnum (fromIntegral (extType e)) - hush $ - withSomeSing t $ \st -> - decodeMLSWith' (SomeExtension st <$> parseExtension st) (extData e) + case toMLSEnum' (extType e) of + Left MLSEnumUnkonwn -> pure Nothing + Left MLSEnumInvalid -> Left "Invalid extension type" + Right t -> withSomeSing t $ \st -> + Just <$> decodeMLSWith' (SomeExtension st <$> parseExtension st) (extData e) data Capabilities = Capabilities { capVersions :: [ProtocolVersion], @@ -234,7 +229,7 @@ data KeyPackageTBS = KeyPackageTBS kpCredential :: Credential, kpExtensions :: [Extension] } - deriving stock (Show, Generic) + deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform KeyPackageTBS instance ParseMLS KeyPackageTBS where @@ -250,10 +245,13 @@ data KeyPackage = KeyPackage { kpTBS :: KeyPackageTBS, kpSignature :: ByteString } - deriving (Show) + deriving stock (Eq, Show) newtype KeyPackageRef = KeyPackageRef {unKeyPackageRef :: ByteString} - deriving stock (Show) + deriving stock (Eq, Show) + +instance ParseMLS KeyPackageRef where + parseMLS = KeyPackageRef <$> getByteString 16 kpRef :: CipherSuiteTag -> KeyPackageData -> KeyPackageRef kpRef cs = diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs new file mode 100644 index 0000000000..03d37e407f --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -0,0 +1,154 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE StandaloneKindSignatures #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.MLS.Message + ( Message (..), + WireFormatTag (..), + SWireFormatTag (..), + SomeMessage (..), + ContentType (..), + MessagePayload (..), + MessagePayloadTBS (..), + Sender (..), + MLSPlainTextSym0, + MLSCipherTextSym0, + ) +where + +import Data.Binary +import Data.Singletons.TH +import Imports +import Wire.API.MLS.Commit +import Wire.API.MLS.Group +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Proposal +import Wire.API.MLS.Serialisation + +data WireFormatTag = MLSPlainText | MLSCipherText + deriving (Bounded, Enum, Eq, Show) + +$(genSingletons [''WireFormatTag]) + +instance ParseMLS WireFormatTag where + parseMLS = parseMLSEnum @Word8 "wire format" + +data Message (tag :: WireFormatTag) = Message + { msgGroupId :: GroupId, + msgEpoch :: Word64, + msgAuthData :: ByteString, + msgSender :: Sender tag, + msgPayload :: MessagePayload tag + } + +instance ParseMLS (Message 'MLSPlainText) where + parseMLS = do + g <- parseMLS + e <- parseMLS + s <- parseMLS + d <- parseMLSBytes @Word32 + p <- parseMLS + pure (Message g e d s p) + +instance ParseMLS (Message 'MLSCipherText) where + parseMLS = do + g <- parseMLS + e <- parseMLS + ct <- parseMLS + d <- parseMLSBytes @Word32 + s <- parseMLS + p <- parseMLSBytes @Word32 + pure $ Message g e d s (CipherText ct p) + +data SomeMessage where + SomeMessage :: Sing tag -> Message tag -> SomeMessage + +instance ParseMLS SomeMessage where + parseMLS = + parseMLS >>= \case + MLSPlainText -> SomeMessage SMLSPlainText <$> parseMLS + MLSCipherText -> SomeMessage SMLSCipherText <$> parseMLS + +data family Sender (tag :: WireFormatTag) :: * + +data instance Sender 'MLSCipherText = EncryptedSender {esData :: ByteString} + +instance ParseMLS (Sender 'MLSCipherText) where + parseMLS = EncryptedSender <$> parseMLSBytes @Word8 + +data SenderTag = MemberSenderTag | PreconfiguredSenderTag | NewMemberSenderTag + deriving (Bounded, Enum, Show, Eq) + +instance ParseMLS SenderTag where + parseMLS = parseMLSEnum @Word8 "sender type" + +data instance Sender 'MLSPlainText + = MemberSender KeyPackageRef + | PreconfiguredSender ByteString + | NewMemberSender + +instance ParseMLS (Sender 'MLSPlainText) where + parseMLS = + parseMLS >>= \case + MemberSenderTag -> MemberSender <$> parseMLS + PreconfiguredSenderTag -> PreconfiguredSender <$> parseMLSBytes @Word8 + NewMemberSenderTag -> pure NewMemberSender + +data family MessagePayload (tag :: WireFormatTag) :: * + +data instance MessagePayload 'MLSCipherText = CipherText + { msgContentType :: Word8, + msgCipherText :: ByteString + } + +data instance MessagePayload 'MLSPlainText = MessagePayload + { msgTBS :: MessagePayloadTBS, + msgSignature :: ByteString, + msgConfirmation :: Maybe ByteString, + msgMembership :: Maybe ByteString + } + +instance ParseMLS (MessagePayload 'MLSPlainText) where + parseMLS = + MessagePayload + <$> parseMLS + <*> parseMLSBytes @Word16 + <*> parseMLSOptional (parseMLSBytes @Word8) + <*> parseMLSOptional (parseMLSBytes @Word8) + +data MessagePayloadTBS + = ApplicationMessage ByteString + | ProposalMessage Proposal + | CommitMessage Commit + +data ContentType + = ApplicationMessageTag + | ProposalMessageTag + | CommitMessageTag + deriving (Bounded, Enum, Eq, Show) + +instance ParseMLS ContentType where + parseMLS = parseMLSEnum @Word8 "content type" + +instance ParseMLS MessagePayloadTBS where + parseMLS = + parseMLS >>= \case + ApplicationMessageTag -> ApplicationMessage <$> parseMLSBytes @Word32 + ProposalMessageTag -> ProposalMessage <$> parseMLS + CommitMessageTag -> CommitMessage <$> parseMLS diff --git a/libs/wire-api/src/Wire/API/MLS/Proposal.hs b/libs/wire-api/src/Wire/API/MLS/Proposal.hs index abfc1553eb..801ff69bbf 100644 --- a/libs/wire-api/src/Wire/API/MLS/Proposal.hs +++ b/libs/wire-api/src/Wire/API/MLS/Proposal.hs @@ -18,20 +18,130 @@ module Wire.API.MLS.Proposal where import Data.Binary +import Data.Binary.Get import Imports import Wire.API.Arbitrary +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Group +import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation -data ProposalType - = AddProposal - | UpdateProposal - | RemoveProposal - | PreSharedKeyProposal - | ReInitProposal - | ExternalInitProposal - | AppAckProposal - | GroupContextExtensionsProposal - | ExternalProposal +data ProposalTag + = AddProposalTag + | UpdateProposalTag + | RemoveProposalTag + | PreSharedKeyProposalTag + | ReInitProposalTag + | ExternalInitProposalTag + | AppAckProposalTag + | GroupContextExtensionsProposalTag deriving stock (Bounded, Enum, Eq, Generic, Show) - deriving (ParseMLS) via (EnumMLS Word16 ProposalType) - deriving (Arbitrary) via GenericUniform ProposalType + deriving (Arbitrary) via GenericUniform ProposalTag + +instance ParseMLS ProposalTag where + parseMLS = parseMLSEnum @Word16 "proposal type" + +data Proposal + = AddProposal KeyPackage + | UpdateProposal KeyPackage + | RemoveProposal KeyPackageRef + | PreSharedKeyProposal PreSharedKeyID + | ReInitProposal ReInit + | ExternalInitProposal ByteString + | AppAckProposal [MessageRange] + | GroupContextExtensionsProposal [Extension] + deriving stock (Eq, Show) + +instance ParseMLS Proposal where + parseMLS = + parseMLS >>= \case + AddProposalTag -> AddProposal <$> parseMLS + UpdateProposalTag -> UpdateProposal <$> parseMLS + RemoveProposalTag -> RemoveProposal <$> parseMLS + PreSharedKeyProposalTag -> PreSharedKeyProposal <$> parseMLS + ReInitProposalTag -> ReInitProposal <$> parseMLS + ExternalInitProposalTag -> ExternalInitProposal <$> parseMLSBytes @Word16 + AppAckProposalTag -> AppAckProposal <$> parseMLSVector @Word32 parseMLS + GroupContextExtensionsProposalTag -> + GroupContextExtensionsProposal <$> parseMLSVector @Word32 parseMLS + +data PreSharedKeyTag = ExternalKeyTag | ResumptionKeyTag + deriving (Bounded, Enum, Eq, Show) + +instance ParseMLS PreSharedKeyTag where + parseMLS = parseMLSEnum @Word16 "PreSharedKeyID type" + +data PreSharedKeyID = ExternalKeyID ByteString | ResumptionKeyID Resumption + deriving stock (Eq, Show) + +instance ParseMLS PreSharedKeyID where + parseMLS = do + t <- parseMLS + case t of + ExternalKeyTag -> ExternalKeyID <$> parseMLSBytes @Word8 + ResumptionKeyTag -> ResumptionKeyID <$> parseMLS + +data Resumption = Resumption + { resUsage :: Word8, + resGroupId :: GroupId, + resEpoch :: Word64 + } + deriving stock (Eq, Show) + +instance ParseMLS Resumption where + parseMLS = + Resumption + <$> parseMLS + <*> parseMLS + <*> parseMLS + +data ReInit = ReInit + { riGroupId :: GroupId, + riProtocolVersion :: ProtocolVersion, + riCipherSuite :: CipherSuite, + riExtensions :: [Extension] + } + deriving stock (Eq, Show) + +instance ParseMLS ReInit where + parseMLS = + ReInit + <$> parseMLS + <*> parseMLS + <*> parseMLS + <*> parseMLSVector @Word32 parseMLS + +data MessageRange = MessageRange + { mrSender :: KeyPackageRef, + mrFirstGeneration :: Word32, + mrLastGenereation :: Word32 + } + deriving stock (Eq, Show) + +instance ParseMLS MessageRange where + parseMLS = + MessageRange + <$> parseMLS + <*> parseMLS + <*> parseMLS + +data ProposalOrRefTag = InlineTag | RefTag + deriving stock (Bounded, Enum, Eq, Show) + +instance ParseMLS ProposalOrRefTag where + parseMLS = parseMLSEnum @Word8 "ProposalOrRef type" + +data ProposalOrRef = Inline Proposal | Ref ProposalRef + deriving stock (Eq, Show) + +instance ParseMLS ProposalOrRef where + parseMLS = + parseMLS >>= \case + InlineTag -> Inline <$> parseMLS + RefTag -> Ref <$> parseMLS + +newtype ProposalRef = ProposalRef {unProposalRef :: ByteString} + deriving stock (Eq, Show) + +instance ParseMLS ProposalRef where + parseMLS = ProposalRef <$> getByteString 16 diff --git a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs index f773e8fa2e..cc6db424ef 100644 --- a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs @@ -19,9 +19,13 @@ module Wire.API.MLS.Serialisation ( ParseMLS (..), parseMLSVector, parseMLSBytes, + parseMLSOptional, + parseMLSEnum, BinaryMLS (..), - EnumMLS (..), - safeToEnum, + MLSEnumError (..), + fromMLSEnum, + toMLSEnum', + toMLSEnum, decodeMLS, decodeMLS', decodeMLSWith, @@ -58,6 +62,40 @@ parseMLSBytes = do len <- fromIntegral <$> get @w getByteString len +parseMLSOptional :: Get a -> Get (Maybe a) +parseMLSOptional g = do + b <- getWord8 + sequenceA $ guard (b /= 0) $> g + +-- | Parse a positive tag for an enumeration. The value 0 is considered +-- "reserved", and all other values are shifted down by 1 to get the +-- corresponding enumeration index. This makes it possible to parse enumeration +-- types that don't contain an explicit constructor for a "reserved" value. +parseMLSEnum :: + forall (w :: *) a. + (Bounded a, Enum a, Integral w, Binary w) => + String -> + Get a +parseMLSEnum name = toMLSEnum name =<< get @w + +data MLSEnumError = MLSEnumUnkonwn | MLSEnumInvalid + +toMLSEnum' :: forall a w. (Bounded a, Enum a, Integral w) => w -> Either MLSEnumError a +toMLSEnum' w = case fromIntegral w - 1 of + n + | n < 0 -> Left MLSEnumInvalid + | n < fromEnum @a minBound || n > fromEnum @a maxBound -> Left MLSEnumUnkonwn + | otherwise -> pure (toEnum n) + +toMLSEnum :: forall a w f. (Bounded a, Enum a, MonadFail f, Integral w) => String -> w -> f a +toMLSEnum name = either err pure . toMLSEnum' + where + err MLSEnumUnkonwn = fail $ "Unknown " <> name + err MLSEnumInvalid = fail $ "Invalid " <> name + +fromMLSEnum :: (Integral w, Enum a) => a -> w +fromMLSEnum = fromIntegral . succ . fromEnum + instance ParseMLS Word8 where parseMLS = get instance ParseMLS Word16 where parseMLS = get @@ -72,21 +110,6 @@ newtype BinaryMLS a = BinaryMLS a instance Binary a => ParseMLS (BinaryMLS a) where parseMLS = BinaryMLS <$> get --- | A wrapper to generate a 'Binary' instance for an enumerated type. -newtype EnumMLS w a = EnumMLS {unEnumMLS :: a} - -safeToEnum :: forall a f. (Bounded a, Enum a, MonadFail f) => Int -> f a -safeToEnum n - | n >= fromEnum @a minBound && n <= fromEnum @a maxBound = - pure (toEnum n) - | otherwise = - fail "Out of bound enumeration" - -instance (Binary w, Integral w, Bounded a, Enum a) => ParseMLS (EnumMLS w a) where - parseMLS = do - n <- fromIntegral <$> get @w - EnumMLS <$> safeToEnum n - -- | Decode an MLS value from a lazy bytestring. Return an error message in case of failure. decodeMLS :: ParseMLS a => LByteString -> Either Text a decodeMLS = decodeMLSWith parseMLS diff --git a/libs/wire-api/src/Wire/API/MLS/Welcome.hs b/libs/wire-api/src/Wire/API/MLS/Welcome.hs new file mode 100644 index 0000000000..76166969f4 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Welcome.hs @@ -0,0 +1,47 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.MLS.Welcome where + +import Imports +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Commit +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Serialisation + +data Welcome = Welcome + { welCipherSuite :: CipherSuite, + welSecrets :: [GroupSecrets], + welGroupInfo :: ByteString + } + +instance ParseMLS Welcome where + parseMLS = + Welcome + -- Note: the extra protocol version at the beginning of the welcome + -- message is present in openmls-0.4.0-pre, but is not part of the spec + <$> (parseMLS @ProtocolVersion *> parseMLS) + <*> parseMLSVector @Word32 parseMLS + <*> parseMLSBytes @Word32 + +data GroupSecrets = GroupSecrets + { gsNewMember :: KeyPackageRef, + gsSecrets :: HPKECiphertext + } + +instance ParseMLS GroupSecrets where + parseMLS = GroupSecrets <$> parseMLS <*> parseMLS diff --git a/libs/wire-api/src/Wire/API/Message.hs b/libs/wire-api/src/Wire/API/Message.hs index f734a162de..7c09431c75 100644 --- a/libs/wire-api/src/Wire/API/Message.hs +++ b/libs/wire-api/src/Wire/API/Message.hs @@ -80,7 +80,7 @@ import qualified Data.ProtocolBuffers as Protobuf import Data.Qualified (Qualified (..)) import Data.SOP (I (..), NS (..), unI, unZ) import Data.Schema -import Data.Serialize (runGetLazy) +import Data.Serialize (runGet) import qualified Data.Set as Set import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc @@ -157,7 +157,7 @@ instance ToSchema NewOtrMessage where <*> newOtrReportMissing .= maybe_ (optField "report_missing" (array schema)) instance FromProto NewOtrMessage where - fromProto bs = protoToNewOtrMessage <$> runGetLazy Protobuf.decodeMessage bs + fromProto bs = protoToNewOtrMessage <$> runGet Protobuf.decodeMessage bs protoToNewOtrMessage :: Proto.NewOtrMessage -> NewOtrMessage protoToNewOtrMessage msg = @@ -198,10 +198,10 @@ instance S.ToSchema QualifiedNewOtrMessage where \https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." instance FromProto QualifiedNewOtrMessage where - fromProto bs = protolensToQualifiedNewOtrMessage =<< ProtoLens.decodeMessage (LBS.toStrict bs) + fromProto bs = protolensToQualifiedNewOtrMessage =<< ProtoLens.decodeMessage bs instance ToProto QualifiedNewOtrMessage where - toProto = LBS.fromStrict . ProtoLens.encodeMessage . qualifiedNewOtrMessageToProto + toProto = ProtoLens.encodeMessage . qualifiedNewOtrMessageToProto protolensToQualifiedNewOtrMessage :: Proto.Otr.QualifiedNewOtrMessage -> Either String QualifiedNewOtrMessage protolensToQualifiedNewOtrMessage protoMsg = do diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 8665d12ca7..e8747fb173 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -17,6 +17,8 @@ module Wire.API.Routes.Internal.Brig ( API, + EJPD_API, + AccountAPI, EJPDRequest, GetAccountFeatureConfig, PutAccountFeatureConfig, @@ -39,7 +41,10 @@ import Servant.Swagger.UI import Wire.API.Connection import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Brig.EJPD +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named import qualified Wire.API.Team.Feature as ApiFt +import Wire.API.User type EJPDRequest = Summary @@ -109,15 +114,29 @@ type GetAllConnections = :> ReqBody '[Servant.JSON] ConnectionsStatusRequestV2 :> Post '[Servant.JSON] [ConnectionStatusV2] +type EJPD_API = + ( EJPDRequest + :<|> GetAccountFeatureConfig + :<|> PutAccountFeatureConfig + :<|> DeleteAccountFeatureConfig + :<|> GetAllConnectionsUnqualified + :<|> GetAllConnections + ) + +type AccountAPI = + -- This endpoint can lead to the following events being sent: + -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID + -- - UserIdentityUpdated event to created user, if email or phone get activated + Named + "createUserNoVerify" + ( "users" + :> ReqBody '[Servant.JSON] NewUser + :> MultiVerb 'POST '[Servant.JSON] RegisterInternalResponses (Either RegisterError SelfProfile) + ) + type API = "i" - :> ( EJPDRequest - :<|> GetAccountFeatureConfig - :<|> PutAccountFeatureConfig - :<|> DeleteAccountFeatureConfig - :<|> GetAllConnectionsUnqualified - :<|> GetAllConnections - ) + :> (EJPD_API :<|> AccountAPI) type SwaggerDocsAPI = "api" :> "internal" :> SwaggerSchemaUI "swagger-ui" "swagger.json" diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index 9688ef75f1..4e132b7f76 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -22,6 +22,7 @@ import Data.Proxy import GHC.TypeLits import Imports import Servant +import Servant.Client import Servant.Swagger newtype Named named x = Named {unnamed :: x} @@ -40,6 +41,11 @@ instance HasServer api ctx => HasServer (Named name api) ctx where instance RoutesToPaths api => RoutesToPaths (Named name api) where getRoutes = getRoutes @api +instance HasClient m api => HasClient m (Named n api) where + type Client m (Named n api) = Client m api + clientWithRoute pm _ req = clientWithRoute pm (Proxy @api) req + hoistClientMonad pm _ f = hoistClientMonad pm (Proxy @api) f + type family FindName n (api :: *) :: (n, *) where FindName n (Named name api) = '(name, api) FindName n (x :> api) = AddPrefix x (FindName n api) 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 9181305a43..83463b2133 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -312,6 +312,24 @@ type SelfAPI = :> MultiVerb 'PUT '[JSON] ChangeHandleResponses (Maybe ChangeHandleError) ) +type AccountAPI = + -- docs/reference/user/registration.md {#RefRegistration} + -- + -- This endpoint can lead to the following events being sent: + -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID + -- - UserIdentityUpdated event to created user, if email code or phone code is provided + Named + "register" + ( Summary "Register a new user." + :> Description + "If the environment where the registration takes \ + \place is private and a registered email address or phone \ + \number is not whitelisted, a 403 error is returned." + :> "register" + :> ReqBody '[JSON] NewUserPublic + :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess) + ) + type PrekeyAPI = Named "get-users-prekeys-client-unqualified" @@ -714,6 +732,7 @@ type MLSAPI = LiftNamed (ZLocalUser :> "mls" :> MLSKeyPackageAPI) type BrigAPI = UserAPI :<|> SelfAPI + :<|> AccountAPI :<|> ClientAPI :<|> PrekeyAPI :<|> UserClientAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index bd4c46d60f..b1c5b1f97f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -880,6 +880,25 @@ type MessagingAPI = (PostOtrResponses MessageSendingStatus) (Either (MessageNotSent MessageSendingStatus) MessageSendingStatus) ) + :<|> Named + "post-proteus-broadcast" + ( Summary "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + :> Description PostOtrDescription + :> ZLocalUser + :> ZConn + :> CanThrow TeamNotFound + :> CanThrow BroadcastLimitExceeded + :> CanThrow NonBindingTeam + :> "broadcast" + :> "proteus" + :> "messages" + :> ReqBody '[Proto] QualifiedNewOtrMessage + :> MultiVerb + 'POST + '[JSON] + (PostOtrResponses MessageSendingStatus) + (Either (MessageNotSent MessageSendingStatus) MessageSendingStatus) + ) type BotAPI = Named @@ -1048,7 +1067,7 @@ type PostOtrDescription = \- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n\ \- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\ \\n\ - \The sending of messages in a federated conversation could theorectically fail partially. \ + \The sending of messages in a federated conversation could theoretically fail partially. \ \To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. \ \So, if any backend is down, the message is not propagated to anyone. \ \But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, \ diff --git a/libs/wire-api/src/Wire/API/ServantProto.hs b/libs/wire-api/src/Wire/API/ServantProto.hs index aac4230d70..226c94f891 100644 --- a/libs/wire-api/src/Wire/API/ServantProto.hs +++ b/libs/wire-api/src/Wire/API/ServantProto.hs @@ -17,6 +17,7 @@ module Wire.API.ServantProto where +import qualified Data.ByteString.Lazy as LBS import Data.List.NonEmpty (NonEmpty (..)) import Data.Swagger import Imports @@ -34,22 +35,22 @@ data Proto -- it is fairly difficult to keep our custom data type, e.g. in -- Wire.API.Message.Proto in sync with the proto files. class FromProto a where - fromProto :: LByteString -> Either String a + fromProto :: ByteString -> Either String a class ToProto a where - toProto :: a -> LByteString + toProto :: a -> ByteString instance Accept Proto where contentTypes _ = ("application" // "x-protobuf") :| [] instance FromProto a => MimeUnrender Proto a where - mimeUnrender _ bs = fromProto bs + mimeUnrender _ bs = fromProto (LBS.toStrict bs) -- | This wrapper can be used to get the raw protobuf representation of a type. -- It is used when the protobuf is supposed to be forwarded somewhere like a -- federated remote, this saves us from having to re-encode it. data RawProto a = RawProto - { rpRaw :: LByteString, + { rpRaw :: ByteString, rpValue :: a } diff --git a/libs/wire-api/src/Wire/API/Swagger.hs b/libs/wire-api/src/Wire/API/Swagger.hs index 4fbc4ed77c..89cfe1f3f4 100644 --- a/libs/wire-api/src/Wire/API/Swagger.hs +++ b/libs/wire-api/src/Wire/API/Swagger.hs @@ -113,7 +113,6 @@ models = Push.Token.modelPushTokenList, Team.modelTeam, Team.modelTeamList, - Team.modelNewBindingTeam, Team.modelNewNonBindingTeam, Team.modelUpdateData, Team.modelTeamDelete, @@ -141,7 +140,6 @@ models = Team.SearchVisibility.modelTeamSearchVisibility, User.modelUserIdList, User.modelUser, - User.modelNewUser, User.modelEmailUpdate, User.modelDelete, User.modelVerifyDelete, diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index 5b3563e181..8fb331559c 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -39,6 +39,7 @@ module Wire.API.Team -- * NewTeam BindingNewTeam (..), + bindingNewTeamObjectSchema, NonBindingNewTeam (..), NewTeam (..), newNewTeam, @@ -62,7 +63,6 @@ module Wire.API.Team -- * Swagger modelTeam, modelTeamList, - modelNewBindingTeam, modelNewNonBindingTeam, modelUpdateData, modelTeamDelete, @@ -181,24 +181,14 @@ newtype BindingNewTeam = BindingNewTeam (NewTeam ()) deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema BindingNewTeam) -modelNewBindingTeam :: Doc.Model -modelNewBindingTeam = Doc.defineModel "NewBindingTeam" $ do - Doc.description "Required data when creating new teams" - Doc.property "name" Doc.string' $ - Doc.description "team name" - Doc.property "icon" Doc.string' $ - Doc.description "team icon (asset ID)" - Doc.property "icon_key" Doc.string' $ do - Doc.description "team icon asset key" - Doc.optional - instance ToSchema BindingNewTeam where - schema = BindingNewTeam <$> unwrap .= newTeamSchema "BindingNewTeam" sch - where - unwrap (BindingNewTeam nt) = nt + schema = object "BindingNewTeam" bindingNewTeamObjectSchema - sch :: ValueSchema SwaggerDoc () - sch = null_ +bindingNewTeamObjectSchema :: ObjectSchema SwaggerDoc BindingNewTeam +bindingNewTeamObjectSchema = + BindingNewTeam <$> unwrap .= newTeamObjectSchema null_ + where + unwrap (BindingNewTeam nt) = nt -- FUTUREWORK: since new team members do not get serialized, we zero them here. -- it may be worth looking into how this can be solved in the types. @@ -214,7 +204,10 @@ newtype NonBindingNewTeam = NonBindingNewTeam (NewTeam (Range 1 127 [TeamMember] deriving (FromJSON, ToJSON, S.ToSchema) via (Schema NonBindingNewTeam) instance ToSchema NonBindingNewTeam where - schema = NonBindingNewTeam <$> unwrap .= newTeamSchema "NonBindingNewTeam" sch + schema = + object "NonBindingNewTeam" $ + NonBindingNewTeam + <$> unwrap .= newTeamObjectSchema sch where unwrap (NonBindingNewTeam nt) = nt @@ -247,14 +240,13 @@ data NewTeam a = NewTeam newNewTeam :: Range 1 256 Text -> Range 1 256 Text -> NewTeam a newNewTeam nme ico = NewTeam nme ico Nothing Nothing -newTeamSchema :: HasSchemaRef d => Text -> ValueSchema d a -> ValueSchema NamedSwaggerDoc (NewTeam a) -newTeamSchema name sch = - object name $ - NewTeam - <$> _newTeamName .= field "name" schema - <*> _newTeamIcon .= field "icon" schema - <*> _newTeamIconKey .= maybe_ (optField "icon_key" schema) - <*> _newTeamMembers .= maybe_ (optField "members" sch) +newTeamObjectSchema :: ValueSchema SwaggerDoc a -> ObjectSchema SwaggerDoc (NewTeam a) +newTeamObjectSchema sch = + NewTeam + <$> _newTeamName .= field "name" schema + <*> _newTeamIcon .= field "icon" schema + <*> _newTeamIconKey .= maybe_ (optField "icon_key" schema) + <*> _newTeamMembers .= maybe_ (optField "members" sch) -------------------------------------------------------------------------------- -- TeamUpdateData diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 92aa3b062c..2c7b97a880 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -263,7 +263,7 @@ typeTeamFeatureName = Doc.string . Doc.enum $ cs . toByteString' <$> [(minBound data TeamFeatureStatusValue = TeamFeatureEnabled | TeamFeatureDisabled - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Ord, Enum, Bounded, Show, Generic) deriving (Arbitrary) via (GenericUniform TeamFeatureStatusValue) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema TeamFeatureStatusValue) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index f097607fd7..148fd4a702 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -37,6 +37,10 @@ module Wire.API.User -- * NewUser NewUserPublic (..), + RegisterError (..), + RegisterSuccess (..), + RegisterResponses, + RegisterInternalResponses, NewUser (..), emptyNewUser, ExpiresIn, @@ -83,9 +87,6 @@ module Wire.API.User -- * List Users ListUsersQuery (..), - -- * helpers - parseIdentity, - -- * re-exports module Wire.API.User.Identity, module Wire.API.User.Profile, @@ -93,7 +94,6 @@ module Wire.API.User -- * Swagger modelDelete, modelEmailUpdate, - modelNewUser, modelUser, modelUserIdList, modelVerifyDelete, @@ -107,9 +107,8 @@ where import Control.Applicative import Control.Error.Safe (rightMay) -import Control.Lens (over, view, (.~), (?~)) +import Control.Lens (over, (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) -import qualified Data.Aeson.KeyMap as KeyMap import qualified Data.Aeson.Types as A import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI @@ -121,7 +120,6 @@ import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap import Data.Id import Data.Json.Util (UTCTimeMillis, (#)) import Data.LegalHold (UserLegalHoldStatus) -import qualified Data.List as List import Data.Misc (PlainTextPassword (..)) import Data.Qualified import Data.Range @@ -139,11 +137,12 @@ import Imports import qualified SAML2.WebSSO as SAML import Servant (type (.++)) import qualified Test.QuickCheck as QC +import qualified Web.Cookie as Web 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.Team (BindingNewTeam, bindingNewTeamObjectSchema) import Wire.API.User.Activation (ActivationCode) import Wire.API.User.Auth (CookieLabel) import Wire.API.User.Identity @@ -338,77 +337,28 @@ data User = User } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform User) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema User) --- Cannot use deriving (ToSchema) via (CustomSwagger ...) because we need to --- mark 'deleted' as optional, but it is not a 'Maybe' --- and we need to manually add the identity schema fields at the top level --- instead of nesting them under the 'identity' field. -instance S.ToSchema User where - declareNamedSchema _ = do - identityProperties <- view (S.schema . S.properties) <$> S.declareNamedSchema (Proxy @UserIdentity) - genericSchema <- - S.genericDeclareNamedSchema - ( swaggerOptions - @'[ FieldLabelModifier - ( StripPrefix "user", - CamelToSnake, - LabelMappings - '[ "pict" ':-> "picture", - "expire" ':-> "expires_at", - "display_name" ':-> "name" - ] - ) - ] - ) - (Proxy @User) - pure $ - genericSchema - & over (S.schema . S.required) (List.delete "deleted") - -- The UserIdentity fields need to be flat-included, not be in a sub-object - & over (S.schema . S.properties) (InsOrdHashMap.delete "identity") - & over (S.schema . S.properties) (InsOrdHashMap.union identityProperties) - --- FUTUREWORK: --- disentangle json serializations for 'User', 'NewUser', 'UserIdentity', 'NewUserOrigin'. -instance ToJSON User where - toJSON u = - A.object $ - "id" A..= userId u - # "qualified_id" A..= userQualifiedId u - # "name" A..= userDisplayName u - # "picture" A..= userPict u - # "assets" A..= userAssets u - # "email" A..= userEmail u - # "phone" A..= userPhone u - # "accent_id" A..= userAccentId u - # "deleted" A..= (if userDeleted u then Just True else Nothing) - # "locale" A..= userLocale u - # "service" A..= userService u - # "handle" A..= userHandle u - # "expires_at" A..= userExpire u - # "team" A..= userTeam u - # "sso_id" A..= userSSOId u - # "managed_by" A..= userManagedBy u - # [] - -instance FromJSON User where - parseJSON = A.withObject "user" $ \o -> do - ssoid <- o A..:? "sso_id" - User - <$> o A..: "id" - <*> o A..: "qualified_id" - <*> parseIdentity ssoid o - <*> o A..: "name" - <*> o A..:? "picture" A..!= noPict - <*> o A..:? "assets" A..!= [] - <*> o A..: "accent_id" - <*> o A..:? "deleted" A..!= False - <*> o A..: "locale" - <*> o A..:? "service" - <*> o A..:? "handle" - <*> o A..:? "expires_at" - <*> o A..:? "team" - <*> o A..:? "managed_by" A..!= ManagedByWire +-- -- FUTUREWORK: +-- -- disentangle json serializations for 'User', 'NewUser', 'UserIdentity', 'NewUserOrigin'. +instance ToSchema User where + schema = + object "User" $ + User + <$> userId .= field "id" schema + <*> userQualifiedId .= field "qualified_id" schema + <*> userIdentity .= maybeUserIdentityObjectSchema + <*> userDisplayName .= field "name" schema + <*> userPict .= (fromMaybe noPict <$> optField "picture" schema) + <*> userAssets .= (fromMaybe [] <$> optField "assets" (array schema)) + <*> userAccentId .= field "accent_id" schema + <*> (fromMaybe False <$> (\u -> if userDeleted u then Just True else Nothing) .= maybe_ (optField "deleted" schema)) + <*> userLocale .= field "locale" schema + <*> userService .= maybe_ (optField "service" schema) + <*> userHandle .= maybe_ (optField "handle" schema) + <*> userExpire .= maybe_ (optField "expires_at" schema) + <*> userTeam .= maybe_ (optField "team" schema) + <*> userManagedBy .= (fromMaybe ManagedByWire <$> optField "managed_by" schema) userEmail :: User -> Maybe Email userEmail = emailIdentity <=< userIdentity @@ -502,56 +452,13 @@ publicProfile u legalHoldStatus = -- SCIM-managed user) newtype NewUserPublic = NewUserPublic NewUser deriving stock (Eq, Show, Generic) - deriving newtype (ToJSON) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewUserPublic) -modelNewUser :: Doc.Model -modelNewUser = Doc.defineModel "NewUser" $ do - Doc.description "New User Data" - Doc.property "name" Doc.string' $ - Doc.description "Name (1 - 128 characters)" - Doc.property "email" Doc.string' $ do - Doc.description "Email address" - Doc.optional - Doc.property "password" Doc.string' $ do - Doc.description "Password (6 - 1024 characters)" - Doc.optional - Doc.property "assets" (Doc.array (Doc.ref modelAsset)) $ do - Doc.description "Profile assets" - Doc.optional - Doc.property "phone" Doc.string' $ do - Doc.description "E.164 phone number" - Doc.optional - Doc.property "accent_id" Doc.int32' $ do - Doc.description "Accent colour ID" - Doc.optional - Doc.property "email_code" Doc.bytes' $ do - Doc.description "Email activation code" - Doc.optional - Doc.property "phone_code" Doc.bytes' $ do - Doc.description "Phone activation code" - Doc.optional - Doc.property "invitation_code" Doc.bytes' $ do - Doc.description "Invitation code. Mutually exclusive with team|team_code" - Doc.optional - Doc.property "locale" Doc.string' $ do - Doc.description "Locale in format." - Doc.optional - Doc.property "label" Doc.string' $ do - Doc.description - "An optional label to associate with the access cookie, \ - \if one is granted during account creation." - Doc.optional - Doc.property "team_code" Doc.string' $ do - Doc.description "Team invitation code. Mutually exclusive with team|invitation_code" - Doc.optional - Doc.property "team" (Doc.ref modelNewBindingTeam) $ do - Doc.description "New team information. Mutually exclusive with team_code|invitation_code" - Doc.optional - -instance FromJSON NewUserPublic where - parseJSON val = do - nu <- parseJSON val - either fail pure $ validateNewUserPublic nu +instance ToSchema NewUserPublic where + schema = + unwrap .= withParser schema (either fail pure . validateNewUserPublic) + where + unwrap (NewUserPublic nu) = nu validateNewUserPublic :: NewUser -> Either String NewUserPublic validateNewUserPublic nu @@ -586,6 +493,75 @@ isNewUserTeamMember u = case newUserTeam u of instance Arbitrary NewUserPublic where arbitrary = arbitrary `QC.suchThatMap` (rightMay . validateNewUserPublic) +data RegisterError + = RegisterErrorWhitelistError + | RegisterErrorInvalidInvitationCode + | RegisterErrorMissingIdentity + | RegisterErrorUserKeyExists + | RegisterErrorInvalidActivationCodeWrongUser + | RegisterErrorInvalidActivationCodeWrongCode + | RegisterErrorInvalidEmail + | RegisterErrorInvalidPhone + | RegisterErrorBlacklistedPhone + | RegisterErrorBlacklistedEmail + | RegisterErrorTooManyTeamMembers + | RegisterErrorUserCreationRestricted + deriving (Generic) + deriving (AsUnion RegisterErrorResponses) via GenericAsUnion RegisterErrorResponses RegisterError + +instance GSOP.Generic RegisterError + +type RegisterErrorResponses = + '[ WhitelistError, + InvalidInvitationCode, + MissingIdentity, + UserKeyExists, + InvalidActivationCodeWrongUser, + InvalidActivationCodeWrongCode, + InvalidEmail, + InvalidPhone, + BlacklistedPhone, + BlacklistedEmail, + TooManyTeamMembers, + UserCreationRestricted + ] + +type RegisterResponses = + RegisterErrorResponses + .++ '[ WithHeaders + '[ DescHeader "Set-Cookie" "Cookie" Web.SetCookie, + DescHeader "Location" "UserId" UserId + ] + RegisterSuccess + (Respond 201 "User created and pending activation" SelfProfile) + ] + +instance AsHeaders '[Web.SetCookie, UserId] SelfProfile RegisterSuccess where + fromHeaders (I cookie :* (_ :* Nil), sp) = RegisterSuccess cookie sp + toHeaders (RegisterSuccess cookie sp) = (I cookie :* (I (userId (selfUser sp)) :* Nil), sp) + +data RegisterSuccess = RegisterSuccess Web.SetCookie SelfProfile + +instance (res ~ RegisterResponses) => AsUnion res (Either RegisterError RegisterSuccess) where + toUnion = eitherToUnion (toUnion @RegisterErrorResponses) (Z . I) + fromUnion = eitherFromUnion (fromUnion @RegisterErrorResponses) (unI . unZ) + +type RegisterInternalResponses = + RegisterErrorResponses + .++ '[ WithHeaders + '[DescHeader "Location" "UserId" UserId] + SelfProfile + (Respond 201 "User created and pending activation" SelfProfile) + ] + +instance AsHeaders '[UserId] SelfProfile SelfProfile where + fromHeaders (_ :* Nil, sp) = sp + toHeaders sp = (I (userId (selfUser sp)) :* Nil, sp) + +instance (res ~ RegisterInternalResponses) => AsUnion res (Either RegisterError SelfProfile) where + toUnion = eitherToUnion (toUnion @RegisterErrorResponses) (Z . I) + fromUnion = eitherFromUnion (fromUnion @RegisterErrorResponses) (unI . unZ) + data NewUser = NewUser { newUserDisplayName :: Name, -- | use this as 'UserId' (if 'Nothing', call 'Data.UUID.nextRandom'). @@ -605,6 +581,7 @@ data NewUser = NewUser newUserManagedBy :: Maybe ManagedBy } deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewUser) emptyNewUser :: Name -> NewUser emptyNewUser name = @@ -628,47 +605,112 @@ emptyNewUser name = -- | 1 second - 1 week type ExpiresIn = Range 1 604800 Integer -instance ToJSON NewUser where - toJSON u = - A.object $ - "name" A..= newUserDisplayName u - # "uuid" A..= newUserUUID u - # "email" A..= newUserEmail u - # "email_code" A..= newUserEmailCode u - # "picture" A..= newUserPict u - # "assets" A..= newUserAssets u - # "phone" A..= newUserPhone u - # "phone_code" A..= newUserPhoneCode u - # "accent_id" A..= newUserAccentId u - # "label" A..= newUserLabel u - # "locale" A..= newUserLocale u - # "password" A..= newUserPassword u - # "expires_in" A..= newUserExpiresIn u - # "sso_id" A..= newUserSSOId u - # "managed_by" A..= newUserManagedBy u - # maybe [] jsonNewUserOrigin (newUserOrigin u) - -instance FromJSON NewUser where - parseJSON = A.withObject "new-user" $ \o -> do - ssoid <- o A..:? "sso_id" - newUserDisplayName <- o A..: "name" - newUserUUID <- o A..:? "uuid" - newUserIdentity <- parseIdentity ssoid o - newUserPict <- o A..:? "picture" - newUserAssets <- o A..:? "assets" A..!= [] - newUserAccentId <- o A..:? "accent_id" - newUserEmailCode <- o A..:? "email_code" - newUserPhoneCode <- o A..:? "phone_code" - newUserLabel <- o A..:? "label" - newUserLocale <- o A..:? "locale" - newUserPassword <- o A..:? "password" - newUserOrigin <- parseNewUserOrigin newUserPassword newUserIdentity ssoid o - newUserExpires <- o A..:? "expires_in" - newUserExpiresIn <- case (newUserExpires, newUserIdentity) of +-- | Raw representation of 'NewUser' to help with writing Schema instances. +data NewUserRaw = NewUserRaw + { newUserRawDisplayName :: Name, + newUserRawUUID :: Maybe UUID, + newUserRawEmail :: Maybe Email, + newUserRawPhone :: Maybe Phone, + newUserRawSSOId :: Maybe UserSSOId, + -- | DEPRECATED + newUserRawPict :: Maybe Pict, + newUserRawAssets :: [Asset], + newUserRawAccentId :: Maybe ColourId, + newUserRawEmailCode :: Maybe ActivationCode, + newUserRawPhoneCode :: Maybe ActivationCode, + newUserRawInvitationCode :: Maybe InvitationCode, + newUserRawTeamCode :: Maybe InvitationCode, + newUserRawTeam :: Maybe BindingNewTeamUser, + newUserRawTeamId :: Maybe TeamId, + newUserRawLabel :: Maybe CookieLabel, + newUserRawLocale :: Maybe Locale, + newUserRawPassword :: Maybe PlainTextPassword, + newUserRawExpiresIn :: Maybe ExpiresIn, + newUserRawManagedBy :: Maybe ManagedBy + } + +newUserRawObjectSchema :: ObjectSchema SwaggerDoc NewUserRaw +newUserRawObjectSchema = + NewUserRaw + <$> newUserRawDisplayName .= field "name" schema + <*> newUserRawUUID .= maybe_ (optField "uuid" genericToSchema) + <*> newUserRawEmail .= maybe_ (optField "email" schema) + <*> newUserRawPhone .= maybe_ (optField "phone" schema) + <*> newUserRawSSOId .= maybe_ (optField "sso_id" genericToSchema) + <*> newUserRawPict .= maybe_ (optField "picture" schema) + <*> newUserRawAssets .= (fromMaybe [] <$> optField "assets" (array schema)) + <*> newUserRawAccentId .= maybe_ (optField "accent_id" schema) + <*> newUserRawEmailCode .= maybe_ (optField "email_code" schema) + <*> newUserRawPhoneCode .= maybe_ (optField "phone_code" schema) + <*> newUserRawInvitationCode .= maybe_ (optField "invitation_code" schema) + <*> newUserRawTeamCode .= maybe_ (optField "team_code" schema) + <*> newUserRawTeam .= maybe_ (optField "team" schema) + <*> newUserRawTeamId .= maybe_ (optField "team_id" schema) + <*> newUserRawLabel .= maybe_ (optField "label" schema) + <*> newUserRawLocale .= maybe_ (optField "locale" schema) + <*> newUserRawPassword .= maybe_ (optField "password" schema) + <*> newUserRawExpiresIn .= maybe_ (optField "expires_in" schema) + <*> newUserRawManagedBy .= maybe_ (optField "managed_by" schema) + +instance ToSchema NewUser where + schema = + object "NewUser" $ newUserToRaw .= withParser newUserRawObjectSchema newUserFromRaw + +newUserToRaw :: NewUser -> NewUserRaw +newUserToRaw NewUser {..} = + let maybeOriginNTU = newUserOriginNewTeamUser =<< newUserOrigin + in NewUserRaw + { newUserRawDisplayName = newUserDisplayName, + newUserRawUUID = newUserUUID, + newUserRawEmail = emailIdentity =<< newUserIdentity, + newUserRawPhone = phoneIdentity =<< newUserIdentity, + newUserRawSSOId = ssoIdentity =<< newUserIdentity, + newUserRawPict = newUserPict, + newUserRawAssets = newUserAssets, + newUserRawAccentId = newUserAccentId, + newUserRawEmailCode = newUserEmailCode, + newUserRawPhoneCode = newUserPhoneCode, + newUserRawInvitationCode = newUserOriginInvitationCode =<< newUserOrigin, + newUserRawTeamCode = newTeamUserCode =<< maybeOriginNTU, + newUserRawTeam = newTeamUserCreator =<< maybeOriginNTU, + newUserRawTeamId = newTeamUserTeamId =<< maybeOriginNTU, + newUserRawLabel = newUserLabel, + newUserRawLocale = newUserLocale, + newUserRawPassword = newUserPassword, + newUserRawExpiresIn = newUserExpiresIn, + newUserRawManagedBy = newUserManagedBy + } + +newUserFromRaw :: NewUserRaw -> A.Parser NewUser +newUserFromRaw NewUserRaw {..} = do + origin <- + either fail pure $ + maybeNewUserOriginFromComponents + (isJust newUserRawPassword) + (isJust newUserRawSSOId) + (newUserRawInvitationCode, newUserRawTeamCode, newUserRawTeam, newUserRawTeamId) + let identity = maybeUserIdentityFromComponents (newUserRawEmail, newUserRawPhone, newUserRawSSOId) + expiresIn <- + case (newUserRawExpiresIn, identity) of (Just _, Just _) -> fail "Only users without an identity can expire" - _ -> return newUserExpires - newUserManagedBy <- o A..:? "managed_by" - return NewUser {..} + _ -> pure newUserRawExpiresIn + pure $ + NewUser + { newUserDisplayName = newUserRawDisplayName, + newUserUUID = newUserRawUUID, + newUserIdentity = identity, + newUserPict = newUserRawPict, + newUserAssets = newUserRawAssets, + newUserAccentId = newUserRawAccentId, + newUserEmailCode = newUserRawEmailCode, + newUserPhoneCode = newUserRawPhoneCode, + newUserOrigin = origin, + newUserLabel = newUserRawLabel, + newUserLocale = newUserRawLocale, + newUserPassword = newUserRawPassword, + newUserExpiresIn = expiresIn, + newUserManagedBy = newUserRawManagedBy + } -- FUTUREWORK: align more with FromJSON instance? instance Arbitrary NewUser where @@ -739,56 +781,46 @@ data NewUserOrigin deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewUserOrigin) -jsonNewUserOrigin :: NewUserOrigin -> [A.Pair] -jsonNewUserOrigin = \case - NewUserOriginInvitationCode inv -> ["invitation_code" A..= inv] - NewUserOriginTeamUser (NewTeamMember tc) -> ["team_code" A..= tc] - NewUserOriginTeamUser (NewTeamCreator team) -> ["team" A..= team] - NewUserOriginTeamUser (NewTeamMemberSSO ti) -> ["team_id" A..= ti] - -parseNewUserOrigin :: - Maybe PlainTextPassword -> - Maybe UserIdentity -> - Maybe UserSSOId -> - A.Object -> - A.Parser (Maybe NewUserOrigin) -parseNewUserOrigin pass uid ssoid o = do - invcode <- o A..:? "invitation_code" - teamcode <- o A..:? "team_code" - team <- o A..:? "team" - teamid <- o A..:? "team_id" - result <- case (invcode, teamcode, team, ssoid, teamid) of - (Just a, Nothing, Nothing, Nothing, Nothing) -> return . Just . NewUserOriginInvitationCode $ a - (Nothing, Just a, Nothing, Nothing, Nothing) -> return . Just . NewUserOriginTeamUser $ NewTeamMember a - (Nothing, Nothing, Just a, Nothing, Nothing) -> return . Just . NewUserOriginTeamUser $ NewTeamCreator a - (Nothing, Nothing, Nothing, Just _, Just t) -> return . Just . NewUserOriginTeamUser $ NewTeamMemberSSO t - (Nothing, Nothing, Nothing, Nothing, Nothing) -> return Nothing - (_, _, _, Just _, Nothing) -> fail "sso_id, team_id must be either both present or both absent." - (_, _, _, Nothing, Just _) -> fail "sso_id, team_id must be either both present or both absent." - _ -> fail "team_code, team, invitation_code, sso_id, and the pair (sso_id, team_id) are mutually exclusive" - case (result, pass, uid) of - (_, _, Just SSOIdentity {}) -> pure result - (Just (NewUserOriginTeamUser _), Nothing, _) -> fail "all team users must set a password on creation" +type NewUserOriginComponents = (Maybe InvitationCode, Maybe InvitationCode, Maybe BindingNewTeamUser, Maybe TeamId) + +newUserOriginInvitationCode :: NewUserOrigin -> Maybe InvitationCode +newUserOriginInvitationCode = \case + NewUserOriginInvitationCode ic -> Just ic + NewUserOriginTeamUser _ -> Nothing + +newUserOriginNewTeamUser :: NewUserOrigin -> Maybe NewTeamUser +newUserOriginNewTeamUser = \case + NewUserOriginInvitationCode _ -> Nothing + NewUserOriginTeamUser ntu -> Just ntu + +maybeNewUserOriginFromComponents :: + -- | Does the user have a password + Bool -> + -- | Does the user have an SSO Identity + Bool -> + NewUserOriginComponents -> + Either String (Maybe NewUserOrigin) +maybeNewUserOriginFromComponents hasPassword hasSSO (invcode, teamcode, team, teamid) = do + result <- case (invcode, teamcode, team, hasSSO, teamid) of + (Just a, Nothing, Nothing, False, Nothing) -> Right . Just . NewUserOriginInvitationCode $ a + (Nothing, Just a, Nothing, False, Nothing) -> Right . Just . NewUserOriginTeamUser $ NewTeamMember a + (Nothing, Nothing, Just a, False, Nothing) -> Right . Just . NewUserOriginTeamUser $ NewTeamCreator a + (Nothing, Nothing, Nothing, True, Just t) -> Right . Just . NewUserOriginTeamUser $ NewTeamMemberSSO t + (Nothing, Nothing, Nothing, False, Nothing) -> Right Nothing + (_, _, _, True, Nothing) -> Left "sso_id, team_id must be either both present or both absent." + (_, _, _, False, Just _) -> Left "sso_id, team_id must be either both present or both absent." + _ -> Left "team_code, team, invitation_code, sso_id, and the pair (sso_id, team_id) are mutually exclusive" + case (result, hasPassword, hasSSO) of + (_, _, True) -> Right result + (Just (NewUserOriginTeamUser _), False, _) -> Left "all team users must set a password on creation" _ -> pure result -- | A random invitation code for use during registration newtype InvitationCode = InvitationCode {fromInvitationCode :: AsciiBase64Url} deriving stock (Eq, Show, Generic) - deriving newtype (FromJSON, ToJSON, ToByteString, FromByteString, Arbitrary) - --------------------------------------------------------------------------------- --- helpers - --- | Fails if email or phone or ssoid are present but invalid. --- If neither are present, it will not fail, but return Nothing. --- --- FUTUREWORK: Why is the SSO ID passed separately? -parseIdentity :: Maybe UserSSOId -> A.Object -> A.Parser (Maybe UserIdentity) -parseIdentity ssoid o = - if isJust (KeyMap.lookup "email" o <|> KeyMap.lookup "phone" o) || isJust ssoid - then Just <$> parseJSON (A.Object o) - else pure Nothing + deriving newtype (ToSchema, ToByteString, FromByteString, Arbitrary) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema InvitationCode -------------------------------------------------------------------------------- -- NewTeamUser @@ -802,6 +834,24 @@ data NewTeamUser deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewTeamUser) +newTeamUserCode :: NewTeamUser -> Maybe InvitationCode +newTeamUserCode = \case + NewTeamMember ic -> Just ic + NewTeamCreator _ -> Nothing + NewTeamMemberSSO _ -> Nothing + +newTeamUserCreator :: NewTeamUser -> Maybe BindingNewTeamUser +newTeamUserCreator = \case + NewTeamMember _ -> Nothing + NewTeamCreator bntu -> Just bntu + NewTeamMemberSSO _ -> Nothing + +newTeamUserTeamId :: NewTeamUser -> Maybe TeamId +newTeamUserTeamId = \case + NewTeamMember _ -> Nothing + NewTeamCreator _ -> Nothing + NewTeamMemberSSO tid -> Just tid + data BindingNewTeamUser = BindingNewTeamUser { bnuTeam :: BindingNewTeam, bnuCurrency :: Maybe Currency.Alpha @@ -810,28 +860,14 @@ data BindingNewTeamUser = BindingNewTeamUser } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform BindingNewTeamUser) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema BindingNewTeamUser) -instance ToJSON BindingNewTeamUser where - toJSON (BindingNewTeamUser (BindingNewTeam t) c) = - A.object $ - "currency" A..= c - # newTeamJson t - where - -- FUTUREWORK(leif): this was originally defined in libs/wire-api/src/Wire/API/Team.hs and I moved it here - -- during the process of servantifying, it should go away when servantification is complete - newTeamJson :: NewTeam a -> [A.Pair] - newTeamJson (NewTeam n i ik _) = - "name" A..= fromRange n - # "icon" A..= fromRange i - # "icon_key" A..= (fromRange <$> ik) - # [] - -instance FromJSON BindingNewTeamUser where - parseJSON j@(A.Object o) = do - c <- o A..:? "currency" - t <- parseJSON j - return $ BindingNewTeamUser t c - parseJSON _ = fail "parseJSON BindingNewTeamUser: must be an object" +instance ToSchema BindingNewTeamUser where + schema = + object "BindingNewTeamUser" $ + BindingNewTeamUser + <$> bnuTeam .= bindingNewTeamObjectSchema + <*> bnuCurrency .= maybe_ (optField "currency" genericToSchema) -------------------------------------------------------------------------------- -- Profile Updates diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index a4a2f2a056..d97b12eb70 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -43,6 +43,8 @@ where import Data.Aeson import Data.ByteString.Conversion import Data.Json.Util ((#)) +import Data.Schema (Schema (..), ToSchema, schemaIn) +import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc import Data.Text.Ascii import Imports @@ -84,7 +86,8 @@ newtype ActivationKey = ActivationKey newtype ActivationCode = ActivationCode {fromActivationCode :: AsciiBase64Url} deriving stock (Eq, Show, Generic) - deriving newtype (ToByteString, FromByteString, ToJSON, FromJSON, Arbitrary) + deriving newtype (ToByteString, FromByteString, ToSchema, Arbitrary) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema ActivationCode -------------------------------------------------------------------------------- -- Activate @@ -181,7 +184,7 @@ instance ToJSON ActivationResponse where instance FromJSON ActivationResponse where parseJSON = withObject "ActivationResponse" $ \o -> ActivationResponse - <$> parseJSON (Object o) + <$> schemaIn userIdentityObjectSchema o <*> o .:? "first" .!= False -------------------------------------------------------------------------------- diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 07ca0f4bc9..4b1e2fcc27 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -26,6 +26,9 @@ module Wire.API.User.Identity emailIdentity, phoneIdentity, ssoIdentity, + userIdentityObjectSchema, + maybeUserIdentityObjectSchema, + maybeUserIdentityFromComponents, -- * Email Email (..), @@ -50,7 +53,7 @@ module Wire.API.User.Identity where import Control.Applicative (optional) -import Control.Lens (over, (.~), (?~), (^.)) +import Control.Lens (dimap, over, (.~), (?~), (^.)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A import qualified Data.Aeson.Types as A @@ -65,6 +68,7 @@ import qualified Data.Swagger as S import qualified Data.Text as Text import Data.Text.Encoding (decodeUtf8', encodeUtf8) import Data.Time.Clock +import Data.Tuple.Extra (fst3, snd3, thd3) import Imports import SAML2.WebSSO.Test.Arbitrary () import qualified SAML2.WebSSO.Types as SAML @@ -91,40 +95,37 @@ data UserIdentity deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserIdentity) -instance S.ToSchema UserIdentity where - declareNamedSchema _ = do - emailSchema <- S.declareSchemaRef (Proxy @Email) - phoneSchema <- S.declareSchemaRef (Proxy @Phone) - ssoSchema <- S.declareSchemaRef (Proxy @UserSSOId) - return $ - S.NamedSchema (Just "userIdentity") $ - mempty - & S.type_ ?~ S.SwaggerObject - & S.properties - .~ [ ("email", emailSchema), - ("phone", phoneSchema), - ("sso_id", ssoSchema) - ] - -instance ToJSON UserIdentity where - toJSON = \case - FullIdentity em ph -> go (Just em) (Just ph) Nothing - EmailIdentity em -> go (Just em) Nothing Nothing - PhoneIdentity ph -> go Nothing (Just ph) Nothing - SSOIdentity si em ph -> go em ph (Just si) - where - go :: Maybe Email -> Maybe Phone -> Maybe UserSSOId -> A.Value - go em ph si = A.object ["email" A..= em, "phone" A..= ph, "sso_id" A..= si] - -instance FromJSON UserIdentity where - parseJSON = A.withObject "UserIdentity" $ \o -> do - email <- o A..:? "email" - phone <- o A..:? "phone" - ssoid <- o A..:? "sso_id" - maybe - (fail "Missing 'email' or 'phone' or 'sso_id'.") - return - (newIdentity email phone ssoid) +userIdentityObjectSchema :: ObjectSchema SwaggerDoc UserIdentity +userIdentityObjectSchema = + Just .= withParser maybeUserIdentityObjectSchema (maybe (fail "Missing 'email' or 'phone' or 'sso_id'.") pure) + +maybeUserIdentityObjectSchema :: ObjectSchema SwaggerDoc (Maybe UserIdentity) +maybeUserIdentityObjectSchema = + dimap maybeUserIdentityToComponents maybeUserIdentityFromComponents userIdentityComponentsObjectSchema + +type UserIdentityComponents = (Maybe Email, Maybe Phone, Maybe UserSSOId) + +userIdentityComponentsObjectSchema :: ObjectSchema SwaggerDoc UserIdentityComponents +userIdentityComponentsObjectSchema = + (,,) + <$> fst3 .= maybe_ (optField "email" schema) + <*> snd3 .= maybe_ (optField "phone" schema) + <*> thd3 .= maybe_ (optField "sso_id" genericToSchema) + +maybeUserIdentityFromComponents :: UserIdentityComponents -> Maybe UserIdentity +maybeUserIdentityFromComponents = \case + (maybeEmail, maybePhone, Just ssoid) -> Just $ SSOIdentity ssoid maybeEmail maybePhone + (Just email, Just phone, Nothing) -> Just $ FullIdentity email phone + (Just email, Nothing, Nothing) -> Just $ EmailIdentity email + (Nothing, Just phone, Nothing) -> Just $ PhoneIdentity phone + (Nothing, Nothing, Nothing) -> Nothing + +maybeUserIdentityToComponents :: Maybe UserIdentity -> UserIdentityComponents +maybeUserIdentityToComponents Nothing = (Nothing, Nothing, Nothing) +maybeUserIdentityToComponents (Just (FullIdentity email phone)) = (Just email, Just phone, Nothing) +maybeUserIdentityToComponents (Just (EmailIdentity email)) = (Just email, Nothing, Nothing) +maybeUserIdentityToComponents (Just (PhoneIdentity phone)) = (Nothing, Just phone, Nothing) +maybeUserIdentityToComponents (Just (SSOIdentity ssoid m_email m_phone)) = (m_email, m_phone, Just ssoid) newIdentity :: Maybe Email -> Maybe Phone -> Maybe UserSSOId -> Maybe UserIdentity newIdentity email phone (Just sso) = Just $! SSOIdentity sso email phone diff --git a/libs/wire-api/src/Wire/API/User/Orphans.hs b/libs/wire-api/src/Wire/API/User/Orphans.hs index 232d4f1f85..7dd5e5dfd2 100644 --- a/libs/wire-api/src/Wire/API/User/Orphans.hs +++ b/libs/wire-api/src/Wire/API/User/Orphans.hs @@ -21,6 +21,7 @@ module Wire.API.User.Orphans where import Control.Lens +import qualified Data.Currency as Currency import Data.ISO3166_CountryCodes import Data.LanguageCodes import Data.Proxy @@ -121,3 +122,6 @@ instance ToParamSchema URI where instance ToSchema X509.SignedCertificate where declareNamedSchema _ = declareNamedSchema (Proxy @String) + +instance ToSchema Currency.Alpha where + declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index b5950500fe..5327fce297 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -67,7 +67,6 @@ import Data.Schema import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text as Text -import Deriving.Swagger (CamelToSnake, ConstructorTagModifier, CustomSwagger, StripPrefix) import Imports import Wire.API.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) import Wire.API.User.Orphans () @@ -252,7 +251,7 @@ data ManagedBy ManagedByScim deriving stock (Eq, Bounded, Enum, Show, Generic) deriving (Arbitrary) via (GenericUniform ManagedBy) - deriving (S.ToSchema) via (CustomSwagger '[ConstructorTagModifier (StripPrefix "ManagedBy", CamelToSnake)] ManagedBy) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema ManagedBy) typeManagedBy :: Doc.DataType typeManagedBy = @@ -262,17 +261,13 @@ typeManagedBy = "scim" ] -instance ToJSON ManagedBy where - toJSON = - A.String . \case - ManagedByWire -> "wire" - ManagedByScim -> "scim" - -instance FromJSON ManagedBy where - parseJSON = A.withText "ManagedBy" $ \case - "wire" -> pure ManagedByWire - "scim" -> pure ManagedByScim - other -> fail $ "Invalid ManagedBy: " ++ show other +instance ToSchema ManagedBy where + schema = + enum @Text "ManagedBy" $ + mconcat + [ element "wire" ManagedByWire, + element "scim" ManagedByScim + ] instance ToByteString ManagedBy where builder ManagedByWire = "wire" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index f0f20bc7e3..798a88c877 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -225,7 +225,6 @@ import qualified Test.Wire.API.Golden.Generated.UserClients_user import qualified Test.Wire.API.Golden.Generated.UserConnectionList_user import qualified Test.Wire.API.Golden.Generated.UserConnection_user import qualified Test.Wire.API.Golden.Generated.UserHandleInfo_user -import qualified Test.Wire.API.Golden.Generated.UserIdentity_user import qualified Test.Wire.API.Golden.Generated.UserLegalHoldStatusResponse_team import qualified Test.Wire.API.Golden.Generated.UserProfile_user import qualified Test.Wire.API.Golden.Generated.UserSSOId_user @@ -1028,8 +1027,6 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_1, "testObject_Phone_user_1.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_2, "testObject_Phone_user_2.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_3, "testObject_Phone_user_3.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_4, "testObject_Phone_user_4.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_5, "testObject_Phone_user_5.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_6, "testObject_Phone_user_6.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_7, "testObject_Phone_user_7.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_8, "testObject_Phone_user_8.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_9, "testObject_Phone_user_9.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_10, "testObject_Phone_user_10.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_11, "testObject_Phone_user_11.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_12, "testObject_Phone_user_12.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_13, "testObject_Phone_user_13.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_14, "testObject_Phone_user_14.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_15, "testObject_Phone_user_15.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_16, "testObject_Phone_user_16.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_17, "testObject_Phone_user_17.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_18, "testObject_Phone_user_18.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_19, "testObject_Phone_user_19.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_20, "testObject_Phone_user_20.json")], testGroup "Golden: UserSSOId_user" $ testObjects [(Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_2, "testObject_UserSSOId_user_2.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_9, "testObject_UserSSOId_user_9.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_13, "testObject_UserSSOId_user_13.json")], - testGroup "Golden: UserIdentity_user" $ - testObjects [(Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_1, "testObject_UserIdentity_user_1.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_2, "testObject_UserIdentity_user_2.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_3, "testObject_UserIdentity_user_3.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_4, "testObject_UserIdentity_user_4.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_6, "testObject_UserIdentity_user_6.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_7, "testObject_UserIdentity_user_7.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_9, "testObject_UserIdentity_user_9.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_10, "testObject_UserIdentity_user_10.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_11, "testObject_UserIdentity_user_11.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_12, "testObject_UserIdentity_user_12.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_13, "testObject_UserIdentity_user_13.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_14, "testObject_UserIdentity_user_14.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_15, "testObject_UserIdentity_user_15.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_17, "testObject_UserIdentity_user_17.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_18, "testObject_UserIdentity_user_18.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_19, "testObject_UserIdentity_user_19.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_20, "testObject_UserIdentity_user_20.json")], testGroup "Golden: NewPasswordReset_user" $ testObjects [(Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_1, "testObject_NewPasswordReset_user_1.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_2, "testObject_NewPasswordReset_user_2.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_3, "testObject_NewPasswordReset_user_3.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_4, "testObject_NewPasswordReset_user_4.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_5, "testObject_NewPasswordReset_user_5.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_6, "testObject_NewPasswordReset_user_6.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_7, "testObject_NewPasswordReset_user_7.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_8, "testObject_NewPasswordReset_user_8.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_9, "testObject_NewPasswordReset_user_9.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_10, "testObject_NewPasswordReset_user_10.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_11, "testObject_NewPasswordReset_user_11.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_12, "testObject_NewPasswordReset_user_12.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_13, "testObject_NewPasswordReset_user_13.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_14, "testObject_NewPasswordReset_user_14.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_15, "testObject_NewPasswordReset_user_15.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_16, "testObject_NewPasswordReset_user_16.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_17, "testObject_NewPasswordReset_user_17.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_18, "testObject_NewPasswordReset_user_18.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_19, "testObject_NewPasswordReset_user_19.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_20, "testObject_NewPasswordReset_user_20.json")], testGroup "Golden: PasswordResetKey_user" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/AccessRoleLegacy_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/AccessRoleLegacy_user.hs index ad1587d065..8331b06be8 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/AccessRoleLegacy_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/AccessRoleLegacy_user.hs @@ -14,6 +14,7 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . + module Test.Wire.API.Golden.Generated.AccessRoleLegacy_user where import Wire.API.Conversation (AccessRoleLegacy (..)) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/AddBotResponse_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/AddBotResponse_user.hs index 380efedc0b..93f6f0ade0 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/AddBotResponse_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/AddBotResponse_user.hs @@ -25,24 +25,10 @@ import Data.Qualified import qualified Data.UUID as UUID (fromString) import Imports (Maybe (Just, Nothing), fromJust, read, (.)) import Wire.API.Conversation - ( ConversationRename (ConversationRename, cupName), - ) -import Wire.API.Conversation.Bot (AddBotResponse (..)) -import Wire.API.Conversation.Typing (TypingData (TypingData, tdStatus), TypingStatus (StartedTyping)) +import Wire.API.Conversation.Bot +import Wire.API.Conversation.Typing import Wire.API.Event.Conversation - ( Event (Event), - EventData (..), - EventType - ( ConvRename, - Typing - ), - ) import Wire.API.User - ( Asset (ImageAsset), - AssetSize (AssetPreview), - ColourId (ColourId, fromColourId), - Name (Name, fromName), - ) testObject_AddBotResponse_user_1 :: AddBotResponse testObject_AddBotResponse_user_1 = @@ -58,7 +44,6 @@ testObject_AddBotResponse_user_1 = rsAddBotAssets = [ImageAsset "7" Nothing, ImageAsset "" (Just AssetPreview)], rsAddBotEvent = Event - ConvRename (Qualified (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000200000003"))) (Domain "faraway.example.com")) (Qualified (Id (fromJust (UUID.fromString "00000004-0000-0004-0000-000400000004"))) (Domain "faraway.example.com")) (read "1864-05-12 19:20:22.286 UTC") @@ -79,7 +64,6 @@ testObject_AddBotResponse_user_2 = rsAddBotAssets = [], rsAddBotEvent = Event - Typing (Qualified (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000300000001"))) (Domain "faraway.example.com")) (Qualified (Id (fromJust (UUID.fromString "00000004-0000-0000-0000-000300000001"))) (Domain "faraway.example.com")) (read "1864-05-08 19:02:58.6 UTC") diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs index e9a1ca0ec3..bf4a0e92ac 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs @@ -27,376 +27,285 @@ import qualified Data.UUID as UUID (fromString) import GHC.Exts (IsList (fromList)) import Imports (Maybe (Just, Nothing), fromJust, read, (&)) import Wire.API.Event.Team - ( Event, - EventData - ( EdConvCreate, - EdConvDelete, - EdMemberJoin, - EdMemberLeave, - EdMemberUpdate, - EdTeamCreate, - EdTeamUpdate - ), - EventType - ( ConvCreate, - ConvDelete, - MemberJoin, - MemberLeave, - MemberUpdate, - TeamCreate, - TeamDelete, - TeamUpdate - ), - eventData, - newEvent, - ) import Wire.API.Team - ( TeamBinding (Binding, NonBinding), - TeamUpdateData - ( TeamUpdateData, - _iconKeyUpdate, - _iconUpdate, - _nameUpdate - ), - newTeam, - teamIconKey, - ) import Wire.API.Team.Permission - ( Perm - ( AddTeamMember, - CreateConversation, - DeleteTeam, - DoNotUseDeprecatedAddRemoveConvMember, - DoNotUseDeprecatedDeleteConversation, - DoNotUseDeprecatedModifyConvName, - GetBilling, - GetMemberPermissions, - GetTeamConversations, - RemoveTeamMember, - SetBilling, - SetMemberPermissions, - SetTeamData - ), - Permissions (Permissions, _copy, _self), - ) testObject_Event_team_1 :: Event testObject_Event_team_1 = ( newEvent - (TeamCreate) ((Id (fromJust (UUID.fromString "0000103e-0000-62d6-0000-7840000079b9")))) (read ("1864-05-15 23:16:24.423381912958 UTC")) - & eventData - .~ ( Just - ( EdTeamCreate - ( newTeam - ((Id (fromJust (UUID.fromString "00000003-0000-0004-0000-000000000001")))) - ((Id (fromJust (UUID.fromString "00000003-0000-0001-0000-000300000002")))) - ("\EOTX\996492h") - ("#\93847\21278(\997485") - (Binding) - & teamIconKey .~ (Nothing) - ) - ) - ) + ( EdTeamCreate + ( newTeam + ((Id (fromJust (UUID.fromString "00000003-0000-0004-0000-000000000001")))) + ((Id (fromJust (UUID.fromString "00000003-0000-0001-0000-000300000002")))) + ("\EOTX\996492h") + ("#\93847\21278(\997485") + (Binding) + & teamIconKey .~ (Nothing) + ) + ) ) testObject_Event_team_2 :: Event testObject_Event_team_2 = ( newEvent - (TeamUpdate) ((Id (fromJust (UUID.fromString "000019fb-0000-03a5-0000-009c00006067")))) (read ("1864-05-06 06:03:20.68447167825 UTC")) - & eventData - .~ ( Just - ( EdTeamUpdate - ( TeamUpdateData - { _nameUpdate = - Just - ( unsafeRange - ("i5\EOT\1002575\1097973\1066101\&1u\1105430\&1\41840U*/*\999102\1001662\DC3\994167d\1096830\&4uG\173887\fUh09\\\1028574\vPy\t\171003\SI\GS0bV\CAN]\17049\96404\15202\RS\SYNX\ESC3[\CANf\NAK") - ), - _iconUpdate = fromByteString' "3-1-f595b8ed-6dcf-41f2-8a2f-f662a9c0fce4", - _iconKeyUpdate = - Just (unsafeRange ("\131355Pp\1067299\987603\ENQS\22773S\ACK\NAKmM\19084\&0\19257\31361$rL,XvJ")) - } - ) - ) - ) + ( EdTeamUpdate + ( TeamUpdateData + { _nameUpdate = + Just + ( unsafeRange + ("i5\EOT\1002575\1097973\1066101\&1u\1105430\&1\41840U*/*\999102\1001662\DC3\994167d\1096830\&4uG\173887\fUh09\\\1028574\vPy\t\171003\SI\GS0bV\CAN]\17049\96404\15202\RS\SYNX\ESC3[\CANf\NAK") + ), + _iconUpdate = fromByteString' "3-1-f595b8ed-6dcf-41f2-8a2f-f662a9c0fce4", + _iconKeyUpdate = + Just (unsafeRange ("\131355Pp\1067299\987603\ENQS\22773S\ACK\NAKmM\19084\&0\19257\31361$rL,XvJ")) + } + ) + ) ) testObject_Event_team_3 :: Event testObject_Event_team_3 = ( newEvent - (MemberJoin) ((Id (fromJust (UUID.fromString "00000bfa-0000-53cd-0000-2f8e00004e38")))) (read ("1864-04-20 19:30:43.065358805164 UTC")) - & eventData .~ (Just (EdMemberJoin (Id (fromJust (UUID.fromString "000030c1-0000-1c28-0000-71af000036f3"))))) + (EdMemberJoin (Id (fromJust (UUID.fromString "000030c1-0000-1c28-0000-71af000036f3")))) ) testObject_Event_team_4 :: Event testObject_Event_team_4 = ( newEvent - (TeamUpdate) ((Id (fromJust (UUID.fromString "000060cd-0000-2fae-0000-3620000011d4")))) (read ("1864-06-07 17:44:20.841616476784 UTC")) - & eventData - .~ ( Just - ( EdTeamUpdate - ( TeamUpdateData - { _nameUpdate = - Just - ( unsafeRange - ("d\SI\172132@o\988798s&na\136232\1090952\149487|\83503\1016948/\989099v\NAKu\DC2f\1093640\1011936KC\47338\1066997\1059386\&9_\v_^\1045398K\155463\SO Y*T\CAN\1086598<\1056774>\171907\4929\rt\1038163\1072126w2E\127366hS>\ACK_PQN,Vk\SYN\1083970=90\EM2e\984550\USVA!\EM\FS\EOTe;\189780\&1\171907\4929\rt\1038163\1072126w2E\127366hS>\ACK_PQN,Vk\SYN\1083970=90\EM2e\984550\USVA!\EM\FS\EOTe;\189780\&1 --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Wire.API.Golden.Generated.UserIdentity_user where - -import Imports (Maybe (Just, Nothing)) -import Wire.API.User - ( Email (Email, emailDomain, emailLocal), - Phone (Phone, fromPhone), - UserIdentity (..), - UserSSOId (UserSSOId, UserScimExternalId), - ) -import Wire.API.User.Identity (mkSimpleSampleUref) - -testObject_UserIdentity_user_1 :: UserIdentity -testObject_UserIdentity_user_1 = - EmailIdentity (Email {emailLocal = "S\ENQX\1076723$\STX\"\1110507e\1015716\24831\1031964L\ETB", emailDomain = "P.b"}) - -testObject_UserIdentity_user_2 :: UserIdentity -testObject_UserIdentity_user_2 = - EmailIdentity - ( Email - { emailLocal = "\1061008\1068189\1013266\EOT\vE\ENQW\SYNO\DC3X_F\9141\STX $}\179559\USJ3\128480S?", - emailDomain = "4WL;'\DLEl1]x\119077" - } - ) - -testObject_UserIdentity_user_3 :: UserIdentity -testObject_UserIdentity_user_3 = - EmailIdentity - ( Email - { emailLocal = "\10821:\DC4E\60072i\1074224P\1054022\1037567\&6phe\DC3\ETXH,\CAN\v\145604\v>", - emailDomain = "bwtC\1110390z2RT28\STX\1049837<3Y" - } - ) - -testObject_UserIdentity_user_4 :: UserIdentity -testObject_UserIdentity_user_4 = - FullIdentity - (Email {emailLocal = "\rH)\65718", emailDomain = ")\1107842\US\27126\t\ACK\1111725_{\154804\&7#"}) - (Phone {fromPhone = "+2559583362"}) - -testObject_UserIdentity_user_5 :: UserIdentity -testObject_UserIdentity_user_5 = - SSOIdentity (UserSSOId mkSimpleSampleUref) Nothing (Just (Phone {fromPhone = "+49198172826"})) - -testObject_UserIdentity_user_6 :: UserIdentity -testObject_UserIdentity_user_6 = PhoneIdentity (Phone {fromPhone = "+03038459796465"}) - -testObject_UserIdentity_user_7 :: UserIdentity -testObject_UserIdentity_user_7 = PhoneIdentity (Phone {fromPhone = "+805676294"}) - -testObject_UserIdentity_user_8 :: UserIdentity -testObject_UserIdentity_user_8 = SSOIdentity (UserSSOId mkSimpleSampleUref) Nothing (Just (Phone {fromPhone = "+149548802116267"})) - -testObject_UserIdentity_user_9 :: UserIdentity -testObject_UserIdentity_user_9 = - EmailIdentity - ( Email - { emailLocal = "'\ACKB\1000542\&90\NAKKK\EOTin\1096701r\EOT", - emailDomain = "Jj\\\172302>nY\9522\987654VO\DC2Q\r_:$\7618\EOTc~H8e}{g" - } - ) - -testObject_UserIdentity_user_10 :: UserIdentity -testObject_UserIdentity_user_10 = - EmailIdentity - ( Email - { emailLocal = "No\b\1006784b=`yl\133702p.w\1048001\142089\DC4\149735lm\183993&j9\a", - emailDomain = "\1054243.1\1031882\ETB_\1053320Q\1087931z.Ywe\1016096\39626>" - } - ) - -testObject_UserIdentity_user_11 :: UserIdentity -testObject_UserIdentity_user_11 = PhoneIdentity (Phone {fromPhone = "+755837448"}) - -testObject_UserIdentity_user_12 :: UserIdentity -testObject_UserIdentity_user_12 = - EmailIdentity (Email {emailLocal = "K\1012027\DC2", emailDomain = "\DC4N0Q\4986rva\NAK5\1080896+S\1070062;\FS%\NAK"}) - -testObject_UserIdentity_user_13 :: UserIdentity -testObject_UserIdentity_user_13 = - FullIdentity - (Email {emailLocal = "e\ACK\1036331\1062258vN:%\1058229\SUBSi\1035816Qq", emailDomain = ""}) - (Phone {fromPhone = "+387350906"}) - -testObject_UserIdentity_user_14 :: UserIdentity -testObject_UserIdentity_user_14 = - FullIdentity - ( Email - { emailLocal = "\1004575\184062\CAN\92545\&3\US<=gg", - emailDomain = "\1035369\1022539Nbo\tQ:\1085902f\136614L\1009643" - } - ) - (Phone {fromPhone = "+79378139213406"}) - -testObject_UserIdentity_user_15 :: UserIdentity -testObject_UserIdentity_user_15 = PhoneIdentity (Phone {fromPhone = "+092380942233194"}) - -testObject_UserIdentity_user_16 :: UserIdentity -testObject_UserIdentity_user_16 = - SSOIdentity - (UserSSOId mkSimpleSampleUref) - (Just (Email {emailLocal = "%x\DC3\1049873\EOT.", emailDomain = "G\48751t.6"})) - (Just (Phone {fromPhone = "+298116118047"})) - -testObject_UserIdentity_user_17 :: UserIdentity -testObject_UserIdentity_user_17 = - SSOIdentity (UserScimExternalId "") (Just (Email {emailLocal = "\GS\FS1k", emailDomain = "CV7\147439K"})) Nothing - -testObject_UserIdentity_user_18 :: UserIdentity -testObject_UserIdentity_user_18 = PhoneIdentity (Phone {fromPhone = "+7322674905"}) - -testObject_UserIdentity_user_19 :: UserIdentity -testObject_UserIdentity_user_19 = PhoneIdentity (Phone {fromPhone = "+133514352685272"}) - -testObject_UserIdentity_user_20 :: UserIdentity -testObject_UserIdentity_user_20 = - FullIdentity - (Email {emailLocal = "\133292A", emailDomain = "|\1083873\1005880N<\DC3z9\NAKV;^\1015230"}) - (Phone {fromPhone = "+926403020"}) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs index 0e3ea5512b..143b58f520 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs @@ -83,7 +83,7 @@ protoTestObject :: IO Bool protoTestObject obj path = do let actual = toProto obj - msg <- assertRight (decodeMessage @m (LBS.toStrict actual)) + msg <- assertRight (decodeMessage @m actual) let pretty = render (pprintMessage msg) dir = "test/golden" fullPath = dir <> "/" <> path @@ -100,7 +100,7 @@ protoTestObject obj path = do assertEqual (show (typeRep @a) <> ": FromProto of " <> path <> " should match object") (Right obj) - (fromProto (LBS.fromStrict (encodeMessage expected))) + (fromProto (encodeMessage expected)) pure exists diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_1.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_1.json deleted file mode 100644 index 029c472a9d..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_1.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "S\u0005X􆷳$\u0002\"􏇫e󷾤惿󻼜L\u0017@P.b", - "phone": null, - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_10.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_10.json deleted file mode 100644 index 0142780ab7..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_10.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "No\u0008󵳀b=`yl𠩆p.w󿷁𢬉\u0014𤣧lm𬺹&j9\u0007@􁘣.1󻻊\u0017_􁊈Q􉦻z.Ywe󸄠髊>", - "phone": null, - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_11.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_11.json deleted file mode 100644 index bff6512261..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_11.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": null, - "phone": "+755837448", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_12.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_12.json deleted file mode 100644 index 6d4144937f..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_12.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "K󷄻\u0012@\u0014N0Q፺rva\u00155􇹀+S􅏮;\u001c%\u0015", - "phone": null, - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_13.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_13.json deleted file mode 100644 index c4632b61e5..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_13.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "e\u0006󽀫􃕲vN:%􂖵\u001aSi󼸨Qq@", - "phone": "+387350906", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_14.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_14.json deleted file mode 100644 index 5d2a9ce392..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_14.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "󵐟𬻾\u0018𖦁3\u001f<=gg@󼱩󹩋Nbo\tQ:􉇎f𡖦L󶟫", - "phone": "+79378139213406", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_15.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_15.json deleted file mode 100644 index 35fa355d0f..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_15.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": null, - "phone": "+092380942233194", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_17.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_17.json deleted file mode 100644 index de51e10642..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_17.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "email": "\u001d\u001c1k@CV7𣿯K", - "phone": null, - "sso_id": { - "scim_external_id": "" - } -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_18.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_18.json deleted file mode 100644 index 4c7aa3559a..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_18.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": null, - "phone": "+7322674905", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_19.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_19.json deleted file mode 100644 index 2d276e9f0a..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_19.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": null, - "phone": "+133514352685272", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_2.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_2.json deleted file mode 100644 index ed26790334..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_2.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "􃂐􄲝󷘒\u0004\u000bE\u0005W\u0016O\u0013X_F⎵\u0002 $}𫵧\u001fJ3🗠S?@4WL;'\u0010l1]x𝄥", - "phone": null, - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_20.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_20.json deleted file mode 100644 index c2b067e0ec..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_20.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "𠢬A@|􈧡󵤸N<\u0013z9\u0015V;^󷶾", - "phone": "+926403020", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_3.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_3.json deleted file mode 100644 index 12387a4c4e..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_3.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "⩅:\u0014Ei􆐰P􁕆󽓿6phe\u0013\u0003H,\u0018\u000b𣣄\u000b>@bwtC􏅶z2RT28\u0002􀓭<3Y", - "phone": null, - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_4.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_4.json deleted file mode 100644 index 19e7f13385..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_4.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "\rH)𐂶@)􎞂\u001f槶\t\u0006􏚭_{𥲴7#", - "phone": "+2559583362", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_6.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_6.json deleted file mode 100644 index c841bfca76..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_6.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": null, - "phone": "+03038459796465", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_7.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_7.json deleted file mode 100644 index 5edc20024a..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_7.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": null, - "phone": "+805676294", - "sso_id": null -} diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_9.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_9.json deleted file mode 100644 index 2500b91fd2..0000000000 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_9.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "email": "'\u0006B󴑞90\u0015KK\u0004in􋯽r\u0004@Jj\\𪄎>nY┲󱈆VO\u0012Q\r_:$᷂\u0004c~H8e}{g", - "phone": null, - "sso_id": null -} diff --git a/libs/wire-api/test/resources/app_message1.mls b/libs/wire-api/test/resources/app_message1.mls new file mode 100644 index 0000000000..0426a5a98f Binary files /dev/null and b/libs/wire-api/test/resources/app_message1.mls differ diff --git a/libs/wire-api/test/resources/commit1.mls b/libs/wire-api/test/resources/commit1.mls new file mode 100644 index 0000000000..c8f40b1bf1 Binary files /dev/null and b/libs/wire-api/test/resources/commit1.mls differ diff --git a/libs/wire-api/test/resources/welcome1.mls b/libs/wire-api/test/resources/welcome1.mls new file mode 100644 index 0000000000..0a628b1097 Binary files /dev/null and b/libs/wire-api/test/resources/welcome1.mls differ diff --git a/libs/wire-api/test/unit/Test/Wire/API/Conversation.hs b/libs/wire-api/test/unit/Test/Wire/API/Conversation.hs index 27d198a22f..f9df9a9af2 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Conversation.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Conversation.hs @@ -3,7 +3,7 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2020 Wire Swiss GmbH +-- Copyright (C) 2022 Wire Swiss GmbH -- -- This program is free software: you can redistribute it and/or modify it under -- the terms of the GNU Affero General Public License as published by the Free diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs index 662b94662a..9302b91079 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs @@ -20,6 +20,8 @@ module Test.Wire.API.MLS where import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.Domain +import Data.Either.Combinators +import Data.Hex import Data.Id import qualified Data.Text as T import qualified Data.UUID as UUID @@ -27,14 +29,21 @@ import Imports import Test.Tasty import Test.Tasty.HUnit import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Commit import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Message +import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation +import Wire.API.MLS.Welcome tests :: TestTree tests = testGroup "MLS" $ - [ testCase "parse key packages" testParseKeyPackage + [ testCase "parse key package" testParseKeyPackage, + testCase "parse commit message" testParseCommit, + testCase "parse application message" testParseApplication, + testCase "parse welcome message" testParseWelcome ] testParseKeyPackage :: IO () @@ -55,3 +64,52 @@ testParseKeyPackage = do ciUser = Id (fromJust (UUID.fromString "b455a431-9db6-4404-86e7-6a3ebe73fcaf")), ciClient = newClientId 0x3ae58155 } + +testParseCommit :: IO () +testParseCommit = do + msgData <- LBS.readFile "test/resources/commit1.mls" + msg :: Message 'MLSPlainText <- case decodeMLS @SomeMessage msgData of + Left err -> assertFailure (T.unpack err) + Right (SomeMessage SMLSCipherText _) -> + assertFailure "Expected plain text message, found encrypted" + Right (SomeMessage SMLSPlainText msg) -> + pure msg + + msgGroupId msg @?= "test_group" + msgEpoch msg @?= 0 + + case msgSender msg of + MemberSender kp -> kp @?= KeyPackageRef (fromRight' (unhex "24e4b0a802a2b81f00a9af7df5e91da8")) + _ -> assertFailure "Unexpected sender type" + + let payload = msgPayload msg + commit <- case msgTBS payload of + CommitMessage c -> pure c + _ -> assertFailure "Unexpected message type" + + case cProposals commit of + [Inline (AddProposal _)] -> pure () + _ -> assertFailure "Unexpected proposals" + +testParseApplication :: IO () +testParseApplication = do + msgData <- LBS.readFile "test/resources/app_message1.mls" + msg :: Message 'MLSCipherText <- case decodeMLS @SomeMessage msgData of + Left err -> assertFailure (T.unpack err) + Right (SomeMessage SMLSCipherText msg) -> pure msg + Right (SomeMessage SMLSPlainText _) -> + assertFailure "Expected encrypted message, found plain text" + + msgGroupId msg @?= "test_group" + msgEpoch msg @?= 0 + msgContentType (msgPayload msg) @?= fromMLSEnum ApplicationMessageTag + +testParseWelcome :: IO () +testParseWelcome = do + welData <- LBS.readFile "test/resources/welcome1.mls" + wel <- case decodeMLS welData of + Left err -> assertFailure (T.unpack err) + Right x -> pure x + + welCipherSuite wel @?= CipherSuite 1 + map gsNewMember (welSecrets wel) @?= [KeyPackageRef (fromRight' (unhex "ab4692703ca6d50ffdeaae3096f885c2"))] diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 5ff334d8a1..b63079c202 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -297,7 +297,6 @@ tests = testRoundTrip @User.Identity.Email, testRoundTrip @User.Identity.Phone, testRoundTrip @User.Identity.UserSSOId, - testRoundTrip @User.Identity.UserIdentity, testRoundTrip @User.Password.NewPasswordReset, testRoundTrip @User.Password.PasswordResetKey, -- FUTUREWORK: this should probably be tested individually, diff --git a/libs/wire-api/test/unit/Test/Wire/API/User.hs b/libs/wire-api/test/unit/Test/Wire/API/User.hs index 2b23905559..5c05e6a619 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/User.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/User.hs @@ -26,6 +26,7 @@ import Data.Domain import Data.Id import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldNoConsent)) import Data.Qualified +import Data.Schema (schemaIn) import qualified Data.UUID.V4 as UUID import Imports import Test.Tasty @@ -53,28 +54,28 @@ testUserProfile = do parseIdentityTests :: [TestTree] parseIdentityTests = - [ let (=#=) :: Either String (Maybe UserIdentity) -> (Maybe UserSSOId, [Pair]) -> Assertion - (=#=) uid (mssoid, object -> Object obj) = assertEqual "=#=" uid (parseEither (parseIdentity mssoid) obj) + [ let (=#=) :: Either String (Maybe UserIdentity) -> [Pair] -> Assertion + (=#=) uid (object -> Object obj) = assertEqual "=#=" uid (parseEither (schemaIn maybeUserIdentityObjectSchema) obj) (=#=) _ bad = error $ "=#=: impossible: " <> show bad in testGroup "parseIdentity" [ testCase "FullIdentity" $ - Right (Just (FullIdentity hemail hphone)) =#= (Nothing, [email, phone]), + Right (Just (FullIdentity hemail hphone)) =#= [email, phone], testCase "EmailIdentity" $ - Right (Just (EmailIdentity hemail)) =#= (Nothing, [email]), + Right (Just (EmailIdentity hemail)) =#= [email], testCase "PhoneIdentity" $ - Right (Just (PhoneIdentity hphone)) =#= (Nothing, [phone]), + Right (Just (PhoneIdentity hphone)) =#= [phone], testCase "SSOIdentity" $ do - Right (Just (SSOIdentity hssoid Nothing Nothing)) =#= (Just hssoid, [ssoid]) - Right (Just (SSOIdentity hssoid Nothing (Just hphone))) =#= (Just hssoid, [ssoid, phone]) - Right (Just (SSOIdentity hssoid (Just hemail) Nothing)) =#= (Just hssoid, [ssoid, email]) - Right (Just (SSOIdentity hssoid (Just hemail) (Just hphone))) =#= (Just hssoid, [ssoid, email, phone]), + Right (Just (SSOIdentity hssoid Nothing Nothing)) =#= [ssoid] + Right (Just (SSOIdentity hssoid Nothing (Just hphone))) =#= [ssoid, phone] + Right (Just (SSOIdentity hssoid (Just hemail) Nothing)) =#= [ssoid, email] + Right (Just (SSOIdentity hssoid (Just hemail) (Just hphone))) =#= [ssoid, email, phone], testCase "Bad phone" $ - Left "Error in $.phone: Invalid phone number. Expected E.164 format." =#= (Nothing, [badphone]), + Left "Error in $.phone: Invalid phone number. Expected E.164 format." =#= [badphone], testCase "Bad email" $ - Left "Error in $.email: Invalid email. Expected '@'." =#= (Nothing, [bademail]), + Left "Error in $.email: Invalid email. Expected '@'." =#= [bademail], testCase "Nothing" $ - Right Nothing =#= (Nothing, [("something_unrelated", "#")]) + Right Nothing =#= [("something_unrelated", "#")] ] ] where diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 8fd943c89c..9de414542f 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -37,11 +37,15 @@ library Wire.API.Message Wire.API.Message.Proto Wire.API.MLS.CipherSuite + Wire.API.MLS.Commit Wire.API.MLS.Credential + Wire.API.MLS.Group Wire.API.MLS.KeyPackage + Wire.API.MLS.Message Wire.API.MLS.Proposal Wire.API.MLS.Serialisation Wire.API.MLS.Servant + Wire.API.MLS.Welcome Wire.API.Notification Wire.API.Properties Wire.API.Provider @@ -452,7 +456,6 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.UserConnection_user Test.Wire.API.Golden.Generated.UserConnectionList_user Test.Wire.API.Golden.Generated.UserHandleInfo_user - Test.Wire.API.Golden.Generated.UserIdentity_user Test.Wire.API.Golden.Generated.UserLegalHoldStatusResponse_team Test.Wire.API.Golden.Generated.UserProfile_user Test.Wire.API.Golden.Generated.UserSSOId_user @@ -643,6 +646,7 @@ test-suite wire-api-tests , containers >=0.5 , currency-codes , directory + , either , filepath , hex , hscim @@ -656,6 +660,7 @@ test-suite wire-api-tests , pretty , proto-lens , saml2-web-sso + , schema-profunctor , servant , servant-swagger-ui , string-conversions diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index bb97ae837a..f1f4ef9773 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -112,7 +112,7 @@ connError NotConnected {} = StdError (errorDescriptionTypeToWai @NotConnected) connError InvalidUser {} = StdError (errorDescriptionTypeToWai @InvalidUser) connError ConnectNoIdentity {} = StdError (errorDescriptionToWai (noIdentity 0)) connError (ConnectBlacklistedUserKey k) = StdError $ foldKey (const blacklistedEmail) (const (errorDescriptionTypeToWai @BlacklistedPhone)) k -connError (ConnectInvalidEmail _ _) = StdError invalidEmail +connError (ConnectInvalidEmail _ _) = StdError (errorDescriptionTypeToWai @InvalidEmail) connError ConnectInvalidPhone {} = StdError (errorDescriptionTypeToWai @InvalidPhone) connError ConnectSameBindingTeamUsers = StdError sameBindingTeamUsers connError ConnectMissingLegalholdConsent = StdError (errorDescriptionTypeToWai @MissingLegalholdConsent) @@ -120,8 +120,9 @@ connError (ConnectFederationError e) = fedError e actError :: ActivationError -> Error actError (UserKeyExists _) = StdError (errorDescriptionTypeToWai @UserKeyExists) -actError (InvalidActivationCode e) = StdError (invalidActivationCode e) -actError (InvalidActivationEmail _ _) = StdError invalidEmail +actError InvalidActivationCodeWrongUser = StdError (errorDescriptionTypeToWai @InvalidActivationCodeWrongUser) +actError InvalidActivationCodeWrongCode = StdError (errorDescriptionTypeToWai @InvalidActivationCodeWrongCode) +actError (InvalidActivationEmail _ _) = StdError (errorDescriptionTypeToWai @InvalidEmail) actError (InvalidActivationPhone _) = StdError (errorDescriptionTypeToWai @InvalidPhone) pwResetError :: PasswordResetError -> Error @@ -135,30 +136,17 @@ pwResetError (PasswordResetInProgress (Just t)) = [("Retry-After", toByteString' t)] pwResetError ResetPasswordMustDiffer = StdError resetPasswordMustDiffer -newUserError :: CreateUserError -> Error -newUserError InvalidInvitationCode = StdError invalidInvitationCode -newUserError MissingIdentity = StdError missingIdentity -newUserError (InvalidEmail _ _) = StdError invalidEmail -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 (errorDescriptionTypeToWai @BlacklistedPhone)) k -newUserError TooManyTeamMembers = StdError tooManyTeamMembers -newUserError UserCreationRestricted = StdError userCreationRestricted -newUserError (ExternalPreconditionFailed e) = StdError e - sendLoginCodeError :: SendLoginCodeError -> Error sendLoginCodeError (SendLoginInvalidPhone _) = StdError (errorDescriptionTypeToWai @InvalidPhone) sendLoginCodeError SendLoginPasswordExists = StdError passwordExists sendActCodeError :: SendActivationCodeError -> Error -sendActCodeError (InvalidRecipient k) = StdError $ foldKey (const invalidEmail) (const (errorDescriptionTypeToWai @InvalidPhone)) k +sendActCodeError (InvalidRecipient k) = StdError $ foldKey (const (errorDescriptionTypeToWai @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 (InvalidNewEmail _ _) = StdError (errorDescriptionTypeToWai @InvalidEmail) changeEmailError (EmailExists _) = StdError (errorDescriptionTypeToWai @UserKeyExists) changeEmailError (ChangeBlacklistedEmail _) = StdError blacklistedEmail changeEmailError EmailManagedByScim = StdError $ propertyManagedByScim "email" @@ -267,21 +255,12 @@ 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." -invalidEmail :: Wai.Error -invalidEmail = Wai.mkError status400 "invalid-email" "Invalid e-mail address." - invalidPwResetKey :: Wai.Error invalidPwResetKey = Wai.mkError status400 "invalid-key" "Invalid email or mobile number for password reset." resetPasswordMustDiffer :: Wai.Error resetPasswordMustDiffer = Wai.mkError status409 "password-must-differ" "For password reset, new and old password must be different." -invalidInvitationCode :: Wai.Error -invalidInvitationCode = Wai.mkError status400 "invalid-invitation-code" "Invalid invitation code." - -missingIdentity :: Wai.Error -missingIdentity = Wai.mkError status403 "missing-identity" "Using an invitation code requires registering the given email and/or phone." - invalidPwResetCode :: Wai.Error invalidPwResetCode = Wai.mkError status400 "invalid-code" "Invalid password reset code." @@ -414,13 +393,6 @@ sameBindingTeamUsers = Wai.mkError status403 "same-binding-team-users" "Operatio tooManyTeamInvitations :: Wai.Error tooManyTeamInvitations = Wai.mkError status403 "too-many-team-invitations" "Too many team invitations for this team." -tooManyTeamMembers :: Wai.Error -tooManyTeamMembers = Wai.mkError status403 "too-many-team-members" "Too many members in this team." - --- | docs/reference/user/registration.md {#RefRestrictRegistration}. -userCreationRestricted :: Wai.Error -userCreationRestricted = Wai.mkError status403 "user-creation-restricted" "This instance does not allow creation of personal users or teams." - -- | In contrast to 'tooManyFailedLogins', this is about too many *successful* logins. loginsTooFrequent :: Wai.Error loginsTooFrequent = Wai.mkError status429 "client-error" "Logins too frequent" diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index 967e2afefb..9cbfec7a6a 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -25,10 +25,13 @@ module Brig.API.Handler JSON, parseJsonBody, checkWhitelist, + checkWhitelistWithError, + isWhiteListed, + UserNotAllowedToJoinTeam (..), ) where -import Bilge (RequestId (..)) +import Bilge (MonadHttp, RequestId (..)) import Brig.API.Error import qualified Brig.AWS as AWS import Brig.App (AppIO, Env, applog, requestId, runAppT, settings) @@ -40,6 +43,7 @@ import Control.Error import Control.Lens (set, view) import Control.Monad.Catch (catches, throwM) import qualified Control.Monad.Catch as Catch +import Control.Monad.Except (MonadError, throwError) import Data.Aeson (FromJSON) import qualified Data.Aeson as Aeson import Data.Default (def) @@ -60,6 +64,7 @@ import Network.Wai.Utilities.Response (addHeader, json, setStatus) import qualified Network.Wai.Utilities.Server as Server import qualified Servant import System.Logger.Class (Logger) +import Wire.API.ErrorDescription (InvalidEmail) ------------------------------------------------------------------------------- -- HTTP Handler Monad @@ -96,6 +101,11 @@ toServantHandler env action = do Servant.throwError $ Servant.ServerError (mkCode werr) (mkPhrase (WaiError.code werr)) (Aeson.encode body) headers +newtype UserNotAllowedToJoinTeam = UserNotAllowedToJoinTeam WaiError.Error + deriving (Show) + +instance Exception UserNotAllowedToJoinTeam + brigErrorHandlers :: [Catch.Handler IO (Either Error a)] brigErrorHandlers = [ Catch.Handler $ \(ex :: PhoneException) -> @@ -104,8 +114,10 @@ brigErrorHandlers = pure (Left (zauthError ex)), Catch.Handler $ \(ex :: AWS.Error) -> case ex of - AWS.SESInvalidDomain -> pure (Left (StdError invalidEmail)) - _ -> throwM ex + AWS.SESInvalidDomain -> pure (Left (StdError (errorDescriptionTypeToWai @InvalidEmail))) + _ -> throwM ex, + Catch.Handler $ \(UserNotAllowedToJoinTeam e) -> + pure (Left $ StdError e) ] onError :: Logger -> Request -> Continue IO -> Error -> IO ResponseReceived @@ -140,10 +152,16 @@ parseJsonBody req = parseBody req !>> StdError . badRequest -- | If a whitelist is configured, consult it, otherwise a no-op. {#RefActivationWhitelist} checkWhitelist :: Either Email Phone -> (Handler r) () -checkWhitelist key = do +checkWhitelist = checkWhitelistWithError (StdError whitelistError) + +checkWhitelistWithError :: (Monad m, MonadReader Env m, MonadIO m, Catch.MonadMask m, MonadHttp m, MonadError e m) => e -> Either Email Phone -> m () +checkWhitelistWithError e key = do + ok <- isWhiteListed key + unless ok (throwError e) + +isWhiteListed :: (Monad m, MonadReader Env m, MonadIO m, Catch.MonadMask m, MonadHttp m) => Either Email Phone -> m Bool +isWhiteListed key = do eb <- setWhitelist <$> view settings case eb of - Nothing -> return () - Just b -> do - ok <- lift $ Whitelist.verify b key - unless ok (throwStd whitelistError) + Nothing -> pure True + Just b -> Whitelist.verify b key diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 7e21804bf7..a2fc26916f 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -32,6 +32,7 @@ import Brig.API.Types import qualified Brig.API.User as API import Brig.API.Util (validateHandle) import Brig.App +import Brig.Data.Activation import qualified Brig.Data.Client as Data import qualified Brig.Data.Connection as Data import qualified Brig.Data.User as Data @@ -73,6 +74,7 @@ import qualified System.Logger.Class as Log import Wire.API.ErrorDescription import qualified Wire.API.Routes.Internal.Brig as BrigIRoutes import Wire.API.Routes.Internal.Brig.Connection +import Wire.API.Routes.Named import qualified Wire.API.Team.Feature as ApiFt import Wire.API.User import Wire.API.User.Client (UserClientsFull (..)) @@ -82,7 +84,10 @@ import Wire.API.User.RichInfo -- Sitemap (servant) servantSitemap :: ServerT BrigIRoutes.API (Handler r) -servantSitemap = +servantSitemap = ejpdAPI :<|> accountAPI + +ejpdAPI :: ServerT BrigIRoutes.EJPD_API (Handler r) +ejpdAPI = Brig.User.EJPD.ejpdRequest :<|> getAccountFeatureConfig :<|> putAccountFeatureConfig @@ -90,6 +95,9 @@ servantSitemap = :<|> getConnectionsStatusUnqualified :<|> getConnectionsStatus +accountAPI :: ServerT BrigIRoutes.AccountAPI (Handler r) +accountAPI = Named @"createUserNoVerify" createUserNoVerify + -- | Responds with 'Nothing' if field is NULL in existing user or user does not exist. getAccountFeatureConfig :: UserId -> (Handler r) ApiFt.TeamFeatureStatusNoConfig getAccountFeatureConfig uid = @@ -115,13 +123,6 @@ sitemap = do get "/i/status" (continue $ const $ return empty) true head "/i/status" (continue $ const $ return empty) true - -- This endpoint can lead to the following events being sent: - -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID - -- - UserIdentityUpdated event to created user, if email or phone get activated - post "/i/users" (continue createUserNoVerifyH) $ - accept "application" "json" - .&. jsonRequest @NewUser - -- internal email activation (used in tests and in spar for validating emails obtained as -- SAML user identifiers). if the validate query parameter is false or missing, only set -- the activation timeout, but do not send an email, and do not do anything about activating @@ -316,18 +317,9 @@ internalListFullClients :: UserSet -> (AppIO r) UserClientsFull internalListFullClients (UserSet usrs) = UserClientsFull <$> Data.lookupClientsBulk (Set.toList usrs) -createUserNoVerifyH :: JSON ::: JsonRequest NewUser -> (Handler r) Response -createUserNoVerifyH (_ ::: req) = do - CreateUserNoVerifyResponse uid prof <- createUserNoVerify =<< parseJsonBody req - return . setStatus status201 - . addHeader "Location" (toByteString' uid) - $ json prof - -data CreateUserNoVerifyResponse = CreateUserNoVerifyResponse UserId SelfProfile - -createUserNoVerify :: NewUser -> (Handler r) CreateUserNoVerifyResponse -createUserNoVerify uData = do - result <- API.createUser uData !>> newUserError +createUserNoVerify :: NewUser -> (Handler r) (Either RegisterError SelfProfile) +createUserNoVerify uData = lift . runExceptT $ do + result <- API.createUser uData let acc = createdAccount result let usr = accountUser acc let uid = userId usr @@ -336,8 +328,8 @@ createUserNoVerify uData = do for_ (catMaybes [eac, pac]) $ \adata -> let key = ActivateKey $ activationKey adata code = activationCode adata - in API.activate key code (Just uid) !>> actError - return $ CreateUserNoVerifyResponse uid (SelfProfile usr) + in API.activate key code (Just uid) !>> activationErrorToRegisterError + pure (SelfProfile usr) deleteUserNoVerifyH :: UserId -> (Handler r) Response deleteUserNoVerifyH uid = do diff --git a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs index cb9b600ee9..83a9f1e94e 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs @@ -107,10 +107,9 @@ findExtensions :: [Extension] -> Either Text (RequiredExtensions Identity) findExtensions = (checkRequiredExtensions =<<) . getAp . foldMap findExtension findExtension :: Extension -> Ap (Either Text) (RequiredExtensions Maybe) -findExtension ext = flip foldMap (decodeExtension ext) $ \case +findExtension ext = (Ap (decodeExtension ext) >>=) . foldMap $ \case (SomeExtension SLifetimeExtensionTag lt) -> pure $ RequiredExtensions (Just lt) Nothing (SomeExtension SCapabilitiesExtensionTag _) -> pure $ RequiredExtensions Nothing (Just ()) - _ -> Ap (Left "Invalid extension") validateExtensions :: [Extension] -> Handler r () validateExtensions exts = do diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 30d3d4b1df..453adbb751 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -61,7 +61,6 @@ import Control.Error hiding (bool) import Control.Lens (view, (%~), (.~), (?~), (^.), _Just) import Control.Monad.Catch (throwM) import Data.Aeson hiding (json) -import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as Lazy import qualified Data.ByteString.Lazy.Char8 as LBS import qualified Data.Code as Code @@ -113,6 +112,7 @@ import Wire.API.Routes.Version import qualified Wire.API.Swagger as Public.Swagger (models) import qualified Wire.API.Team as Public import Wire.API.Team.LegalHold (LegalholdProtectee (..)) +import Wire.API.User (RegisterError (RegisterErrorWhitelistError)) import qualified Wire.API.User as Public import qualified Wire.API.User.Activation as Public import qualified Wire.API.User.Auth as Public @@ -162,7 +162,7 @@ swaggerDocsAPI = . (S.enum_ . _Just %~ nub) servantSitemap :: ServerT BrigAPI (Handler r) -servantSitemap = userAPI :<|> selfAPI :<|> clientAPI :<|> prekeyAPI :<|> userClientAPI :<|> connectionAPI :<|> mlsAPI +servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekeyAPI :<|> userClientAPI :<|> connectionAPI :<|> mlsAPI where userAPI :: ServerT UserAPI (Handler r) userAPI = @@ -188,6 +188,9 @@ servantSitemap = userAPI :<|> selfAPI :<|> clientAPI :<|> prekeyAPI :<|> userCli :<|> Named @"change-locale" changeLocale :<|> Named @"change-handle" changeHandle + accountAPI :: ServerT AccountAPI (Handler r) + accountAPI = Named @"register" createUser + clientAPI :: ServerT ClientAPI (Handler r) clientAPI = Named @"get-user-clients-unqualified" getUserClientsUnqualified @@ -365,34 +368,7 @@ sitemap = do Doc.response 200 "Object with properties as attributes." Doc.end -- TODO: put delete here, too? - -- /register, /activate, /password-reset ---------------------------------- - - -- docs/reference/user/registration.md {#RefRegistration} - -- - -- This endpoint can lead to the following events being sent: - -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID - -- - UserIdentityUpdated event to created user, if email code or phone code is provided - post "/register" (continue createUserH) $ - accept "application" "json" - .&. jsonRequest @Public.NewUserPublic - document "POST" "register" $ do - Doc.summary "Register a new user." - Doc.notes - "If the environment where the registration takes \ - \place is private and a registered email address or phone \ - \number is not whitelisted, a 403 error is returned." - Doc.body (Doc.ref Public.modelNewUser) $ - Doc.description "JSON body" - -- FUTUREWORK: I think this should be 'Doc.self' instead of 'user' - Doc.returns (Doc.ref Public.modelUser) - Doc.response 201 "User created and pending activation." Doc.end - Doc.errorResponse whitelistError - Doc.errorResponse invalidInvitationCode - Doc.errorResponse missingIdentity - Doc.errorResponse (errorDescriptionTypeToWai @UserKeyExists) - Doc.errorResponse activationCodeNotFound - Doc.errorResponse blacklistedEmail - Doc.errorResponse (errorDescriptionTypeToWai @BlacklistedPhone) + -- /activate, /password-reset ---------------------------------- -- This endpoint can lead to the following events being sent: -- - UserActivated event to the user, if account gets activated @@ -440,7 +416,7 @@ sitemap = do Doc.body (Doc.ref Public.modelSendActivationCode) $ Doc.description "JSON body" Doc.response 200 "Activation code sent." Doc.end - Doc.errorResponse invalidEmail + Doc.errorResponse (errorDescriptionTypeToWai @InvalidEmail) Doc.errorResponse (errorDescriptionTypeToWai @InvalidPhone) Doc.errorResponse (errorDescriptionTypeToWai @UserKeyExists) Doc.errorResponse blacklistedEmail @@ -675,24 +651,13 @@ getRichInfo self user = do getClientPrekeys :: UserId -> ClientId -> (Handler r) [Public.PrekeyId] getClientPrekeys usr clt = lift (API.lookupPrekeyIds usr clt) --- docs/reference/user/registration.md {#RefRegistration} -createUserH :: JSON ::: JsonRequest Public.NewUserPublic -> (Handler r) Response -createUserH (_ ::: req) = do - CreateUserResponse cok loc prof <- createUser =<< parseJsonBody req - lift . Auth.setResponseCookie cok - . setStatus status201 - . addHeader "Location" (toByteString' loc) - $ json prof - -data CreateUserResponse - = CreateUserResponse (Public.Cookie (ZAuth.Token ZAuth.User)) UserId Public.SelfProfile - -createUser :: Public.NewUserPublic -> (Handler r) CreateUserResponse -createUser (Public.NewUserPublic new) = do - API.checkRestrictedUserCreation new !>> newUserError - for_ (Public.newUserEmail new) $ checkWhitelist . Left - for_ (Public.newUserPhone new) $ checkWhitelist . Right - result <- API.createUser new !>> newUserError +-- | docs/reference/user/registration.md {#RefRegistration} +createUser :: Public.NewUserPublic -> (Handler r) (Either Public.RegisterError Public.RegisterSuccess) +createUser (Public.NewUserPublic new) = lift . runExceptT $ do + API.checkRestrictedUserCreation new + for_ (Public.newUserEmail new) $ checkWhitelistWithError RegisterErrorWhitelistError . Left + for_ (Public.newUserPhone new) $ checkWhitelistWithError RegisterErrorWhitelistError . Right + result <- API.createUser new let acc = createdAccount result let eac = createdEmailActivation result @@ -726,10 +691,12 @@ createUser (Public.NewUserPublic new) = do sendActivationSms p c (Just userLocale) for_ (liftM3 (,,) userEmail (createdUserTeam result) newUserTeam) $ \(e, ct, ut) -> sendWelcomeEmail e ct ut (Just userLocale) - cok <- case acc of - UserAccount _ Ephemeral -> lift $ Auth.newCookie @ZAuth.User userId Public.SessionCookie newUserLabel - UserAccount _ _ -> lift $ Auth.newCookie @ZAuth.User userId Public.PersistentCookie newUserLabel - pure $ CreateUserResponse cok userId (Public.SelfProfile usr) + cok <- + Auth.toWebCookie =<< case acc of + UserAccount _ Ephemeral -> lift $ Auth.newCookie @ZAuth.User userId Public.SessionCookie newUserLabel + UserAccount _ _ -> lift $ Auth.newCookie @ZAuth.User userId Public.PersistentCookie newUserLabel + -- pure $ CreateUserResponse cok userId (Public.SelfProfile usr) + pure $ Public.RegisterSuccess cok (Public.SelfProfile usr) where sendActivationEmail :: Public.Email -> Public.Name -> ActivationPair -> Maybe Public.Locale -> Maybe Public.NewTeamUser -> (AppIO r) () sendActivationEmail e u p l mTeamUser diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 447cc3b3ef..1598be9006 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -88,13 +88,14 @@ module Brig.API.User ) where +import Brig.API.Error (errorDescriptionTypeToWai) import qualified Brig.API.Error as Error -import qualified Brig.API.Handler as API (Handler) +import qualified Brig.API.Handler as API (Handler, UserNotAllowedToJoinTeam (..)) import Brig.API.Types import Brig.API.Util import Brig.App import qualified Brig.Code as Code -import Brig.Data.Activation (ActivationEvent (..)) +import Brig.Data.Activation (ActivationEvent (..), activationErrorToRegisterError) import qualified Brig.Data.Activation as Data import qualified Brig.Data.Blacklist as Blacklist import qualified Brig.Data.Client as Data @@ -150,6 +151,7 @@ import Network.Wai.Utilities import qualified System.Logger.Class as Log import System.Logger.Message import UnliftIO.Async +import Wire.API.ErrorDescription import Wire.API.Federation.Error import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Team.Member (legalHoldStatus) @@ -163,21 +165,37 @@ data AllowSCIMUpdates ------------------------------------------------------------------------------- -- Create User -verifyUniquenessAndCheckBlacklist :: UserKey -> ExceptT CreateUserError (AppIO r) () +data IdentityError + = IdentityErrorBlacklistedEmail + | IdentityErrorBlacklistedPhone + | IdentityErrorUserKeyExists + +identityErrorToRegisterError :: IdentityError -> RegisterError +identityErrorToRegisterError = \case + IdentityErrorBlacklistedEmail -> RegisterErrorBlacklistedEmail + IdentityErrorBlacklistedPhone -> RegisterErrorBlacklistedPhone + IdentityErrorUserKeyExists -> RegisterErrorUserKeyExists + +identityErrorToBrigError :: IdentityError -> Error.Error +identityErrorToBrigError = \case + IdentityErrorBlacklistedEmail -> Error.StdError $ errorDescriptionTypeToWai @BlacklistedEmail + IdentityErrorBlacklistedPhone -> Error.StdError $ errorDescriptionTypeToWai @BlacklistedPhone + IdentityErrorUserKeyExists -> Error.StdError $ errorDescriptionTypeToWai @UserKeyExists + +verifyUniquenessAndCheckBlacklist :: UserKey -> ExceptT IdentityError (AppIO r) () verifyUniquenessAndCheckBlacklist uk = do checkKey Nothing uk blacklisted <- lift $ Blacklist.exists uk when blacklisted $ - throwE (BlacklistedUserKey uk) + throwE (foldKey (const IdentityErrorBlacklistedEmail) (const IdentityErrorBlacklistedPhone) uk) where checkKey u k = do av <- lift $ Data.keyAvailable k u unless av $ - throwE $ - DuplicateUserKey k + throwE IdentityErrorUserKeyExists -- docs/reference/user/registration.md {#RefRegistration} -createUser :: NewUser -> ExceptT CreateUserError (AppIO r) CreateUserResult +createUser :: NewUser -> ExceptT RegisterError (AppIO r) CreateUserResult createUser new = do (email, phone) <- validateEmailAndPhone new @@ -276,29 +294,29 @@ createUser new = do where -- NOTE: all functions in the where block don't use any arguments of createUser - validateEmailAndPhone :: NewUser -> ExceptT CreateUserError (AppT r IO) (Maybe Email, Maybe Phone) + validateEmailAndPhone :: NewUser -> ExceptT RegisterError (AppT r IO) (Maybe Email, Maybe Phone) validateEmailAndPhone newUser = do -- Validate e-mail email <- for (newUserEmail newUser) $ \e -> either - (throwE . InvalidEmail e) + (const $ throwE RegisterErrorInvalidEmail) return (validateEmail e) -- Validate phone phone <- for (newUserPhone newUser) $ \p -> maybe - (throwE (InvalidPhone p)) + (throwE RegisterErrorInvalidPhone) return =<< lift (validatePhone p) - for_ (catMaybes [userEmailKey <$> email, userPhoneKey <$> phone]) $ do - verifyUniquenessAndCheckBlacklist + for_ (catMaybes [userEmailKey <$> email, userPhoneKey <$> phone]) $ \k -> + verifyUniquenessAndCheckBlacklist k !>> identityErrorToRegisterError pure (email, phone) - findTeamInvitation :: Maybe UserKey -> InvitationCode -> ExceptT CreateUserError (AppIO r) (Maybe (Team.Invitation, Team.InvitationInfo, TeamId)) - findTeamInvitation Nothing _ = throwE MissingIdentity + findTeamInvitation :: Maybe UserKey -> InvitationCode -> ExceptT RegisterError (AppIO r) (Maybe (Team.Invitation, Team.InvitationInfo, TeamId)) + findTeamInvitation Nothing _ = throwE RegisterErrorMissingIdentity findTeamInvitation (Just e) c = lift (Team.lookupInvitationInfo c) >>= \case Just ii -> do @@ -308,20 +326,20 @@ createUser new = do | e == userEmailKey em -> do _ <- ensureMemberCanJoin (Team.iiTeam ii) return $ Just (invite, ii, Team.iiTeam ii) - _ -> throwE InvalidInvitationCode - Nothing -> throwE InvalidInvitationCode + _ -> throwE RegisterErrorInvalidInvitationCode + Nothing -> throwE RegisterErrorInvalidInvitationCode - ensureMemberCanJoin :: TeamId -> ExceptT CreateUserError (AppIO r) () + ensureMemberCanJoin :: TeamId -> ExceptT RegisterError (AppIO r) () ensureMemberCanJoin tid = do maxSize <- fromIntegral . setMaxTeamSize <$> view settings (TeamSize teamSize) <- TeamSize.teamSize tid when (teamSize >= maxSize) $ - throwE TooManyTeamMembers + throwE RegisterErrorTooManyTeamMembers -- FUTUREWORK: The above can easily be done/tested in the intra call. -- Remove after the next release. canAdd <- lift $ Intra.checkUserCanJoinTeam tid case canAdd of - Just e -> throwE (ExternalPreconditionFailed e) + Just e -> throwM $ API.UserNotAllowedToJoinTeam e Nothing -> pure () acceptTeamInvitation :: @@ -330,18 +348,17 @@ createUser new = do Team.InvitationInfo -> UserKey -> UserIdentity -> - ExceptT CreateUserError (AppT r IO) () + ExceptT RegisterError (AppT r IO) () acceptTeamInvitation account inv ii uk ident = do let uid = userId (accountUser account) ok <- lift $ Data.claimKey uk uid unless ok $ - throwE $ - DuplicateUserKey uk + throwE RegisterErrorUserKeyExists let minvmeta :: (Maybe (UserId, UTCTimeMillis), Team.Role) minvmeta = ((,inCreatedAt inv) <$> inCreatedBy inv, Team.inRole inv) added <- lift $ Intra.addTeamMember uid (Team.iiTeam ii) minvmeta unless added $ - throwE TooManyTeamMembers + throwE RegisterErrorTooManyTeamMembers lift $ do activateUser uid ident -- ('insertAccount' sets column activated to False; here it is set to True.) void $ onActivated (AccountActivated account) @@ -352,12 +369,12 @@ createUser new = do Data.usersPendingActivationRemove uid Team.deleteInvitation (Team.inTeam inv) (Team.inInvitation inv) - addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> ExceptT CreateUserError (AppIO r) CreateUserTeam + addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> ExceptT RegisterError (AppIO r) CreateUserTeam addUserToTeamSSO account tid ident = do let uid = userId (accountUser account) added <- lift $ Intra.addTeamMember uid tid (Nothing, Team.defaultRole) unless added $ - throwE TooManyTeamMembers + throwE RegisterErrorTooManyTeamMembers lift $ do activateUser uid ident void $ onActivated (AccountActivated account) @@ -369,7 +386,7 @@ createUser new = do pure $ CreateUserTeam tid nm -- Handle e-mail activation (deprecated, see #RefRegistrationNoPreverification in /docs/reference/user/registration.md) - handleEmailActivation :: Maybe Email -> UserId -> Maybe BindingNewTeamUser -> ExceptT CreateUserError (AppT r IO) (Maybe Activation) + handleEmailActivation :: Maybe Email -> UserId -> Maybe BindingNewTeamUser -> ExceptT RegisterError (AppT r IO) (Maybe Activation) handleEmailActivation email uid newTeam = do fmap join . for (userEmailKey <$> email) $ \ek -> case newUserEmailCode new of Nothing -> do @@ -382,13 +399,15 @@ createUser new = do return $ Just edata Just c -> do ak <- liftIO $ Data.mkActivationKey ek - void $ activateWithCurrency (ActivateKey ak) c (Just uid) (bnuCurrency =<< newTeam) !>> EmailActivationError + void $ + activateWithCurrency (ActivateKey ak) c (Just uid) (bnuCurrency =<< newTeam) + !>> activationErrorToRegisterError return Nothing -- Handle phone activation (deprecated, see #RefRegistrationNoPreverification in /docs/reference/user/registration.md) - handlePhoneActivation :: Maybe Phone -> UserId -> ExceptT CreateUserError (AppT r IO) (Maybe Activation) + handlePhoneActivation :: Maybe Phone -> UserId -> ExceptT RegisterError (AppT r IO) (Maybe Activation) handlePhoneActivation phone uid = do - pdata <- fmap join . for (userPhoneKey <$> phone) $ \pk -> case newUserPhoneCode new of + fmap join . for (userPhoneKey <$> phone) $ \pk -> case newUserPhoneCode new of Nothing -> do timeout <- setActivationTimeout <$> view settings pdata <- lift $ Data.newActivation pk timeout (Just uid) @@ -399,9 +418,8 @@ createUser new = do return $ Just pdata Just c -> do ak <- liftIO $ Data.mkActivationKey pk - void $ activate (ActivateKey ak) c (Just uid) !>> PhoneActivationError + void $ activate (ActivateKey ak) c (Just uid) !>> activationErrorToRegisterError return Nothing - pure pdata initAccountFeatureConfig :: UserId -> (AppIO r) () initAccountFeatureConfig uid = do @@ -412,10 +430,10 @@ initAccountFeatureConfig uid = do -- all over the place there, we add a new function that handles just the one new flow where -- users are invited to the team via scim. createUserInviteViaScim :: UserId -> NewUserScimInvitation -> ExceptT Error.Error (AppIO r) UserAccount -createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail) = (`catchE` (throwE . Error.newUserError)) $ do - email <- either (throwE . InvalidEmail rawEmail) pure (validateEmail rawEmail) +createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail) = do + email <- either (const . throwE . Error.StdError $ errorDescriptionTypeToWai @InvalidEmail) pure (validateEmail rawEmail) let emKey = userEmailKey email - verifyUniquenessAndCheckBlacklist emKey + verifyUniquenessAndCheckBlacklist emKey !>> identityErrorToBrigError account <- lift $ newAccountInviteViaScim uid tid loc name email Log.debug $ field "user" (toByteString . userId . accountUser $ account) . field "action" (Log.val "User.createUserInviteViaScim") @@ -428,7 +446,6 @@ createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail) = (`ca lift $ Data.usersPendingActivationAdd (UserPendingActivation uid expiresAt) let activated = - -- It would be nice to set this to 'False' to make sure we're not accidentally -- treating 'PendingActivation' as 'Active', but then 'Brig.Data.User.toIdentity' -- would not produce an identity, and so we won't have the email address to construct -- the SCIM user. @@ -438,7 +455,7 @@ createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail) = (`ca return account -- | docs/reference/user/registration.md {#RefRestrictRegistration}. -checkRestrictedUserCreation :: NewUser -> ExceptT CreateUserError (AppIO r) () +checkRestrictedUserCreation :: NewUser -> ExceptT RegisterError (AppIO r) () checkRestrictedUserCreation new = do restrictPlease <- lift . asks $ fromMaybe False . setRestrictUserCreation . view settings when @@ -446,7 +463,7 @@ checkRestrictedUserCreation new = do && not (isNewUserTeamMember new) && not (isNewUserEphemeral new) ) - $ throwE UserCreationRestricted + $ throwE RegisterErrorUserCreationRestricted ------------------------------------------------------------------------------- -- Update Profile diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 919f93df76..41c99b08b0 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -22,6 +22,7 @@ module Brig.Data.Activation ActivationCode (..), ActivationEvent (..), ActivationError (..), + activationErrorToRegisterError, newActivation, mkActivationKey, lookupActivationCode, @@ -48,6 +49,7 @@ import Imports import OpenSSL.BN (randIntegerZeroToNMinusOne) import OpenSSL.EVP.Digest (digestBS, getDigestByName) import Text.Printf (printf) +import Wire.API.User -- | The information associated with the pending activation of a 'UserKey'. data Activation = Activation @@ -60,10 +62,19 @@ data Activation = Activation data ActivationError = UserKeyExists !LT.Text - | InvalidActivationCode !LT.Text + | InvalidActivationCodeWrongUser + | InvalidActivationCodeWrongCode | InvalidActivationEmail !Email !String | InvalidActivationPhone !Phone +activationErrorToRegisterError :: ActivationError -> RegisterError +activationErrorToRegisterError = \case + UserKeyExists _ -> RegisterErrorUserKeyExists + InvalidActivationCodeWrongUser -> RegisterErrorInvalidActivationCodeWrongUser + InvalidActivationCodeWrongCode -> RegisterErrorInvalidActivationCodeWrongCode + InvalidActivationEmail _ _ -> RegisterErrorInvalidEmail + InvalidActivationPhone _ -> RegisterErrorInvalidPhone + data ActivationEvent = AccountActivated !UserAccount | EmailActivated !UserId !Email @@ -189,10 +200,10 @@ deleteActivationPair :: ActivationKey -> (AppIO r) () deleteActivationPair = write keyDelete . params LocalQuorum . Identity invalidUser :: ActivationError -invalidUser = InvalidActivationCode "User does not exist." +invalidUser = InvalidActivationCodeWrongUser -- "User does not exist." invalidCode :: ActivationError -invalidCode = InvalidActivationCode "Invalid activation code" +invalidCode = InvalidActivationCodeWrongCode -- "Invalid activation code" keyInsert :: PrepQuery W (ActivationKey, Text, Text, ActivationCode, Maybe UserId, Int32, Int32) () keyInsert = diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 8c50699662..f6028eafba 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -324,7 +324,7 @@ newAccount :: Public.NewProvider -> (Handler r) Public.NewProviderResponse newAccount new = do email <- case validateEmail (Public.newProviderEmail new) of Right em -> return em - Left _ -> throwStd invalidEmail + Left _ -> throwStd (errorDescriptionTypeToWai @InvalidEmail) let name = Public.newProviderName new let pass = Public.newProviderPassword new let descr = fromRange (Public.newProviderDescr new) @@ -386,7 +386,7 @@ getActivationCode :: Public.Email -> (Handler r) FoundActivationCode getActivationCode e = do email <- case validateEmail e of Right em -> return em - Left _ -> throwStd invalidEmail + Left _ -> throwStd (errorDescriptionTypeToWai @InvalidEmail) gen <- Code.mkGen (Code.ForEmail email) code <- Code.lookup (Code.genKey gen) Code.IdentityVerification maybe (throwStd activationKeyNotFound) (return . FoundActivationCode) code @@ -496,7 +496,7 @@ updateAccountEmail :: ProviderId -> Public.EmailUpdate -> (Handler r) () updateAccountEmail pid (Public.EmailUpdate new) = do email <- case validateEmail new of Right em -> return em - Left _ -> throwStd invalidEmail + Left _ -> throwStd (errorDescriptionTypeToWai @InvalidEmail) let emailKey = mkEmailKey email DB.lookupKey emailKey >>= mapM_ (const $ throwStd emailExists) gen <- Code.mkGen (Code.ForEmail email) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 4ebdb9c2f9..5b18709bf0 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -90,7 +90,7 @@ routesPublic = do Doc.response 201 "Invitation was created and sent." Doc.end Doc.errorResponse noEmail Doc.errorResponse (errorDescriptionToWai (noIdentity 6)) - Doc.errorResponse invalidEmail + Doc.errorResponse (errorDescriptionTypeToWai @InvalidEmail) Doc.errorResponse blacklistedEmail Doc.errorResponse tooManyTeamInvitations @@ -149,7 +149,7 @@ routesPublic = do Doc.description "Invitation code" Doc.returns (Doc.ref Public.modelTeamInvitation) Doc.response 200 "Invitation successful." Doc.end - Doc.errorResponse invalidInvitationCode + Doc.errorResponse (errorDescriptionTypeToWai @InvalidInvitationCode) -- FUTUREWORK: Add another endpoint to allow resending of invitation codes head "/teams/invitations/by-email" (continue headInvitationByEmailH) $ @@ -228,7 +228,7 @@ getInvitationCodeH (_ ::: t ::: r) = do getInvitationCode :: TeamId -> InvitationId -> (Handler r) FoundInvitationCode getInvitationCode t r = do code <- lift $ DB.lookupInvitationCode t r - maybe (throwStd invalidInvitationCode) (return . FoundInvitationCode) code + maybe (throwStd $ errorDescriptionTypeToWai @InvalidInvitationCode) (return . FoundInvitationCode) code data FoundInvitationCode = FoundInvitationCode InvitationCode deriving (Eq, Show, Generic) @@ -321,7 +321,7 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do -- sendActivationCode. Refactor this to a single place -- Validate e-mail - inviteeEmail <- either (const $ throwStd invalidEmail) return (Email.validateEmail (irInviteeEmail body)) + inviteeEmail <- either (const $ throwStd (errorDescriptionTypeToWai @InvalidEmail)) return (Email.validateEmail (irInviteeEmail body)) let uke = userEmailKey inviteeEmail blacklistedEm <- lift $ Blacklist.exists uke when blacklistedEm $ @@ -404,7 +404,7 @@ getInvitationByCodeH (_ ::: c) = do getInvitationByCode :: Public.InvitationCode -> (Handler r) Public.Invitation getInvitationByCode c = do inv <- lift $ DB.lookupInvitationByCode c - maybe (throwStd invalidInvitationCode) return inv + maybe (throwStd $ errorDescriptionTypeToWai @InvalidInvitationCode) return inv headInvitationByEmailH :: JSON ::: Email -> (Handler r) Response headInvitationByEmailH (_ ::: e) = do diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index fdd67fd5fd..15f227e010 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -61,6 +61,8 @@ import Wire.Swagger as Doc (pendingLoginError) routesPublic :: Routes Doc.ApiBuilder (Handler r) () routesPublic = do + -- Note: this endpoint should always remain available at its unversioned + -- path, since the login cookie hardcodes @/access@ as a path. post "/access" (continue renewH) $ accept "application" "json" .&. tokenRequest @@ -155,7 +157,7 @@ routesPublic = do Doc.description "JSON body" 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 (errorDescriptionTypeToWai @InvalidEmail) Doc.errorResponse (errorDescriptionTypeToWai @UserKeyExists) Doc.errorResponse blacklistedEmail Doc.errorResponse (errorDescriptionTypeToWai @BlacklistedPhone) diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index 28023f5863..2cd821f9af 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -32,6 +32,7 @@ module Brig.User.Auth.Cookie -- * HTTP setResponseCookie, + toWebCookie, -- * Re-exports Cookie (..), @@ -226,22 +227,24 @@ setResponseCookie :: Response -> m Response setResponseCookie c r = do - s <- view settings - let hdr = toByteString' (WebCookie.renderSetCookie (cookie s)) + hdr <- toByteString' . WebCookie.renderSetCookie <$> toWebCookie c return (addHeader "Set-Cookie" hdr r) - where - cookie s = - WebCookie.def - { WebCookie.setCookieName = "zuid", - WebCookie.setCookieValue = toByteString' (cookieValue c), - WebCookie.setCookiePath = Just "/access", - WebCookie.setCookieExpires = - if cookieType c == PersistentCookie - then Just (cookieExpires c) - else Nothing, - WebCookie.setCookieSecure = not (setCookieInsecure s), - WebCookie.setCookieHttpOnly = True - } + +toWebCookie :: (Monad m, MonadReader Env m, ZAuth.UserTokenLike u) => Cookie (ZAuth.Token u) -> m WebCookie.SetCookie +toWebCookie c = do + s <- view settings + pure $ + WebCookie.def + { WebCookie.setCookieName = "zuid", + WebCookie.setCookieValue = toByteString' (cookieValue c), + WebCookie.setCookiePath = Just "/access", + WebCookie.setCookieExpires = + if cookieType c == PersistentCookie + then Just (cookieExpires c) + else Nothing, + WebCookie.setCookieSecure = not (setCookieInsecure s), + WebCookie.setCookieHttpOnly = True + } -------------------------------------------------------------------------------- -- Tracking diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 1fff41468d..742fe3e30c 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -87,6 +87,7 @@ import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import Util import Web.Cookie (SetCookie (..), parseSetCookie) +import Wire.API.Event.Conversation tests :: Domain -> Config -> Manager -> DB.ClientState -> Brig -> Cannon -> Galley -> IO TestTree tests dom conf p db b c g = do diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 808f075999..5483a76403 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -1,20 +1,4 @@ {-# LANGUAGE NumericUnderscores #-} --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2020 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -- This file is part of the Wire Server implementation. @@ -73,6 +57,7 @@ import Data.Range (Range (fromRange)) import qualified Data.Set as Set import Data.String.Conversions (cs) import qualified Data.Text as T +import qualified Data.Text as Text import qualified Data.Text.Encoding as T import Data.Time (UTCTime, getCurrentTime) import Data.Time.Clock (diffUTCTime) @@ -84,21 +69,24 @@ import Federator.MockServer (FederatedRequest (..), MockException (..)) import Galley.Types.Teams (noPermissions) import Gundeck.Types.Notification import Imports hiding (head) +import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP.Types as Http import qualified Network.Wai as Wai import qualified Network.Wai.Utilities.Error as Error +import qualified Network.Wai.Utilities.Error as Wai import Test.Tasty hiding (Timeout) import Test.Tasty.Cannon hiding (Cannon) import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import UnliftIO (mapConcurrently_) -import Util as Util +import Util import Util.AWS as Util import Web.Cookie (parseSetCookie) import qualified Wire.API.Asset as Asset import Wire.API.Federation.API.Brig (UserDeletedConnectionsNotification (..)) import qualified Wire.API.Federation.API.Brig as FedBrig import Wire.API.Federation.API.Common (EmptyResponse (EmptyResponse)) +import Wire.API.Team.Invitation (Invitation (inInvitation)) import Wire.API.User (ListUsersQuery (..)) import Wire.API.User.Identity (mkSampleUref, mkSimpleSampleUref) @@ -121,6 +109,7 @@ tests _ at opts p b c ch g aws = test' aws p "post /register - 403 blacklist" $ testCreateUserBlacklist opts b aws, test' aws p "post /register - 400 external-SSO" $ testCreateUserExternalSSO b, test' aws p "post /register - 403 restricted user creation" $ testRestrictedUserCreation opts b, + test' aws p "post /register - 403 too many members for legalhold" $ testTooManyMembersForLegalhold opts b, test' aws p "post /activate - 200/204 + expiry" $ testActivateWithExpiry opts b at, test' aws p "get /users/:uid - 404" $ testNonExistingUserUnqualified b, test' aws p "get /users//:uid - 404" $ testNonExistingUser b, @@ -1586,6 +1575,47 @@ testRestrictedUserCreation opts brig = do ] postUserRegister' ssoUser brig !!! const 400 === statusCode +-- | FUTUREWORK: @setRestrictUserCreation@ perhaps needs to be tested in one place only, since it's the +-- first thing that we check on the /register endpoint. Other tests that make use of @setRestrictUserCreation@ +-- can probably be removed and simplified. It's probably a good candidate for Quickcheck. +testTooManyMembersForLegalhold :: Opt.Opts -> Brig -> Http () +testTooManyMembersForLegalhold opts brig = do + (owner, tid) <- createUserWithTeam brig + + -- Invite a user with mocked galley which tells us that the user cannot be + -- added. We cannot use real galley here as the real galley has legalhold set + -- to "whitelist-teams-and-implicit-consent". In this mode this error is not + -- thrown, so in order to emulate other modes, we just emulate what galley + -- would return in that case. + inviteeEmail <- randomEmail + let invite = stdInvitationRequest inviteeEmail + inv <- + responseJsonError =<< postInvitation brig tid owner invite + Cannon -> User -> [UserId] -> AWS.Env -> (UserId -> HttpT IO ()) -> Http () diff --git a/services/brig/test/integration/API/Version.hs b/services/brig/test/integration/API/Version.hs index eefdf7879c..f6914e2410 100644 --- a/services/brig/test/integration/API/Version.hs +++ b/services/brig/test/integration/API/Version.hs @@ -19,6 +19,7 @@ module API.Version (tests) where import Bilge import Bilge.Assert +import Brig.Options import Imports import qualified Network.Wai.Utilities.Error as Wai import Test.Tasty @@ -26,13 +27,14 @@ import Test.Tasty.HUnit import Util import Wire.API.Routes.Version -tests :: Manager -> Brig -> TestTree -tests p brig = +tests :: Manager -> Opts -> Brig -> TestTree +tests p opts brig = testGroup "version" [ test p "GET /api-version" $ testVersion brig, test p "GET /v1/api-version" $ testVersionV1 brig, - test p "GET /v500/api-version" $ testUnsupportedVersion brig + test p "GET /v500/api-version" $ testUnsupportedVersion brig, + test p "GET /api-version (federation info)" $ testFederationDomain opts brig ] testVersion :: Brig -> Http () @@ -57,3 +59,13 @@ testUnsupportedVersion brig = do responseJsonError =<< get (brig . path "/v500/api-version") Brig -> Http () +testFederationDomain opts brig = do + let domain = setFederationDomain (optSettings opts) + vinfo <- + responseJsonError =<< get (brig . path "/api-version") + Blank.getEnv "INTEGRATION_FEDERATION_TESTS" internalApi <- API.Internal.tests brigOpts mg db b (brig iConf) gd g - let versionApi = API.Version.tests mg b + let versionApi = API.Version.tests mg brigOpts b let mlsApi = MLS.tests mg b brigOpts diff --git a/services/brig/test/unit/Test/Brig/MLS.hs b/services/brig/test/unit/Test/Brig/MLS.hs index f38e8e4852..de7dc37bd8 100644 --- a/services/brig/test/unit/Test/Brig/MLS.hs +++ b/services/brig/test/unit/Test/Brig/MLS.hs @@ -28,6 +28,7 @@ import Test.Tasty import Test.Tasty.QuickCheck import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Serialisation -- | A lifetime with a length of at least 1 day. newtype ValidLifetime = ValidLifetime Lifetime @@ -61,14 +62,20 @@ newtype ValidExtensions = ValidExtensions [Extension] instance Show ValidExtensions where show (ValidExtensions exts) = "ValidExtensions (length " <> show (length exts) <> ")" +unknownExt :: Gen Extension +unknownExt = do + Positive t0 <- arbitrary + let t = t0 + fromEnum (maxBound :: ExtensionTag) + 1 + Extension (fromIntegral t) <$> arbitrary + -- | Generate a list of extensions containing all the required ones. instance Arbitrary ValidExtensions where arbitrary = do - exts0 <- listOf (arbitrary `suchThat` ((/= 0) . extType)) + exts0 <- listOf unknownExt LifetimeAndExtension ext1 _ <- arbitrary - exts2 <- listOf (arbitrary `suchThat` ((/= 0) . extType)) + exts2 <- listOf unknownExt CapabilitiesAndExtension ext3 _ <- arbitrary - exts4 <- listOf (arbitrary `suchThat` ((/= 0) . extType)) + exts4 <- listOf unknownExt pure . ValidExtensions $ exts0 <> [ext1] <> exts2 <> [ext3] <> exts4 newtype InvalidExtensions = InvalidExtensions [Extension] @@ -79,7 +86,7 @@ instance Show InvalidExtensions where instance Arbitrary InvalidExtensions where arbitrary = do - req <- fromIntegral . fromEnum <$> elements [LifetimeExtensionTag, CapabilitiesExtensionTag] + req <- fromMLSEnum <$> elements [LifetimeExtensionTag, CapabilitiesExtensionTag] InvalidExtensions <$> listOf (arbitrary `suchThat` ((/= req) . extType)) data LifetimeAndExtension = LifetimeAndExtension Extension Lifetime @@ -88,7 +95,7 @@ data LifetimeAndExtension = LifetimeAndExtension Extension Lifetime instance Arbitrary LifetimeAndExtension where arbitrary = do lt <- arbitrary - let ext = Extension (fromIntegral (fromEnum LifetimeExtensionTag)) . LBS.toStrict . runPut $ do + let ext = Extension (fromIntegral (fromEnum LifetimeExtensionTag + 1)) . LBS.toStrict . runPut $ do put (timestampSeconds (ltNotBefore lt)) put (timestampSeconds (ltNotAfter lt)) pure $ LifetimeAndExtension ext lt @@ -99,7 +106,7 @@ data CapabilitiesAndExtension = CapabilitiesAndExtension Extension Capabilities instance Arbitrary CapabilitiesAndExtension where arbitrary = do caps <- arbitrary - let ext = Extension (fromIntegral (fromEnum CapabilitiesExtensionTag)) . LBS.toStrict . runPut $ do + let ext = Extension (fromIntegral (fromEnum CapabilitiesExtensionTag + 1)) . LBS.toStrict . runPut $ do putWord8 (fromIntegral (length (capVersions caps))) traverse_ (putWord8 . pvNumber) (capVersions caps) @@ -143,8 +150,8 @@ tests = testProperty "missing required extensions" $ \(InvalidExtensions exts) -> isLeft (findExtensions exts), testProperty "lifetime extension" $ \(LifetimeAndExtension ext lt) -> - decodeExtension ext == Just (SomeExtension SLifetimeExtensionTag lt), + decodeExtension ext == Right (Just (SomeExtension SLifetimeExtensionTag lt)), testProperty "capabilities extension" $ \(CapabilitiesAndExtension ext caps) -> - decodeExtension ext == Just (SomeExtension SCapabilitiesExtensionTag caps) + decodeExtension ext == Right (Just (SomeExtension SCapabilitiesExtensionTag caps)) ] ] diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 4ce7d9094b..f2d8c75888 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -408,6 +408,7 @@ executable galley-integration , containers , cookie , currency-codes + , data-default , data-timeout , errors , exceptions diff --git a/services/galley/package.yaml b/services/galley/package.yaml index 8b17a10f8c..211826d64d 100644 --- a/services/galley/package.yaml +++ b/services/galley/package.yaml @@ -169,6 +169,7 @@ executables: - cookie - currency-codes - metrics-wai + - data-default - data-timeout - errors - exceptions diff --git a/services/galley/schema/src/V58_ConversationAccessRoleV2.hs b/services/galley/schema/src/V58_ConversationAccessRoleV2.hs index 0d1248f070..a477e9b152 100644 --- a/services/galley/schema/src/V58_ConversationAccessRoleV2.hs +++ b/services/galley/schema/src/V58_ConversationAccessRoleV2.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2020 Wire Swiss GmbH +-- Copyright (C) 2022 Wire Swiss GmbH -- -- This program is free software: you can redistribute it and/or modify it under -- the terms of the GNU Affero General Public License as published by the Free diff --git a/services/galley/schema/src/V59_FileSharingLockStatus.hs b/services/galley/schema/src/V59_FileSharingLockStatus.hs index 8195f186b3..d1b8392482 100644 --- a/services/galley/schema/src/V59_FileSharingLockStatus.hs +++ b/services/galley/schema/src/V59_FileSharingLockStatus.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2020 Wire Swiss GmbH +-- Copyright (C) 2022 Wire Swiss GmbH -- -- This program is free software: you can redistribute it and/or modify it under -- the terms of the GNU Affero General Public License as published by the Free diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 68562d43fe..75a2c5f2a7 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -377,7 +377,7 @@ createConnectConversation lusr conn j = do c <- E.createConnectConversation x y n now <- input let lcid = qualifyAs lusr (Data.convId c) - e = Event ConvConnect (qUntagged lcid) (qUntagged lusr) now (EdConnect j) + e = Event (qUntagged lcid) (qUntagged lusr) now (EdConnect j) notifyCreatedConversation Nothing lusr conn c for_ (newPushLocal ListComplete (tUnqualified lusr) (ConvEvent e) (recipient <$> Data.convLocalMembers c)) $ \p -> E.push1 $ @@ -418,7 +418,7 @@ createConnectConversation lusr conn j = do return . Just $ fromRange x Nothing -> return $ Data.convName conv t <- input - let e = Event ConvConnect (qUntagged lcnv) (qUntagged lusr) t (EdConnect j) + let e = Event (qUntagged lcnv) (qUntagged lusr) t (EdConnect j) for_ (newPushLocal ListComplete (tUnqualified lusr) (ConvEvent e) (recipient <$> Data.convLocalMembers conv)) $ \p -> E.push1 $ p @@ -468,7 +468,7 @@ notifyCreatedConversation dtime lusr conn c = do toPush t m = do let lconv = qualifyAs lusr (Data.convId c) c' <- conversationView (qualifyAs lusr (lmId m)) c - let e = Event ConvCreate (qUntagged lconv) (qUntagged lusr) t (EdConversation c') + let e = Event (qUntagged lconv) (qUntagged lusr) t (EdConversation c') return $ newPushLocal1 ListComplete (tUnqualified lusr) (ConvEvent e) (list1 (recipient m) []) & pushConn .~ conn diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 2c86e342fe..66dcc5f776 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -111,7 +111,6 @@ onConversationCreated domain rc = do forM_ (fromNewRemoteConversation loc qrcConnected) $ \(mem, c) -> do let event = Event - ConvCreate (qUntagged (F.rcCnvId qrcConnected)) (qUntagged (F.rcRemoteOrigUserId qrcConnected)) (F.rcTime qrcConnected) diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 16909bcc4b..a40c161c43 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -528,7 +528,6 @@ rmUser lusr conn = do deleteMembers (Data.convId c) (UserList [tUnqualified lusr] []) let e = Event - MemberLeave (qUntagged (qualifyAs lusr (Data.convId c))) (qUntagged lusr) now diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index f8fb535398..96809d0d60 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -234,7 +234,7 @@ postRemoteOtrMessage :: Members '[FederatorAccess] r => Qualified UserId -> Remote ConvId -> - LByteString -> + ByteString -> Sem r (PostOtrResponse MessageSendingStatus) postRemoteOtrMessage sender conv rawMsg = do let msr = @@ -639,7 +639,7 @@ newMessageEvent :: Event newMessageEvent mconvId sender senderClient dat time (receiver, receiverClient) cipherText = let convId = fromMaybe (qUntagged (fmap selfConv receiver)) mconvId - in Event OtrMessageAdd convId sender time . EdOtrMessage $ + in Event convId sender time . EdOtrMessage $ OtrMessage { otrSender = senderClient, otrRecipient = receiverClient, diff --git a/services/galley/src/Galley/API/Public/Servant.hs b/services/galley/src/Galley/API/Public/Servant.hs index 7837481498..3c656cd0b2 100644 --- a/services/galley/src/Galley/API/Public/Servant.hs +++ b/services/galley/src/Galley/API/Public/Servant.hs @@ -83,6 +83,7 @@ servantSitemap = conversations :<|> teamConversations :<|> messaging :<|> bot :< Named @"post-otr-message-unqualified" postOtrMessageUnqualified :<|> Named @"post-otr-broadcast-unqualified" postOtrBroadcastUnqualified :<|> Named @"post-proteus-message" postProteusMessage + :<|> Named @"post-proteus-broadcast" postProteusBroadcast bot = Named @"post-bot-message-unqualified" postBotMessageUnqualified diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index bac8563893..f8714c47a9 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -356,7 +356,7 @@ updateTeamH zusr zcon tid updateData = do E.setTeamData tid updateData now <- input memList <- getTeamMembersForFanout tid - let e = newEvent TeamUpdate tid now & eventData .~ Just (EdTeamUpdate updateData) + let e = newEvent tid now (EdTeamUpdate updateData) let r = list1 (userRecipient zusr) (membersToRecipients (Just zusr) (memList ^. teamMembers)) E.push1 $ newPushLocal1 (memList ^. teamMemberListType) zusr (TeamEvent e) r & pushConn .~ Just zcon @@ -444,7 +444,7 @@ uncheckedDeleteTeam lusr zcon tid = do -- done asynchronously membs <- E.getTeamMembers tid (ue, be) <- foldrM (createConvDeleteEvents now membs) ([], []) convs - let e = newEvent TeamDelete tid now + let e = newEvent tid now EdTeamDelete pushDeleteEvents membs e ue E.deliverAsync be -- TODO: we don't delete bots here, but we should do that, since @@ -483,7 +483,7 @@ uncheckedDeleteTeam lusr zcon tid = do -- all team users are deleted immediately after these events are sent -- and will thus never be able to see these events in practice. let mm = nonTeamMembers convMembs teamMembs - let e = Conv.Event Conv.ConvDelete qconvId (qUntagged lusr) now Conv.EdConvDelete + let e = Conv.Event qconvId (qUntagged lusr) now Conv.EdConvDelete -- This event always contains all the required recipients let p = newPushLocal ListComplete (tUnqualified lusr) (ConvEvent e) (map recipient mm) let ee' = bots `zip` repeat e @@ -948,7 +948,7 @@ updateTeamMember zusr zcon tid targetMember = do privilegedUpdate = mkUpdate $ Just targetPermissions privilegedRecipients = membersToRecipients Nothing privileged now <- input - let ePriv = newEvent MemberUpdate tid now & eventData ?~ privilegedUpdate + let ePriv = newEvent tid now privilegedUpdate -- push to all members (user is privileged) let pushPriv = newPushLocal (updatedMembers ^. teamMemberListType) zusr (TeamEvent ePriv) $ privilegedRecipients for_ pushPriv $ \p -> E.push1 $ p & pushConn .~ Just zcon @@ -1071,7 +1071,7 @@ uncheckedDeleteTeamMember lusr zcon tid remove mems = do -- notify all team members. pushMemberLeaveEvent :: UTCTime -> Sem r () pushMemberLeaveEvent now = do - let e = newEvent MemberLeave tid now & eventData ?~ EdMemberLeave remove + let e = newEvent tid now (EdMemberLeave remove) let r = list1 (userRecipient (tUnqualified lusr)) @@ -1099,7 +1099,7 @@ uncheckedDeleteTeamMember lusr zcon tid remove mems = do let qconvId = qUntagged $ qualifyAs lusr (Data.convId dc) let (bots, users) = localBotsAndUsers (Data.convLocalMembers dc) let x = filter (\m -> not (Conv.lmId m `Set.member` exceptTo)) users - let y = Conv.Event Conv.MemberLeave qconvId (qUntagged lusr) now edata + let y = Conv.Event qconvId (qUntagged lusr) now edata for_ (newPushLocal (mems ^. teamMemberListType) (tUnqualified lusr) (ConvEvent y) (recipient <$> x)) $ \p -> E.push1 $ p & pushConn .~ zcon E.deliverAsync (bots `zip` repeat y) @@ -1339,7 +1339,7 @@ addTeamMemberInternal tid origin originConn (ntmNewTeamMember -> new) memList = sizeBeforeAdd <- ensureNotTooLarge tid E.createTeamMember tid new now <- input - let e = newEvent MemberJoin tid now & eventData ?~ EdMemberJoin (new ^. userId) + let e = newEvent tid now (EdMemberJoin (new ^. userId)) E.push1 $ newPushLocal1 (memList ^. teamMemberListType) (new ^. userId) (TeamEvent e) (recipients origin new) & pushConn .~ originConn APITeamQueue.pushTeamEvent tid e @@ -1400,7 +1400,7 @@ finishCreateTeam team owner others zcon = do for_ (owner : others) $ E.createTeamMember (team ^. teamId) now <- input - let e = newEvent TeamCreate (team ^. teamId) now & eventData ?~ EdTeamCreate team + let e = newEvent (team ^. teamId) now (EdTeamCreate team) let r = membersToRecipients Nothing others E.push1 $ newPushLocal1 ListComplete zusr (TeamEvent e) (list1 (userRecipient zusr) r) & pushConn .~ zcon @@ -1429,6 +1429,18 @@ canUserJoinTeamH :: canUserJoinTeamH tid = canUserJoinTeam tid >> pure empty -- This could be extended for more checks, for now we test only legalhold +-- +-- Brig's `POST /register` endpoint throws the errors returned by this endpoint +-- verbatim. +-- +-- FUTUREWORK: When this enpoint gets Servantified, it should have a more +-- precise list of errors, LegalHoldError is too wide, currently this can +-- actaully only error with TooManyTeamMembersOnTeamWithLegalhold. Once we have +-- a more precise list of errors and the endpoint is servantified, we can use +-- those to enrich 'Wire.API.User.RegisterError' and ensure that these errors +-- also show up in swagger. Currently, the error returned by this endpoint is +-- thrown in IO, we could then refactor that to be thrown in `ExceptT +-- RegisterError`. canUserJoinTeam :: Members '[ BrigAccess, diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 54b6f91063..85e358b34b 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -52,6 +52,7 @@ module Galley.API.Update -- * Talking postProteusMessage, postOtrMessageUnqualified, + postProteusBroadcast, postOtrBroadcastUnqualified, isTypingUnqualified, @@ -565,7 +566,7 @@ addCode lusr zcon lcnv = do E.createCode code now <- input conversationCode <- createCode code - let event = Event ConvCodeUpdate (qUntagged lcnv) (qUntagged lusr) now (EdConvCodeUpdate conversationCode) + let event = Event (qUntagged lcnv) (qUntagged lusr) now (EdConvCodeUpdate conversationCode) pushConversationEvent (Just zcon) event (qualifyAs lusr (map lmId users)) bots pure $ CodeAdded event Just code -> do @@ -621,7 +622,7 @@ rmCode lusr zcon lcnv = do key <- E.makeKey (tUnqualified lcnv) E.deleteCode key ReusableCode now <- input - let event = Event ConvCodeDelete (qUntagged lcnv) (qUntagged lusr) now EdConvCodeDelete + let event = Event (qUntagged lcnv) (qUntagged lusr) now EdConvCodeDelete pushConversationEvent (Just zcon) event (qualifyAs lusr (map lmId users)) bots pure event @@ -844,7 +845,7 @@ updateSelfMember lusr zcon qcnv update = do unless exists . throw $ ConvNotFound E.setSelfMember qcnv lusr update now <- input - let e = Event MemberStateUpdate qcnv (qUntagged lusr) now (EdMemberUpdate (updateData lusr)) + let e = Event qcnv (qUntagged lusr) now (EdMemberUpdate (updateData lusr)) pushConversationEvent (Just zcon) e (fmap pure lusr) [] where checkLocalMembership :: @@ -1055,7 +1056,7 @@ removeMemberFromRemoteConv cnv lusr victim handleSuccess _ = do t <- input pure . Just $ - Event MemberLeave (qUntagged cnv) (qUntagged lusr) t $ + Event (qUntagged cnv) (qUntagged lusr) t $ EdMembersLeave (QualifiedUserIdList [victim]) -- | Remove a member from a local conversation. @@ -1114,6 +1115,30 @@ postProteusMessage sender zcon conv msg = runLocalInput sender $ do (\c -> postRemoteOtrMessage (qUntagged sender) c (rpRaw msg)) conv +postProteusBroadcast :: + Members + '[ BotAccess, + BrigAccess, + ClientStore, + ConversationStore, + Error ActionError, + Error TeamError, + FederatorAccess, + GundeckAccess, + ExternalAccess, + Input Opts, + Input UTCTime, + MemberStore, + TeamStore, + TinyLog + ] + r => + Local UserId -> + ConnId -> + QualifiedNewOtrMessage -> + Sem r (PostOtrResponse MessageSendingStatus) +postProteusBroadcast sender zcon msg = postBroadcast sender (Just zcon) msg + unqualifyEndpoint :: Functor f => Local x -> @@ -1351,7 +1376,7 @@ isTyping lusr zcon lcnv typingData = do mm <- E.getLocalMembers (tUnqualified lcnv) unless (tUnqualified lusr `isMember` mm) . throw $ ConvNotFound now <- input - let e = Event Typing (qUntagged lcnv) (qUntagged lusr) now (EdTyping typingData) + let e = Event (qUntagged lcnv) (qUntagged lusr) now (EdTyping typingData) for_ (newPushLocal ListComplete (tUnqualified lusr) (ConvEvent e) (recipient <$> mm)) $ \p -> E.push1 $ p @@ -1433,7 +1458,6 @@ addBot lusr zcon b = do bm <- E.createBotMember (b ^. addBotService) (b ^. addBotId) (b ^. addBotConv) let e = Event - MemberJoin (qUntagged (qualifyAs lusr (b ^. addBotConv))) (qUntagged lusr) t @@ -1509,7 +1533,7 @@ rmBot lusr zcon b = do t <- input do let evd = EdMembersLeave (QualifiedUserIdList [qUntagged (qualifyAs lusr (botUserId (b ^. rmBotId)))]) - let e = Event MemberLeave (qUntagged lcnv) (qUntagged lusr) t evd + let e = Event (qUntagged lcnv) (qUntagged lusr) t evd for_ (newPushLocal ListComplete (tUnqualified lusr) (ConvEvent e) (recipient <$> users)) $ \p -> E.push1 $ p & pushConn .~ zcon E.deleteMembers (Data.convId c) (UserList [botUserId (b ^. rmBotId)] []) diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index b3e35326c5..e2871d3be7 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -302,7 +302,7 @@ memberJoinEvent :: [RemoteMember] -> Event memberJoinEvent lorig qconv t lmems rmems = - Event MemberJoin qconv (qUntagged lorig) t $ + Event qconv (qUntagged lorig) t $ EdMembersJoin (SimpleMembers (map localToSimple lmems <> map remoteToSimple rmems)) where localToSimple u = SimpleMember (qUntagged (qualifyAs lorig (lmId u))) (lmConvRoleName u) diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index ad4482370f..efc2597b3d 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -234,9 +234,10 @@ getAccountFeatureConfigClient uid = getAccountFeatureConfigClientM :: UserId -> Client.ClientM TeamFeatureStatusNoConfig -( _ - :<|> getAccountFeatureConfigClientM - :<|> _ +( ( _ + :<|> getAccountFeatureConfigClientM + :<|> _ + ) :<|> _ ) = Client.client (Proxy @IAPI.API) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index a6405832a3..fa070f2565 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -86,6 +86,7 @@ import TestSetup import Util.Options (Endpoint (Endpoint)) import Wire.API.Conversation import Wire.API.Conversation.Action +import Wire.API.Event.Conversation import Wire.API.Federation.API import qualified Wire.API.Federation.API.Brig as F import Wire.API.Federation.API.Galley @@ -471,7 +472,7 @@ postCryptoMessageVerifyRejectMissingClientAndRepondMissingPrekeysProto = do conv <- decodeConvId <$> postConv alice [bob, eve] (Just "gossip") [] Nothing Nothing -- Missing eve let ciphertext = toBase64Text "hello bob" - let m = otrRecipients [(bob, [(bc, ciphertext)])] + let m = otrRecipients [(bob, bc, ciphertext)] r1 <- postProtoOtrMessage alice ac conv m postConv alice [bob] (Just "gossip") [] Nothing Nothing -- Unknown client ID => 403 let ciphertext = toBase64Text "hello bob" - let m = otrRecipients [(bob, [(bc, ciphertext)])] + let m = otrRecipients [(bob, bc, ciphertext)] postProtoOtrMessage alice (ClientId "172618352518396") conv m !!! const 403 === statusCode @@ -575,7 +576,7 @@ postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do conv <- decodeConvId <$> postConv alice [bob, chad, eve] (Just "gossip") [] Nothing Nothing -- Missing eve let msgMissingChadAndEve = [(bob, bc, toBase64Text "hello bob")] - let m' = otrRecipients [(bob, [(bc, toBase64Text "hello bob")])] + let m' = otrRecipients [(bob, bc, toBase64Text "hello bob")] -- These three are equivalent (i.e. report all missing clients) postOtrMessage id alice ac conv msgMissingChadAndEve !!! const 412 === statusCode diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 21086153d5..b43e9030bc 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -42,7 +42,6 @@ import Control.Lens hiding ((#)) import Data.Aeson (ToJSON (..)) import qualified Data.Aeson as A import Data.ByteString.Conversion (toByteString') -import qualified Data.ByteString.Lazy as LBS import Data.Domain import Data.Id (ConvId, Id (..), UserId, newClientId, randomId) import Data.Json.Util (Base64ByteString (..), toBase64Text) @@ -72,6 +71,7 @@ import TestSetup import Wire.API.Conversation.Action (ConversationAction (..)) import Wire.API.Conversation.Member (Member (..)) import Wire.API.Conversation.Role +import Wire.API.Event.Conversation import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley (GetConversationsRequest (..), GetConversationsResponse (..), RemoteConvMembers (..), RemoteConversation (..)) import qualified Wire.API.Federation.API.Galley as FedGalley @@ -884,9 +884,7 @@ sendMessage = do FedGalley.MessageSendRequest { FedGalley.msrConvId = convId, FedGalley.msrSender = bobId, - FedGalley.msrRawMessage = - Base64ByteString - (LBS.fromStrict (Protolens.encodeMessage msg)) + FedGalley.msrRawMessage = Base64ByteString (Protolens.encodeMessage msg) } let responses2 req | frComponent req == Brig = diff --git a/services/galley/test/integration/API/MessageTimer.hs b/services/galley/test/integration/API/MessageTimer.hs index 2339a993ab..f40e23dd53 100644 --- a/services/galley/test/integration/API/MessageTimer.hs +++ b/services/galley/test/integration/API/MessageTimer.hs @@ -46,6 +46,7 @@ import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation.Action +import Wire.API.Event.Conversation import qualified Wire.API.Federation.API.Galley as F import Wire.API.Federation.Component import qualified Wire.API.Team.Member as Member diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index 9da167a2c0..fda229a8e2 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -42,6 +42,7 @@ import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation.Action +import Wire.API.Event.Conversation import qualified Wire.API.Federation.API.Galley as F import Wire.API.Federation.Component diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 0cd2a8d929..f16d2178e7 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -38,6 +38,7 @@ import Data.ByteString.Conversion import Data.ByteString.Lazy (fromStrict) import Data.Csv (FromNamedRecord (..), decodeByName) import qualified Data.Currency as Currency +import Data.Default import Data.Id import Data.Json.Util hiding ((#)) import qualified Data.LegalHold as LH @@ -135,13 +136,23 @@ tests s = test s "team tests around truncation limits - no events, too large team" (testTeamAddRemoveMemberAboveThresholdNoEvents >> ensureQueueEmpty), test s "send billing events to owners even in large teams" testBillingInLargeTeam, test s "send billing events to some owners in large teams (indexedBillingTeamMembers disabled)" testBillingInLargeTeamWithoutIndexedBillingTeamMembers, - test s "post crypto broadcast message json" postCryptoBroadcastMessageJson, - test s "post crypto broadcast message json - filtered only, too large team" postCryptoBroadcastMessageJsonFilteredTooLargeTeam, - test s "post crypto broadcast message json (report missing in body)" postCryptoBroadcastMessageJsonReportMissingBody, - test s "post crypto broadcast message protobuf" postCryptoBroadcastMessageProto, - test s "post crypto broadcast message redundant/missing" postCryptoBroadcastMessageJson2, - test s "post crypto broadcast message no-team" postCryptoBroadcastMessageNoTeam, - test s "post crypto broadcast message 100 (or max conns)" postCryptoBroadcastMessage100OrMaxConns + testGroup "broadcast" $ + [ (BroadcastLegacyBody, BroadcastJSON), + (BroadcastLegacyQueryParams, BroadcastJSON), + (BroadcastLegacyBody, BroadcastProto), + (BroadcastQualified, BroadcastProto) + ] + <&> \(api, ty) -> + let bcast = def {bAPI = api, bType = ty} + in testGroup + (broadcastAPIName api <> " - " <> broadcastTypeName ty) + [ test s "message" (postCryptoBroadcastMessage bcast), + test s "filtered only, too large team" (postCryptoBroadcastMessageFilteredTooLargeTeam bcast), + test s "report missing in body" (postCryptoBroadcastMessageReportMissingBody bcast), + test s "redundant/missing" (postCryptoBroadcastMessage2 bcast), + test s "no-team" (postCryptoBroadcastMessageNoTeam bcast), + test s "100 (or max conns)" (postCryptoBroadcastMessage100OrMaxConns bcast) + ] ] timeout :: WS.Timeout @@ -160,9 +171,8 @@ testCreateTeam = do eventChecks <- WS.awaitMatch timeout wsOwner $ \notif -> do ntfTransient notif @?= False let e = List1.head (WS.unpackPayload notif) - e ^. eventType @?= TeamCreate e ^. eventTeam @?= tid - e ^. eventData @?= Just (EdTeamCreate team) + e ^. eventData @?= EdTeamCreate team void $ WS.assertSuccess eventChecks testGetTeams :: TestM () @@ -237,9 +247,8 @@ testCreateTeamWithMembers = do checkCreateEvent team w = WS.assertMatch_ timeout w $ \notif -> do ntfTransient notif @?= False let e = List1.head (WS.unpackPayload notif) - e ^. eventType @?= TeamCreate e ^. eventTeam @?= (team ^. teamId) - e ^. eventData @?= Just (EdTeamCreate team) + e ^. eventData @?= EdTeamCreate team testListTeamMembersDefaultLimit :: TestM () testListTeamMembersDefaultLimit = do @@ -1584,9 +1593,8 @@ testUpdateTeamMember = do checkTeamMemberUpdateEvent tid uid w mPerm = WS.assertMatch_ timeout w $ \notif -> do ntfTransient notif @?= False let e = List1.head (WS.unpackPayload notif) - e ^. eventType @?= MemberUpdate e ^. eventTeam @?= tid - e ^. eventData @?= Just (EdMemberUpdate uid mPerm) + e ^. eventData @?= EdMemberUpdate uid mPerm testUpdateTeamStatus :: TestM () testUpdateTeamStatus = do @@ -1613,8 +1621,8 @@ testUpdateTeamStatus = do const 403 === statusCode const "invalid-team-status-update" === (Error.label . responseJsonUnsafeWithMsg "error label") -postCryptoBroadcastMessageJson :: TestM () -postCryptoBroadcastMessageJson = do +postCryptoBroadcastMessage :: Broadcast -> TestM () +postCryptoBroadcastMessage bcast = do localDomain <- viewFederationDomain let q :: Id a -> Qualified (Id a) q = (`Qualified` localDomain) @@ -1645,9 +1653,9 @@ postCryptoBroadcastMessageJson = do -- Alice's clients 1 and 2 listen to their own messages only WS.bracketR (c . queryItem "client" (toByteString' ac2)) alice $ \wsA2 -> WS.bracketR (c . queryItem "client" (toByteString' ac)) alice $ \wsA1 -> do - Util.postOtrBroadcastMessage id alice ac msg !!! do + Util.postBroadcast (q alice) ac bcast {bMessage = msg} !!! do const 201 === statusCode - assertMismatch [] [] [] + assertBroadcastMismatch localDomain (bAPI bcast) [] [] [] -- Bob should get the broadcast (team member of alice) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext1")) @@ -1663,13 +1671,12 @@ postCryptoBroadcastMessageJson = do void . liftIO $ WS.assertMatch t wsA2 (wsAssertOtr (q (selfConv alice)) (q alice) ac ac2 (toBase64Text "ciphertext0")) -postCryptoBroadcastMessageJsonFilteredTooLargeTeam :: TestM () -postCryptoBroadcastMessageJsonFilteredTooLargeTeam = do +postCryptoBroadcastMessageFilteredTooLargeTeam :: Broadcast -> TestM () +postCryptoBroadcastMessageFilteredTooLargeTeam bcast = do localDomain <- viewFederationDomain let q :: Id a -> Qualified (Id a) q = (`Qualified` localDomain) opts <- view tsGConf - g <- view tsCannon c <- view tsCannon -- Team1: alice, bob and 3 unnamed (alice, tid) <- Util.createBindingTeam @@ -1708,14 +1715,14 @@ postCryptoBroadcastMessageJsonFilteredTooLargeTeam = do & optSettings . setMaxConvSize .~ 4 withSettingsOverrides newOpts $ do -- Untargeted, Alice's team is too large - Util.postOtrBroadcastMessage' g Nothing id alice ac msg !!! do + Util.postBroadcast (q alice) ac bcast {bMessage = msg} !!! do const 400 === statusCode const "too-many-users-to-broadcast" === Error.label . responseJsonUnsafeWithMsg "error label" -- We target the message to the 4 users, that should be fine let inbody = Just [alice, bob, charlie, dan] - Util.postOtrBroadcastMessage' g inbody id alice ac msg !!! do + Util.postBroadcast (q alice) ac bcast {bReport = inbody, bMessage = msg} !!! do const 201 === statusCode - assertMismatch [] [] [] + assertBroadcastMismatch localDomain (bAPI bcast) [] [] [] -- Bob should get the broadcast (team member of alice) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext1")) @@ -1731,23 +1738,26 @@ postCryptoBroadcastMessageJsonFilteredTooLargeTeam = do void . liftIO $ WS.assertMatch t wsA2 (wsAssertOtr (q (selfConv alice)) (q alice) ac ac2 (toBase64Text "ciphertext0")) -postCryptoBroadcastMessageJsonReportMissingBody :: TestM () -postCryptoBroadcastMessageJsonReportMissingBody = do - g <- view tsGalley +postCryptoBroadcastMessageReportMissingBody :: Broadcast -> TestM () +postCryptoBroadcastMessageReportMissingBody bcast = do + localDomain <- viewFederationDomain (alice, tid) <- Util.createBindingTeam + let qalice = Qualified alice localDomain bob <- view userId <$> Util.addUserToTeam alice tid _bc <- Util.randomClient bob (someLastPrekeys !! 1) -- this is important! assertQueue "add bob" $ tUpdate 2 [alice] refreshIndex ac <- Util.randomClient alice (someLastPrekeys !! 0) - let inbody = Just [bob] -- body triggers report - inquery = (queryItem "report_missing" (toByteString' alice)) -- query doesn't + let -- add extraneous query parameter (unless using query parameter API) + inquery = case bAPI bcast of + BroadcastLegacyQueryParams -> id + _ -> queryItem "report_missing" (toByteString' alice) msg = [(alice, ac, "ciphertext0")] - Util.postOtrBroadcastMessage' g inbody inquery alice ac msg + Util.postBroadcast qalice ac bcast {bReport = Just [bob], bMessage = msg, bReq = inquery} !!! const 412 === statusCode -postCryptoBroadcastMessageJson2 :: TestM () -postCryptoBroadcastMessageJson2 = do +postCryptoBroadcastMessage2 :: Broadcast -> TestM () +postCryptoBroadcastMessage2 bcast = do localDomain <- viewFederationDomain let q :: Id a -> Qualified (Id a) q = (`Qualified` localDomain) @@ -1766,15 +1776,15 @@ postCryptoBroadcastMessageJson2 = do let t = 3 # Second -- WS receive timeout -- Missing charlie let m1 = [(bob, bc, toBase64Text "ciphertext1")] - Util.postOtrBroadcastMessage id alice ac m1 !!! do + Util.postBroadcast (q alice) ac bcast {bMessage = m1} !!! do const 412 === statusCode - assertMismatchWithMessage (Just "1: Only Charlie and his device") [(charlie, Set.singleton cc)] [] [] + assertBroadcastMismatch localDomain (bAPI bcast) [(charlie, Set.singleton cc)] [] [] -- Complete WS.bracketR2 c bob charlie $ \(wsB, wsE) -> do let m2 = [(bob, bc, toBase64Text "ciphertext2"), (charlie, cc, toBase64Text "ciphertext2")] - Util.postOtrBroadcastMessage id alice ac m2 !!! do + Util.postBroadcast (q alice) ac bcast {bMessage = m2} !!! do const 201 === statusCode - assertMismatchWithMessage (Just "No devices expected") [] [] [] + assertBroadcastMismatch localDomain (bAPI bcast) [] [] [] void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext2")) void . liftIO $ @@ -1786,9 +1796,9 @@ postCryptoBroadcastMessageJson2 = do (bob, bc, toBase64Text "ciphertext3"), (charlie, cc, toBase64Text "ciphertext3") ] - Util.postOtrBroadcastMessage id alice ac m3 !!! do + Util.postBroadcast (q alice) ac bcast {bMessage = m3} !!! do const 201 === statusCode - assertMismatchWithMessage (Just "2: Only Alice and her device") [] [(alice, Set.singleton ac)] [] + assertBroadcastMismatch localDomain (bAPI bcast) [] [(alice, Set.singleton ac)] [] void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext3")) void . liftIO $ @@ -1799,66 +1809,26 @@ postCryptoBroadcastMessageJson2 = do WS.bracketR2 c bob charlie $ \(wsB, wsE) -> do deleteClient charlie cc (Just defPassword) !!! const 200 === statusCode let m4 = [(bob, bc, toBase64Text "ciphertext4"), (charlie, cc, toBase64Text "ciphertext4")] - Util.postOtrBroadcastMessage id alice ac m4 !!! do + Util.postBroadcast (q alice) ac bcast {bMessage = m4} !!! do const 201 === statusCode - assertMismatchWithMessage (Just "3: Only Charlie and his device") [] [] [(charlie, Set.singleton cc)] + assertBroadcastMismatch localDomain (bAPI bcast) [] [] [(charlie, Set.singleton cc)] void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (q (selfConv bob)) (q alice) ac bc (toBase64Text "ciphertext4")) -- charlie should not get it assertNoMsg wsE (wsAssertOtr (q (selfConv charlie)) (q alice) ac cc (toBase64Text "ciphertext4")) -postCryptoBroadcastMessageProto :: TestM () -postCryptoBroadcastMessageProto = do +postCryptoBroadcastMessageNoTeam :: Broadcast -> TestM () +postCryptoBroadcastMessageNoTeam bcast = do localDomain <- viewFederationDomain - let q :: Id a -> Qualified (Id a) - q = (`Qualified` localDomain) - -- similar to postCryptoBroadcastMessageJson, postCryptoBroadcastMessageJsonReportMissingBody except uses protobuf - - c <- view tsCannon - -- Team1: Alice, Bob. Team2: Charlie. Regular user: Dan. Connect Alice,Charlie,Dan - (alice, tid) <- Util.createBindingTeam - bob <- view userId <$> Util.addUserToTeam alice tid - assertQueue "add bob" $ tUpdate 2 [alice] - refreshIndex - (charlie, _) <- Util.createBindingTeam - refreshIndex - ac <- Util.randomClient alice (someLastPrekeys !! 0) - bc <- Util.randomClient bob (someLastPrekeys !! 1) - cc <- Util.randomClient charlie (someLastPrekeys !! 2) - (dan, dc) <- randomUserWithClient (someLastPrekeys !! 3) - connectUsers alice (list1 charlie [dan]) - -- Complete: Alice broadcasts a message to Bob,Charlie,Dan - let t = 1 # Second -- WS receive timeout - let ciphertext = toBase64Text "hello bob" - WS.bracketRN c [alice, bob, charlie, dan] $ \ws@[_, wsB, wsC, wsD] -> do - let msg = otrRecipients [(bob, [(bc, ciphertext)]), (charlie, [(cc, ciphertext)]), (dan, [(dc, ciphertext)])] - Util.postProtoOtrBroadcast alice ac msg !!! do - const 201 === statusCode - assertMismatch [] [] [] - -- Bob should get the broadcast (team member of alice) - void . liftIO $ WS.assertMatch t wsB (wsAssertOtr' (toBase64Text "data") (q (selfConv bob)) (q alice) ac bc ciphertext) - -- Charlie should get the broadcast (contact of alice and user of teams feature) - void . liftIO $ WS.assertMatch t wsC (wsAssertOtr' (toBase64Text "data") (q (selfConv charlie)) (q alice) ac cc ciphertext) - -- Dan should get the broadcast (contact of alice and not user of teams feature) - void . liftIO $ WS.assertMatch t wsD (wsAssertOtr' (toBase64Text "data") (q (selfConv dan)) (q alice) ac dc ciphertext) - -- Alice should not get her own broadcast - WS.assertNoEvent timeout ws - let inbody = Just [bob] -- body triggers report - inquery = (queryItem "report_missing" (toByteString' alice)) -- query doesn't - msg = otrRecipients [(alice, [(ac, ciphertext)])] - Util.postProtoOtrBroadcast' inbody inquery alice ac msg - !!! const 412 === statusCode - -postCryptoBroadcastMessageNoTeam :: TestM () -postCryptoBroadcastMessageNoTeam = do (alice, ac) <- randomUserWithClient (someLastPrekeys !! 0) + let qalice = Qualified alice localDomain (bob, bc) <- randomUserWithClient (someLastPrekeys !! 1) connectUsers alice (list1 bob []) let msg = [(bob, bc, toBase64Text "ciphertext1")] - Util.postOtrBroadcastMessage id alice ac msg !!! const 404 === statusCode + Util.postBroadcast qalice ac bcast {bMessage = msg} !!! const 404 === statusCode -postCryptoBroadcastMessage100OrMaxConns :: TestM () -postCryptoBroadcastMessage100OrMaxConns = do +postCryptoBroadcastMessage100OrMaxConns :: Broadcast -> TestM () +postCryptoBroadcastMessage100OrMaxConns bcast = do localDomain <- viewFederationDomain c <- view tsCannon (alice, ac) <- randomUserWithClient (someLastPrekeys !! 0) @@ -1871,9 +1841,9 @@ postCryptoBroadcastMessage100OrMaxConns = do WS.bracketRN c (bob : (fst <$> others)) $ \ws -> do let f (u, clt) = (u, clt, toBase64Text "ciphertext") let msg = (bob, bc, toBase64Text "ciphertext") : (f <$> others) - Util.postOtrBroadcastMessage id alice ac msg !!! do + Util.postBroadcast qalice ac bcast {bMessage = msg} !!! do const 201 === statusCode - assertMismatch [] [] [] + assertBroadcastMismatch localDomain (bAPI bcast) [] [] [] let qbobself = Qualified (selfConv bob) localDomain void . liftIO $ WS.assertMatch t (Imports.head ws) (wsAssertOtr qbobself qalice ac bc (toBase64Text "ciphertext")) @@ -1925,6 +1895,5 @@ checkJoinEvent :: (MonadIO m, MonadCatch m) => TeamId -> UserId -> WS.WebSocket checkJoinEvent tid usr w = WS.assertMatch_ timeout w $ \notif -> do ntfTransient notif @?= False let e = List1.head (WS.unpackPayload notif) - e ^. eventType @?= MemberJoin e ^. eventTeam @?= tid - e ^. eventData @?= Just (EdMemberJoin usr) + e ^. eventData @?= EdMemberJoin usr diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 1c644e37cc..73fd845971 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -42,11 +42,12 @@ import qualified Data.CaseInsensitive as CI import qualified Data.Code as Code import qualified Data.Currency as Currency import Data.Data (Proxy (Proxy)) +import Data.Default import Data.Domain import qualified Data.Handle as Handle import qualified Data.HashMap.Strict as HashMap import Data.Id -import Data.Json.Util (UTCTimeMillis) +import Data.Json.Util hiding ((#)) import Data.LegalHold (defUserLegalHoldStatus) import Data.List.NonEmpty (NonEmpty) import Data.List1 as List1 @@ -109,7 +110,7 @@ import Web.Cookie import Wire.API.Conversation import qualified Wire.API.Conversation as Public import Wire.API.Conversation.Action -import Wire.API.Event.Conversation (_EdConversation, _EdMembersJoin, _EdMembersLeave) +import Wire.API.Event.Conversation import qualified Wire.API.Event.Team as TE import Wire.API.Federation.API import Wire.API.Federation.API.Galley @@ -673,7 +674,7 @@ postOtrMessage' reportMissing f u d c rec = do . zUser u . zConn "conn" . zType "access" - . json (mkOtrPayload d rec reportMissing) + . json (mkOtrPayload d rec reportMissing "ZXhhbXBsZQ==") postProteusMessageQualifiedWithMockFederator :: UserId -> @@ -711,30 +712,86 @@ postProteusMessageQualified senderUser senderClient (Qualified conv domain) reci . contentProtobuf . bytes (Protolens.encodeMessage protoMsg) --- | FUTUREWORK: remove first argument, it's 'id' in all calls to this function! -postOtrBroadcastMessage :: (Request -> Request) -> UserId -> ClientId -> [(UserId, ClientId, Text)] -> TestM ResponseLBS -postOtrBroadcastMessage req usrs clt rcps = do - g <- view tsGalley - postOtrBroadcastMessage' g Nothing req usrs clt rcps +data BroadcastAPI + = BroadcastLegacyQueryParams + | BroadcastLegacyBody + | BroadcastQualified + +broadcastAPIName :: BroadcastAPI -> String +broadcastAPIName BroadcastLegacyQueryParams = "legacy API with query parameters only" +broadcastAPIName BroadcastLegacyBody = "legacy API with report_missing in the body" +broadcastAPIName BroadcastQualified = "qualified API" + +data BroadcastType = BroadcastJSON | BroadcastProto + +broadcastTypeName :: BroadcastType -> String +broadcastTypeName BroadcastJSON = "json" +broadcastTypeName BroadcastProto = "protobuf" --- | 'postOtrBroadcastMessage' with @"report_missing"@ in body. -postOtrBroadcastMessage' :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, MonadFail m, HasCallStack) => (Request -> Request) -> Maybe [UserId] -> (Request -> Request) -> UserId -> ClientId -> [(UserId, ClientId, Text)] -> m ResponseLBS -postOtrBroadcastMessage' g reportMissingBody f u d rec = +data Broadcast = Broadcast + { bAPI :: BroadcastAPI, + bType :: BroadcastType, + bMessage :: [(UserId, ClientId, Text)], + bData :: Text, + bReport :: Maybe [UserId], + bReq :: Request -> Request + } + +instance Default Broadcast where + def = Broadcast BroadcastLegacyQueryParams BroadcastJSON mempty "ZXhhbXBsZQ==" mempty id + +postBroadcast :: + (MonadIO m, MonadHttp m, HasGalley m) => + Qualified UserId -> + ClientId -> + Broadcast -> + m ResponseLBS +postBroadcast lu c b = do + let u = qUnqualified lu + g <- viewGalley + let (bodyReport, queryReport) = case bAPI b of + BroadcastLegacyQueryParams -> (Nothing, maybe id mkOtrReportMissing (bReport b)) + _ -> (bReport b, id) + let bdy = case (bAPI b, bType b) of + (BroadcastQualified, BroadcastJSON) -> error "JSON not supported for the qualified broadcast API" + (BroadcastQualified, BroadcastProto) -> + let m = + Protolens.encodeMessage $ + mkQualifiedOtrPayload + c + (map ((_1 %~ (lu $>)) . (_3 %~ fromBase64TextLenient)) (bMessage b)) + (fromBase64TextLenient (bData b)) + ( maybe + MismatchReportAll + (MismatchReportOnly . Set.fromList . map (lu $>)) + (bReport b) + ) + in contentProtobuf . bytes m + (_, BroadcastJSON) -> json (mkOtrPayload c (bMessage b) bodyReport (bData b)) + (_, BroadcastProto) -> + let m = + runPut . encodeMessage $ + mkOtrProtoMessage c (otrRecipients (bMessage b)) bodyReport (bData b) + in contentProtobuf . bytes m + let name = case bAPI b of BroadcastQualified -> "proteus"; _ -> "otr" post $ - g - . f - . paths ["broadcast", "otr", "messages"] + g . bReq b + . paths ["broadcast", name, "messages"] . zUser u . zConn "conn" . zType "access" - . json (mkOtrPayload d rec reportMissingBody) + . queryReport + . bdy -mkOtrPayload :: ClientId -> [(UserId, ClientId, Text)] -> Maybe [UserId] -> Value -mkOtrPayload sender rec reportMissingBody = +mkOtrReportMissing :: [UserId] -> Request -> Request +mkOtrReportMissing = queryItem "report_missing" . BS.intercalate "," . map toByteString' + +mkOtrPayload :: ClientId -> [(UserId, ClientId, Text)] -> Maybe [UserId] -> Text -> Value +mkOtrPayload sender rec reportMissingBody ad = object [ "sender" .= sender, "recipients" .= (HashMap.map toJSON . HashMap.fromListWith HashMap.union $ map mkOtrMessage rec), - "data" .= Just ("data" :: Text), + "data" .= Just ad, "report_missing" .= reportMissingBody ] @@ -750,7 +807,7 @@ postProtoOtrMessage = postProtoOtrMessage' Nothing id postProtoOtrMessage' :: Maybe [UserId] -> (Request -> Request) -> UserId -> ClientId -> ConvId -> OtrRecipients -> TestM ResponseLBS postProtoOtrMessage' reportMissing modif u d c rec = do g <- view tsGalley - let m = runPut (encodeMessage $ mkOtrProtoMessage d rec reportMissing) + let m = runPut (encodeMessage $ mkOtrProtoMessage d rec reportMissing "ZXhhbXBsZQ==") in post $ g . modif @@ -761,30 +818,13 @@ postProtoOtrMessage' reportMissing modif u d c rec = do . contentProtobuf . bytes m -postProtoOtrBroadcast :: UserId -> ClientId -> OtrRecipients -> TestM ResponseLBS -postProtoOtrBroadcast = postProtoOtrBroadcast' Nothing id - -postProtoOtrBroadcast' :: Maybe [UserId] -> (Request -> Request) -> UserId -> ClientId -> OtrRecipients -> TestM ResponseLBS -postProtoOtrBroadcast' reportMissing modif u d rec = do - g <- view tsGalley - let m = runPut (encodeMessage $ mkOtrProtoMessage d rec reportMissing) - in post $ - g - . modif - . paths ["broadcast", "otr", "messages"] - . zUser u - . zConn "conn" - . zType "access" - . contentProtobuf - . bytes m - -mkOtrProtoMessage :: ClientId -> OtrRecipients -> Maybe [UserId] -> Proto.NewOtrMessage -mkOtrProtoMessage sender rec reportMissing = +mkOtrProtoMessage :: ClientId -> OtrRecipients -> Maybe [UserId] -> Text -> Proto.NewOtrMessage +mkOtrProtoMessage sender rec reportMissing ad = let rcps = protoFromOtrRecipients rec sndr = Proto.fromClientId sender rmis = Proto.fromUserId <$> fromMaybe [] reportMissing in Proto.newOtrMessage sndr rcps - & Proto.newOtrMessageData ?~ "data" + & Proto.newOtrMessageData ?~ fromBase64TextLenient ad & Proto.newOtrMessageReportMissing .~ rmis getConvs :: UserId -> Maybe (Either [ConvId] ConvId) -> Maybe Int32 -> TestM ResponseLBS @@ -1219,8 +1259,8 @@ getTeamQueue zusr msince msize onlyLast = (Error msg) -> error msg (Success (e :: TE.Event)) -> case e ^. TE.eventData of - Just (EdMemberJoin uid) -> uid - _ -> error ("bad even type: " <> show (e ^. TE.eventType)) + EdMemberJoin uid -> uid + _ -> error ("bad event type: " <> show (TE.eventType e)) getTeamQueue' :: HasCallStack => UserId -> Maybe NotificationId -> Maybe Int -> Bool -> TestM ResponseLBS getTeamQueue' zusr msince msize onlyLast = do @@ -1399,7 +1439,7 @@ wsAssertOtr :: Text -> Notification -> IO () -wsAssertOtr = wsAssertOtr' "data" +wsAssertOtr = wsAssertOtr' "ZXhhbXBsZQ==" wsAssertOtr' :: HasCallStack => @@ -1545,7 +1585,7 @@ decodeConvCode = responseJsonUnsafe decodeConvCodeEvent :: Response (Maybe Lazy.ByteString) -> ConversationCode decodeConvCodeEvent r = case responseJsonUnsafe r of - (Event ConvCodeUpdate _ _ _ (EdConvCodeUpdate c)) -> c + (Event _ _ _ (EdConvCodeUpdate c)) -> c _ -> error "Failed to parse ConversationCode from Event" decodeConvId :: HasCallStack => Response (Maybe Lazy.ByteString) -> ConvId @@ -1924,6 +1964,21 @@ assertExpected msg expected tparser = where addTitle s = unlines [msg, s] +assertBroadcastMismatch :: + Domain -> + BroadcastAPI -> + [(UserId, Set ClientId)] -> + [(UserId, Set ClientId)] -> + [(UserId, Set ClientId)] -> + Assertions () +assertBroadcastMismatch localDomain BroadcastQualified = + \m r d -> assertMismatchQualified mempty (mk m) (mk r) (mk d) + where + mk :: [(UserId, Set ClientId)] -> Client.QualifiedUserClients + mk [] = mempty + mk uc = Client.QualifiedUserClients . Map.singleton localDomain . Map.fromList $ uc +assertBroadcastMismatch _ _ = assertMismatch + assertMismatchWithMessage :: HasCallStack => Maybe String -> @@ -1964,10 +2019,13 @@ assertMismatchQualified failedToSend missing redundant deleted = do assertExpected "deleted" deleted (fmap mssDeletedClients . responseJsonMaybe) -otrRecipients :: [(UserId, [(ClientId, Text)])] -> OtrRecipients -otrRecipients = OtrRecipients . UserClientMap . buildMap - where - buildMap = fmap Map.fromList . Map.fromList +otrRecipients :: [(UserId, ClientId, Text)] -> OtrRecipients +otrRecipients = + OtrRecipients + . UserClientMap + . fmap Map.fromList + . foldr (uncurry Map.insert . fmap pure) mempty + . map (\(a, b, c) -> (a, (b, c))) genRandom :: (Q.Arbitrary a, MonadIO m) => m a genRandom = liftIO . Q.generate $ Q.arbitrary @@ -2359,25 +2417,22 @@ checkTeamMemberJoin :: HasCallStack => TeamId -> UserId -> WS.WebSocket -> TestM checkTeamMemberJoin tid uid w = WS.awaitMatch_ checkTimeout w $ \notif -> do ntfTransient notif @?= False let e = List1.head (WS.unpackPayload notif) - e ^. eventType @?= TE.MemberJoin e ^. eventTeam @?= tid - e ^. eventData @?= Just (EdMemberJoin uid) + e ^. eventData @?= EdMemberJoin uid checkTeamMemberLeave :: HasCallStack => TeamId -> UserId -> WS.WebSocket -> TestM () checkTeamMemberLeave tid usr w = WS.assertMatch_ checkTimeout w $ \notif -> do ntfTransient notif @?= False let e = List1.head (WS.unpackPayload notif) - e ^. eventType @?= TE.MemberLeave e ^. eventTeam @?= tid - e ^. eventData @?= Just (EdMemberLeave usr) + e ^. eventData @?= EdMemberLeave usr checkTeamUpdateEvent :: (HasCallStack, MonadIO m, MonadCatch m) => TeamId -> TeamUpdateData -> WS.WebSocket -> m () checkTeamUpdateEvent tid upd w = WS.assertMatch_ checkTimeout w $ \notif -> do ntfTransient notif @?= False let e = List1.head (WS.unpackPayload notif) - e ^. eventType @?= TE.TeamUpdate e ^. eventTeam @?= tid - e ^. eventData @?= Just (EdTeamUpdate upd) + e ^. eventData @?= EdTeamUpdate upd checkConvCreateEvent :: HasCallStack => ConvId -> WS.WebSocket -> TestM () checkConvCreateEvent cid w = WS.assertMatch_ checkTimeout w $ \notif -> do @@ -2411,9 +2466,8 @@ checkTeamDeleteEvent :: HasCallStack => TeamId -> WS.WebSocket -> TestM () checkTeamDeleteEvent tid w = WS.assertMatch_ checkTimeout w $ \notif -> do ntfTransient notif @?= False let e = List1.head (WS.unpackPayload notif) - e ^. eventType @?= TE.TeamDelete e ^. eventTeam @?= tid - e ^. eventData @?= Nothing + e ^. eventData @?= EdTeamDelete checkConvDeleteEvent :: HasCallStack => Qualified ConvId -> WS.WebSocket -> TestM () checkConvDeleteEvent cid w = WS.assertMatch_ checkTimeout w $ \notif -> do diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index cf57fdd0e3..8ce22b5035 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -167,11 +167,7 @@ instance $ do mIdpConfig <- maybe (pure Nothing) (lift . IdPConfigStore.getConfig) stiIdP let notfound = Scim.notFound "User" (idToText uid) - brigUser <- lift (BrigAccess.getAccount Brig.WithPendingInvitations uid) >>= maybe (throwError notfound) pure - unless (userTeam (accountUser brigUser) == Just stiTeam) (throwError notfound) - case Brig.veidFromBrigUser (accountUser brigUser) ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of - Right veid -> synthesizeStoredUser brigUser veid - Left _ -> throwError notfound + runMaybeT (getUserById mIdpConfig stiTeam uid) >>= maybe (throwError notfound) pure postUser :: ScimTokenInfo -> @@ -439,6 +435,9 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid ST.runValidExternalId ( \uref -> do + -- FUTUREWORK: outsource this and some other fragments from + -- `createValidScimUser` into a function `createValidScimUserBrig` similar + -- to `createValidScimUserSpar`? uid <- Id <$> Random.uuid BrigAccess.createSAML uref uid stiTeam name ManagedByScim ) @@ -474,18 +473,13 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid lift $ Logger.debug ("createValidScimUser: spar says " <> show storedUser) -- {(arianvp): these two actions we probably want to make transactional.} - lift $ do - -- Store scim timestamps, saml credentials, scim externalId locally in spar. - ScimUserTimesStore.write storedUser - ST.runValidExternalId - (`SAMLUserStore.insert` buid) - (\email -> ScimExternalIdStore.insert stiTeam email buid) - veid + createValidScimUserSpar stiTeam buid storedUser veid -- If applicable, trigger email validation procedure on brig. lift $ ST.runValidExternalId (validateEmailIfExists buid) (\_ -> pure ()) veid - -- {suspension via scim: if we don't reach the following line, the user will be active.} + -- TODO: suspension via scim is brittle, and may leave active users behind: if we don't + -- reach the following line due to a crash, the user will be active. lift $ do old <- BrigAccess.getStatus buid let new = ST.scimActiveFlagToAccountStatus old (Scim.unScimBool <$> active) @@ -493,6 +487,28 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid when (new /= old) $ BrigAccess.setStatus buid new pure storedUser +-- | Store scim timestamps, saml credentials, scim externalId locally in spar. Table +-- `spar.scim_external` gets an entry iff there is no `UserRef`: if there is, we don't do a +-- lookup in that table either, but compute the `externalId` from the `UserRef`. +createValidScimUserSpar :: + forall m r. + ( (m ~ Scim.ScimHandler (Sem r)), + Member ScimExternalIdStore r, + Member ScimUserTimesStore r, + Member SAMLUserStore r + ) => + TeamId -> + UserId -> + Scim.StoredUser ST.SparTag -> + ST.ValidExternalId -> + m () +createValidScimUserSpar stiTeam uid storedUser veid = lift $ do + ScimUserTimesStore.write storedUser + ST.runValidExternalId + ((`SAMLUserStore.insert` uid)) + (\email -> ScimExternalIdStore.insert stiTeam email uid) + veid + -- TODO(arianvp): how do we get this safe w.r.t. race conditions / crashes? updateValidScimUser :: forall m r. @@ -872,15 +888,46 @@ synthesizeScimUser info = Scim.active = Just . Scim.ScimBool $ info ^. ST.vsuActive } +getUserById :: + forall r. + ( Member BrigAccess r, + Member (Input Opts) r, + Member (Logger (Msg -> Msg)) r, + Member Now r, + Member SAMLUserStore r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r + ) => + Maybe IdP -> + TeamId -> + UserId -> + MaybeT (Scim.ScimHandler (Sem r)) (Scim.StoredUser ST.SparTag) +getUserById midp stiTeam uid = do + brigUser <- MaybeT . lift $ BrigAccess.getAccount Brig.WithPendingInvitations uid + let mbveid = + Brig.veidFromBrigUser + (accountUser brigUser) + ((^. SAML.idpMetadata . SAML.edIssuer) <$> midp) + case mbveid of + Right veid | userTeam (accountUser brigUser) == Just stiTeam -> lift $ do + storedUser :: Scim.StoredUser ST.SparTag <- synthesizeStoredUser brigUser veid + -- if we get a user from brig that hasn't been touched by scim yet, we call this + -- function to move it under scim control. + assertExternalIdNotUsedElsewhere stiTeam veid uid + createValidScimUserSpar stiTeam uid storedUser veid + pure storedUser + _ -> Applicative.empty + scimFindUserByHandle :: - Members - '[ Input Opts, - Now, - Logger (Msg -> Msg), - BrigAccess, - ScimUserTimesStore - ] - r => + forall r. + ( Member BrigAccess r, + Member (Input Opts) r, + Member (Logger (Msg -> Msg)) r, + Member Now r, + Member SAMLUserStore r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r + ) => Maybe IdP -> TeamId -> Text -> @@ -888,10 +935,7 @@ scimFindUserByHandle :: scimFindUserByHandle mIdpConfig stiTeam hndl = do handle <- MaybeT . pure . parseHandle . Text.toLower $ hndl brigUser <- MaybeT . lift . BrigAccess.getByHandle $ handle - guard $ userTeam (accountUser brigUser) == Just stiTeam - case Brig.veidFromBrigUser (accountUser brigUser) ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of - Right veid -> lift $ synthesizeStoredUser brigUser veid - Left _ -> Applicative.empty + getUserById mIdpConfig stiTeam . userId . accountUser $ brigUser -- | Construct a 'ValidExternalid'. If it an 'Email', find the non-SAML SCIM user in spar; if -- that fails, find the user by email in brig. If it is a 'UserRef', find the SAML user. @@ -901,16 +945,14 @@ scimFindUserByHandle mIdpConfig stiTeam hndl = do -- successful authentication with their SAML credentials. scimFindUserByEmail :: forall r. - Members - '[ Input Opts, - Now, - Logger (Msg -> Msg), - BrigAccess, - ScimExternalIdStore, - ScimUserTimesStore, - SAMLUserStore - ] - r => + ( Member BrigAccess r, + Member (Input Opts) r, + Member (Logger (Msg -> Msg)) r, + Member Now r, + Member SAMLUserStore r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r + ) => Maybe IdP -> TeamId -> Text -> @@ -925,8 +967,7 @@ scimFindUserByEmail mIdpConfig stiTeam email = do veid <- MaybeT (either (const Nothing) Just <$> runExceptT (mkValidExternalId mIdpConfig (pure email))) uid <- MaybeT . lift $ ST.runValidExternalId withUref withEmailOnly veid brigUser <- MaybeT . lift . BrigAccess.getAccount Brig.WithPendingInvitations $ uid - guard $ userTeam (accountUser brigUser) == Just stiTeam - lift $ synthesizeStoredUser brigUser veid + getUserById mIdpConfig stiTeam . userId . accountUser $ brigUser where withUref :: SAML.UserRef -> Sem r (Maybe UserId) withUref uref = do diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index a2efd0acfd..3d323d9e5c 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -30,7 +30,7 @@ where import Bilge import Bilge.Assert -import Brig.Types.Intra (AccountStatus (Active, PendingInvitation, Suspended), accountStatus, accountUser) +import Brig.Types.Intra (AccountStatus (Active, PendingInvitation, Suspended), UserAccount (..), accountStatus, accountUser) import Brig.Types.User as Brig import qualified Control.Exception import Control.Lens @@ -46,7 +46,7 @@ import Data.Aeson.Types (fromJSON, toJSON) import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import qualified Data.Csv as Csv -import Data.Handle (Handle (Handle), fromHandle) +import Data.Handle (Handle (Handle), fromHandle, parseHandleEither) import Data.Id (TeamId, UserId, randomId) import Data.Ix (inRange) import Data.Misc (HttpsUrl, mkHttpsUrl) @@ -58,6 +58,8 @@ import Imports import qualified Network.Wai.Utilities.Error as Wai import qualified SAML2.WebSSO as SAML import qualified SAML2.WebSSO.Test.MockResponse as SAML +import SAML2.WebSSO.Test.Util.TestSP (makeSampleIdPMetadata) +import qualified SAML2.WebSSO.Test.Util.Types as SAML import qualified Spar.Intra.BrigApp as Intra import Spar.Scim import Spar.Scim.Types (normalizeLikeStored) @@ -78,6 +80,7 @@ import qualified Web.Scim.Schema.User as Scim.User import qualified Wire.API.Team.Export as CsvExport import qualified Wire.API.Team.Feature as Feature import Wire.API.Team.Invitation (Invitation (..)) +import Wire.API.User.Identity (emailToSAMLNameID) import Wire.API.User.IdentityProvider (IdP) import qualified Wire.API.User.IdentityProvider as User import Wire.API.User.RichInfo @@ -98,6 +101,8 @@ spec = do specAzureQuirks specEmailValidation specSuspend + specImportToScimFromSAML + specImportToScimFromInvitation specSCIMManaged describe "CRUD operations maintain invariants in mapScimToBrig, mapBrigToScim." $ do it "..." $ do @@ -106,6 +111,199 @@ spec = do it "works" $ do pendingWith "write a list of unit tests here that make the mapping explicit, exhaustive, and easy to read." +specImportToScimFromSAML :: SpecWith TestEnv +specImportToScimFromSAML = + describe "Create with SAML autoprovisioning; then re-provision with SCIM" $ do + forM_ ((,,) <$> [minBound ..] <*> [minBound ..] <*> [minBound ..]) $ \(x, y, z) -> check x y z + where + check :: Bool -> Bool -> Feature.TeamFeatureStatusValue -> SpecWith TestEnv + check sameHandle sameDisplayName valemail = it (show (sameHandle, sameDisplayName, valemail)) $ do + (_ownerid, teamid, idp, (_, privCreds)) <- registerTestIdPWithMeta + setSamlEmailValidation teamid valemail + + -- saml-auto-provision a new user + (usr :: Scim.User.User SparTag, email :: Email) <- do + (usr, email) <- randomScimUserWithEmail + pure + ( -- when auto-provisioning via saml, user display name is set to saml name id. + usr {Scim.User.displayName = Just $ fromEmail email}, + email + ) + + (uref :: SAML.UserRef, uid :: UserId) <- do + let uref = SAML.UserRef tenant subj + subj = emailToSAMLNameID email + tenant = idp ^. SAML.idpMetadata . SAML.edIssuer + !(Just !uid) <- createViaSaml idp privCreds uref + samlUserShouldSatisfy uref isJust + pure (uref, uid) + + let handle = fromRight undefined . parseHandleEither $ Scim.User.userName usr + runSpar (BrigAccess.setHandle uid handle) + + assertSparCassandraUref (uref, Just uid) + assertSparCassandraScim ((teamid, email), Nothing) + assertBrigCassandra uid uref usr (valemail, False) ManagedByWire + + -- activate email + case valemail of + Feature.TeamFeatureEnabled -> do + asks (view teBrig) >>= \brig -> call (activateEmail brig email) + assertBrigCassandra uid uref usr (valemail, True) ManagedByWire + Feature.TeamFeatureDisabled -> do + pure () + + -- now import to scim + tok :: ScimToken <- do + -- this can only happen now, since it turns off saml-autoprovisioning. + registerScimToken teamid (Just (idp ^. SAML.idpId)) + + storedUserGot :: Scim.UserC.StoredUser SparTag <- do + resp <- + aFewTimes (getUser_ (Just tok) uid =<< view teSpar) ((== 200) . statusCode) + u {Scim.User.userName = Scim.User.userName usr_}) + & (if sameDisplayName then id else \u -> u {Scim.User.displayName = Scim.User.displayName usr_}) + & pure + + storedUserUpdated :: Scim.UserC.StoredUser SparTag <- do + resp <- + aFewTimes (updateUser_ (Just tok) (Just uid) usr' =<< view teSpar) ((== 200) . statusCode) + TestSpar (UserId, TeamId) + createTeam = do + env <- ask + call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + + invite :: HasCallStack => UserId -> TeamId -> TestSpar (UserId, Email) + invite owner teamid = do + env <- ask + memberInvited <- call (inviteAndRegisterUser (env ^. teBrig) owner teamid) + let memberIdInvited = userId memberInvited + emailInvited = maybe (error "must have email") id (userEmail memberInvited) + pure (memberIdInvited, emailInvited) + + addSamlIdP :: HasCallStack => UserId -> TestSpar (SAML.IdPConfig User.WireIdP, SAML.SignPrivCreds) + addSamlIdP userid = do + env <- ask + apiVersion <- view teWireIdPAPIVersion + SAML.SampleIdP idpmeta privkey _ _ <- makeSampleIdPMetadata + idp <- call $ callIdpCreate apiVersion (env ^. teSpar) (Just userid) idpmeta + pure (idp, privkey) + + reProvisionWithScim :: HasCallStack => Bool -> Maybe (SAML.IdPConfig User.WireIdP) -> TeamId -> UserId -> ReaderT TestEnv IO () + reProvisionWithScim changeHandle mbidp teamid userid = do + tok :: ScimToken <- do + registerScimToken teamid ((^. SAML.idpId) <$> mbidp) + + storedUserGot :: Scim.UserC.StoredUser SparTag <- do + resp <- + aFewTimes (getUser_ (Just tok) userid =<< view teSpar) ((== 200) . statusCode) + (SAML.IdPConfig User.WireIdP, SAML.SignPrivCreds) -> Email -> TestSpar () + signInWithSaml (idp, privCreds) email = do + let uref = SAML.UserRef tenant subj + subj = emailToSAMLNameID email + tenant = idp ^. SAML.idpMetadata . SAML.edIssuer + void $ createViaSaml idp privCreds uref + + check :: Bool -> SpecWith TestEnv + check changeHandle = it (show changeHandle) $ do + (ownerid, teamid) <- createTeam + (userid, email) <- invite ownerid teamid + idp <- addSamlIdP ownerid + reProvisionWithScim changeHandle (Just $ fst idp) teamid userid + signInWithSaml idp email + +assertSparCassandraUref :: HasCallStack => (SAML.UserRef, Maybe UserId) -> TestSpar () +assertSparCassandraUref (uref, urefAnswer) = do + liftIO . (`shouldBe` urefAnswer) + =<< runSpar (SAMLUserStore.get uref) + +assertSparCassandraScim :: HasCallStack => ((TeamId, Email), Maybe UserId) -> TestSpar () +assertSparCassandraScim ((teamid, email), scimAnswer) = do + liftIO . (`shouldBe` scimAnswer) + =<< runSpar (ScimExternalIdStore.lookup teamid email) + +assertBrigCassandra :: + HasCallStack => + UserId -> + SAML.UserRef -> + Scim.User.User SparTag -> + (Feature.TeamFeatureStatusValue, Bool) -> + ManagedBy -> + TestSpar () +assertBrigCassandra uid uref usr (valemail, emailValidated) managedBy = do + runSpar (BrigAccess.getAccount NoPendingInvitations uid) >>= \(Just acc) -> liftIO $ do + let handle = fromRight errmsg . parseHandleEither $ Scim.User.userName usr + where + errmsg = error . show . Scim.User.userName $ usr + + name = Name . fromMaybe (error "name") $ Scim.User.displayName usr + + email = case (valemail, emailValidated) of + (Feature.TeamFeatureEnabled, True) -> + Just . fromJust . parseEmail . fromJust . Scim.User.externalId $ usr + _ -> + Nothing + + accountStatus acc `shouldBe` Active + userId (accountUser acc) `shouldBe` uid + userHandle (accountUser acc) `shouldBe` Just handle + userDisplayName (accountUser acc) `shouldBe` name + userManagedBy (accountUser acc) `shouldBe` managedBy + + userIdentity (accountUser acc) + `shouldBe` Just (SSOIdentity (UserSSOId uref) email Nothing) + specSuspend :: SpecWith TestEnv specSuspend = do describe "suspend" $ do @@ -686,42 +884,42 @@ testScimCreateVsUserRef = do subj' = either (error . show) id $ SAML.mkNameID uname' Nothing Nothing Nothing tenant' = idp ^. SAML.idpMetadata . SAML.edIssuer createViaSamlFails idp privCreds uref' - where - samlUserShouldSatisfy :: HasCallStack => SAML.UserRef -> (Maybe UserId -> Bool) -> TestSpar () - samlUserShouldSatisfy uref property = do - muid <- getUserIdViaRef' uref - liftIO $ muid `shouldSatisfy` property - - createViaSamlResp :: HasCallStack => IdP -> SAML.SignPrivCreds -> SAML.UserRef -> TestSpar ResponseLBS - createViaSamlResp idp privCreds (SAML.UserRef _ subj) = do - authnReq <- negotiateAuthnRequest idp - let tid = idp ^. SAML.idpExtraInfo . User.wiTeam - spmeta <- getTestSPMetadata tid - authnResp <- - runSimpleSP $ - SAML.mkAuthnResponseWithSubj subj privCreds idp spmeta authnReq True - submitAuthnResponse tid authnResp IdP -> SAML.SignPrivCreds -> SAML.UserRef -> TestSpar () - createViaSamlFails idp privCreds uref = do - resp <- createViaSamlResp idp privCreds uref - liftIO $ do - maybe (error "no body") cs (responseBody resp) - `shouldNotContain` "wire:sso:error:success" - - createViaSaml :: HasCallStack => IdP -> SAML.SignPrivCreds -> SAML.UserRef -> TestSpar (Maybe UserId) - createViaSaml idp privCreds uref = do - resp <- createViaSamlResp idp privCreds uref - liftIO $ do - maybe (error "no body") cs (responseBody resp) - `shouldContain` "wire:sso:success" - getUserIdViaRef' uref - - deleteViaBrig :: UserId -> TestSpar () - deleteViaBrig uid = do - brig <- view teBrig - (call . delete $ brig . paths ["i", "users", toByteString' uid]) - !!! const 202 === statusCode + +samlUserShouldSatisfy :: HasCallStack => SAML.UserRef -> (Maybe UserId -> Bool) -> TestSpar () +samlUserShouldSatisfy uref property = do + muid <- getUserIdViaRef' uref + liftIO $ muid `shouldSatisfy` property + +createViaSamlResp :: HasCallStack => IdP -> SAML.SignPrivCreds -> SAML.UserRef -> TestSpar ResponseLBS +createViaSamlResp idp privCreds (SAML.UserRef _ subj) = do + authnReq <- negotiateAuthnRequest idp + let tid = idp ^. SAML.idpExtraInfo . User.wiTeam + spmeta <- getTestSPMetadata tid + authnResp <- + runSimpleSP $ + SAML.mkAuthnResponseWithSubj subj privCreds idp spmeta authnReq True + submitAuthnResponse tid authnResp IdP -> SAML.SignPrivCreds -> SAML.UserRef -> TestSpar () +createViaSamlFails idp privCreds uref = do + resp <- createViaSamlResp idp privCreds uref + liftIO $ do + maybe (error "no body") cs (responseBody resp) + `shouldNotContain` "wire:sso:error:success" + +createViaSaml :: HasCallStack => IdP -> SAML.SignPrivCreds -> SAML.UserRef -> TestSpar (Maybe UserId) +createViaSaml idp privCreds uref = do + resp <- createViaSamlResp idp privCreds uref + liftIO $ do + maybe (error "no body") cs (responseBody resp) + `shouldContain` "wire:sso:success" + getUserIdViaRef' uref + +deleteViaBrig :: UserId -> TestSpar () +deleteViaBrig uid = do + brig <- view teBrig + (call . delete $ brig . paths ["i", "users", toByteString' uid]) + !!! const 202 === statusCode testCreateUserTimeout :: TestSpar () testCreateUserTimeout = do