diff --git a/cassandra-schema.cql b/cassandra-schema.cql index b6a72d7ea3..0a203375e3 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -418,6 +418,7 @@ CREATE TABLE galley_test.conversation_codes ( key ascii, scope int, conversation uuid, + password blob, value ascii, PRIMARY KEY (key, scope) ) WITH CLUSTERING ORDER BY (scope ASC) diff --git a/changelog.d/2-features/pr-3149 b/changelog.d/2-features/pr-3149 new file mode 100644 index 0000000000..c9d9e251f2 --- /dev/null +++ b/changelog.d/2-features/pr-3149 @@ -0,0 +1 @@ +Optional password for guest links diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 7c335c9cf6..8981908490 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -43,6 +43,7 @@ , hex , hostname-validate , hscim +, HsOpenSSL , hspec , hspec-wai , http-api-data @@ -73,6 +74,7 @@ , saml2-web-sso , schema-profunctor , scientific +, scrypt , servant , servant-client , servant-client-core @@ -150,6 +152,7 @@ mkDerivation { hashable hostname-validate hscim + HsOpenSSL http-api-data http-media http-types @@ -175,6 +178,7 @@ mkDerivation { saml2-web-sso schema-profunctor scientific + scrypt servant servant-client servant-client-core diff --git a/services/brig/src/Brig/Budget.hs b/libs/wire-api/src/Wire/API/Budget.hs similarity index 99% rename from services/brig/src/Brig/Budget.hs rename to libs/wire-api/src/Wire/API/Budget.hs index cf952a3ed7..4b7dd2ee2c 100644 --- a/services/brig/src/Brig/Budget.hs +++ b/libs/wire-api/src/Wire/API/Budget.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Budget +module Wire.API.Budget ( Budget (..), BudgetKey (..), Budgeted (..), diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index ee9b7c6e2a..d5dfd7db87 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -283,7 +283,8 @@ conversationSchema v = -- link about the conversation. data ConversationCoverView = ConversationCoverView { cnvCoverConvId :: ConvId, - cnvCoverName :: Maybe Text + cnvCoverName :: Maybe Text, + cnvCoverHasPassword :: Bool } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConversationCoverView) @@ -299,6 +300,7 @@ instance ToSchema ConversationCoverView where $ ConversationCoverView <$> cnvCoverConvId .= field "id" schema <*> cnvCoverName .= optField "name" (maybeWithDefault A.Null schema) + <*> cnvCoverHasPassword .= field "has_password" schema data ConversationList a = ConversationList { convList :: [a], diff --git a/libs/wire-api/src/Wire/API/Conversation/Code.hs b/libs/wire-api/src/Wire/API/Conversation/Code.hs index 03e3dc30aa..341a7aea3f 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Code.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Code.hs @@ -22,6 +22,8 @@ module Wire.API.Conversation.Code ( -- * ConversationCode ConversationCode (..), mkConversationCode, + CreateConversationCodeRequest (..), + JoinConversationByCode (..), -- * re-exports Code.Key (..), @@ -34,17 +36,49 @@ import Data.Aeson (FromJSON, ToJSON) import Data.ByteString.Conversion (toByteString') -- FUTUREWORK: move content of Data.Code here? import Data.Code as Code -import Data.Misc (HttpsUrl (HttpsUrl)) +import Data.Misc import Data.Schema import qualified Data.Swagger as S import Imports import qualified URI.ByteString as URI import Wire.Arbitrary (Arbitrary, GenericUniform (..)) +newtype CreateConversationCodeRequest = CreateConversationCodeRequest + { cccrPassword :: Maybe PlainTextPassword8 + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform CreateConversationCodeRequest) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema CreateConversationCodeRequest + +instance ToSchema CreateConversationCodeRequest where + schema = + objectWithDocModifier + "CreateConversationCodeRequest" + (description ?~ "Optional request body for creating a conversation code with a password") + $ CreateConversationCodeRequest <$> cccrPassword .= maybe_ (optField "password" schema) + +data JoinConversationByCode = JoinConversationByCode + { jcbcCode :: ConversationCode, + jcbcPassword :: Maybe PlainTextPassword8 + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform JoinConversationByCode) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema JoinConversationByCode + +instance ToSchema JoinConversationByCode where + schema = + objectWithDocModifier + "JoinConversationByCode" + (description ?~ "Request body for joining a conversation by code") + $ JoinConversationByCode + <$> jcbcCode .= fieldWithDocModifier "code" (description ?~ "Conversation code") schema + <*> jcbcPassword .= maybe_ (optField "password" schema) + data ConversationCode = ConversationCode { conversationKey :: Code.Key, conversationCode :: Code.Value, - conversationUri :: Maybe HttpsUrl + conversationUri :: Maybe HttpsUrl, + conversationHasPassword :: Maybe Bool } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConversationCode) @@ -73,13 +107,15 @@ instance ToSchema ConversationCode where (description ?~ "Full URI (containing key/code) to join a conversation") schema ) + <*> conversationHasPassword .= maybe_ (optField "has_password" schema) -mkConversationCode :: Code.Key -> Code.Value -> HttpsUrl -> ConversationCode -mkConversationCode k v (HttpsUrl prefix) = +mkConversationCode :: Code.Key -> Code.Value -> Bool -> HttpsUrl -> ConversationCode +mkConversationCode k v hasPw (HttpsUrl prefix) = ConversationCode { conversationKey = k, conversationCode = v, - conversationUri = Just (HttpsUrl link) + conversationUri = Just (HttpsUrl link), + conversationHasPassword = Just hasPw } where q = [("key", toByteString' k), ("code", toByteString' v)] diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 5019282d33..cd8dbf77ca 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -93,6 +93,7 @@ data GalleyError | ConvMemberNotFound | GuestLinksDisabled | CodeNotFound + | InvalidConversationPassword | InvalidPermissions | InvalidTeamStatusUpdate | AccessDenied @@ -235,6 +236,8 @@ type instance MapError 'GuestLinksDisabled = 'StaticError 409 "guest-links-disab type instance MapError 'CodeNotFound = 'StaticError 404 "no-conversation-code" "Conversation code not found" +type instance MapError 'InvalidConversationPassword = 'StaticError 403 "invalid-conversation-password" "Invalid conversation password" + type instance MapError 'InvalidPermissions = 'StaticError 403 "invalid-permissions" "The specified permissions are invalid" type instance MapError 'InvalidTeamStatusUpdate = 'StaticError 403 "invalid-team-status-update" "Cannot use this endpoint to update the team to the given status." diff --git a/services/brig/src/Brig/Password.hs b/libs/wire-api/src/Wire/API/Password.hs similarity index 98% rename from services/brig/src/Brig/Password.hs rename to libs/wire-api/src/Wire/API/Password.hs index caffaac754..349c5f5b72 100644 --- a/services/brig/src/Brig/Password.hs +++ b/libs/wire-api/src/Wire/API/Password.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Password +module Wire.API.Password ( Password, genPassword, mkSafePassword, diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index dce0290b9a..994ebb1857 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -25,6 +25,7 @@ import Imports hiding (head) import Servant hiding (WithStatus) import Servant.Swagger.Internal.Orphans () import Wire.API.Conversation +import Wire.API.Conversation.Code import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Error @@ -316,6 +317,7 @@ type ConversationAPI = "get-conversation-by-reusable-code" ( Summary "Get limited conversation information by key/code pair" :> CanThrow 'CodeNotFound + :> CanThrow 'InvalidConversationPassword :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> CanThrow 'GuestLinksDisabled @@ -548,14 +550,16 @@ type ConversationAPI = -- This endpoint can lead to the following events being sent: -- - MemberJoin event to members :<|> Named - "join-conversation-by-code-unqualified" + "join-conversation-by-code-unqualified@v3" ( Summary "Join a conversation using a reusable code.\ \If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.\ \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled." + :> Until 'V4 :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> CanThrow 'CodeNotFound + :> CanThrow 'InvalidConversationPassword :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'GuestLinksDisabled @@ -569,6 +573,32 @@ type ConversationAPI = :> ReqBody '[Servant.JSON] ConversationCode :> MultiVerb 'POST '[Servant.JSON] ConvJoinResponses (UpdateResult Event) ) + -- This endpoint can lead to the following events being sent: + -- - MemberJoin event to members + :<|> Named + "join-conversation-by-code-unqualified" + ( Summary + "Join a conversation using a reusable code.\ + \If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.\ + \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled." + :> From 'V4 + :> MakesFederatedCall 'Galley "on-conversation-updated" + :> MakesFederatedCall 'Galley "on-new-remote-conversation" + :> CanThrow 'CodeNotFound + :> CanThrow 'InvalidConversationPassword + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'GuestLinksDisabled + :> CanThrow 'InvalidOperation + :> CanThrow 'NotATeamMember + :> CanThrow 'TooManyMembers + :> ZLocalUser + :> ZConn + :> "conversations" + :> "join" + :> ReqBody '[Servant.JSON] JoinConversationByCode + :> MultiVerb 'POST '[Servant.JSON] ConvJoinResponses (UpdateResult Event) + ) :<|> Named "code-check" ( Summary @@ -577,6 +607,7 @@ type ConversationAPI = \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled." :> CanThrow 'CodeNotFound :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidConversationPassword :> "conversations" :> "code-check" :> ReqBody '[Servant.JSON] ConversationCode @@ -588,9 +619,27 @@ type ConversationAPI = ) -- this endpoint can lead to the following events being sent: -- - ConvCodeUpdate event to members, if code didn't exist before + :<|> Named + "create-conversation-code-unqualified@v3" + ( Summary "Create or recreate a conversation code" + :> Until 'V4 + :> DescriptionOAuthScope 'WriteConversationsCode + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'GuestLinksDisabled + :> ZUser + :> ZOptConn + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "code" + :> CreateConversationCodeVerb + ) + -- this endpoint can lead to the following events being sent: + -- - ConvCodeUpdate event to members, if code didn't exist before :<|> Named "create-conversation-code-unqualified" ( Summary "Create or recreate a conversation code" + :> From 'V4 :> DescriptionOAuthScope 'WriteConversationsCode :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound @@ -600,6 +649,7 @@ type ConversationAPI = :> "conversations" :> Capture' '[Description "Conversation ID"] "cnv" ConvId :> "code" + :> ReqBody '[JSON] CreateConversationCodeRequest :> CreateConversationCodeVerb ) :<|> Named diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationCode_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationCode_user.hs index 27c4ed1676..a2867be2de 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationCode_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationCode_user.hs @@ -68,7 +68,8 @@ testObject_ConversationCode_user_1 = uriQuery = Query {queryPairs = []}, uriFragment = Nothing } - ) + ), + conversationHasPassword = Nothing } testObject_ConversationCode_user_2 :: ConversationCode @@ -76,5 +77,6 @@ testObject_ConversationCode_user_2 = ConversationCode { conversationKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "NEN=eLUWHXclTp=_2Nap"))}, conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))}, - conversationUri = Nothing + conversationUri = Nothing, + conversationHasPassword = Nothing } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs index 1370035150..87f13655a5 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs @@ -79,7 +79,8 @@ testObject_Event_conversation_3 = ( ConversationCode { conversationKey = Key {asciiKey = unsafeRange "CRdONS7988O2QdyndJs1"}, conversationCode = Value {asciiValue = unsafeRange "7d6713"}, - conversationUri = Just $ HttpsUrl (URI {uriScheme = Scheme {schemeBS = "https"}, uriAuthority = Just (Authority {authorityUserInfo = Nothing, authorityHost = Host {hostBS = "example.com"}, authorityPort = Nothing}), uriPath = "", uriQuery = Query {queryPairs = []}, uriFragment = Nothing}) + conversationUri = Just $ HttpsUrl (URI {uriScheme = Scheme {schemeBS = "https"}, uriAuthority = Just (Authority {authorityUserInfo = Nothing, authorityHost = Host {hostBS = "example.com"}, authorityPort = Nothing}), uriPath = "", uriQuery = Query {queryPairs = []}, uriFragment = Nothing}), + conversationHasPassword = Nothing } ) } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs index 3e193a3e66..471f1174bb 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs @@ -290,7 +290,8 @@ testObject_Event_user_14 = ConversationCode { conversationKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "NEN=eLUWHXclTp=_2Nap"))}, conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))}, - conversationUri = Nothing + conversationUri = Nothing, + conversationHasPassword = Nothing } testObject_Event_user_15 :: Event diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationCoverView.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationCoverView.hs index df62ab6169..a6eeb8a6f1 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationCoverView.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationCoverView.hs @@ -28,15 +28,18 @@ testObject_ConversationCoverView_1 = ConversationCoverView (Id (fromJust (UUID.fromString "00000018-0000-0020-0000-000e00000002"))) Nothing + False testObject_ConversationCoverView_2 :: ConversationCoverView testObject_ConversationCoverView_2 = ConversationCoverView (Id (fromJust (UUID.fromString "00000018-0000-0020-0000-000e00000002"))) (Just "conversation name") + False testObject_ConversationCoverView_3 :: ConversationCoverView testObject_ConversationCoverView_3 = ConversationCoverView (Id (fromJust (UUID.fromString "00000018-0000-0020-0000-000e00000002"))) (Just "") + True diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json index 485f5b1ca3..917cfe4360 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json @@ -1,4 +1,5 @@ { "id": "00000018-0000-0020-0000-000e00000002", - "name": null + "name": null, + "has_password": false } diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json index 2087db7b13..c36128fa05 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json @@ -1,4 +1,5 @@ { "id": "00000018-0000-0020-0000-000e00000002", - "name": "conversation name" + "name": "conversation name", + "has_password": false } diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json index 0c280976b1..453b2e9b2d 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json @@ -1,4 +1,5 @@ { "id": "00000018-0000-0020-0000-000e00000002", - "name": "" + "name": "", + "has_password": true } 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 72d4de4cb9..9b83e22e53 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 @@ -119,6 +119,8 @@ tests = testRoundTrip @Conversation.Bot.RemoveBotResponse, testRoundTrip @Conversation.Bot.UpdateBotPrekeys, testRoundTrip @Conversation.Code.ConversationCode, + testRoundTrip @Conversation.Code.JoinConversationByCode, + testRoundTrip @Conversation.Code.CreateConversationCodeRequest, testRoundTrip @Conversation.Member.MemberUpdate, testRoundTrip @Conversation.Member.MutedStatus, testRoundTrip @Conversation.Member.Member, diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index a6da3bc148..682e773324 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -15,6 +15,7 @@ library exposed-modules: Wire.API.ApplyMods Wire.API.Asset + Wire.API.Budget Wire.API.Call.Config Wire.API.Connection Wire.API.ConverProtoLens @@ -63,6 +64,7 @@ library Wire.API.MLS.Welcome Wire.API.Notification Wire.API.OAuth + Wire.API.Password Wire.API.Properties Wire.API.Provider Wire.API.Provider.Bot @@ -242,6 +244,7 @@ library , hashable , hostname-validate , hscim + , HsOpenSSL , http-api-data , http-media , http-types @@ -267,6 +270,7 @@ library , saml2-web-sso , schema-profunctor , scientific + , scrypt , servant , servant-client , servant-client-core diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 34babd7b7f..ef9ccbc81a 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -42,7 +42,6 @@ library Brig.AWS Brig.AWS.SesNotification Brig.AWS.Types - Brig.Budget Brig.Calling Brig.Calling.API Brig.Calling.Internal @@ -92,7 +91,6 @@ library Brig.IO.Journal Brig.Locale Brig.Options - Brig.Password Brig.Phone Brig.Provider.API Brig.Provider.DB diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 380aecc383..57e2da8039 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -28,7 +28,6 @@ import Brig.API.Error (throwStd) import Brig.API.Handler (Handler) import Brig.App import qualified Brig.Options as Opt -import Brig.Password (Password, mkSafePassword, verifyPassword) import Cassandra hiding (Set) import qualified Cassandra as C import Control.Error (assertMay, failWith, failWithM) @@ -49,6 +48,7 @@ import Polysemy (Member) import Servant hiding (Handler, Tagged) import Wire.API.Error import Wire.API.OAuth as OAuth +import Wire.API.Password (Password, mkSafePassword, verifyPassword) import qualified Wire.API.Routes.Internal.Brig.OAuth as I import Wire.API.Routes.Named (Named (..)) import Wire.API.Routes.Public.Brig.OAuth diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 71b164e58e..c21201f28c 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -124,7 +124,6 @@ import qualified Brig.Federation.Client as Federation import qualified Brig.IO.Intra as Intra import qualified Brig.InternalEvent.Types as Internal import Brig.Options hiding (Timeout, internalEvents) -import Brig.Password import qualified Brig.Queue as Queue import qualified Brig.Team.DB as Team import Brig.Team.Types (ShowOrHideInvitationUrl (..)) @@ -172,6 +171,7 @@ import Wire.API.Connection import Wire.API.Error import qualified Wire.API.Error.Brig as E import Wire.API.Federation.Error +import Wire.API.Password import Wire.API.Routes.Internal.Brig.Connection import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team import Wire.API.Team hiding (newTeam) diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 3f79850ad7..7f4b0ee846 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -77,7 +77,6 @@ where import Brig.App (Env, currentTime, settings, viewFederationDomain, zauthEnv) import Brig.Data.Instances () import Brig.Options -import Brig.Password import Brig.Types.Intra import Brig.Types.User (HavePendingInvitations (NoPendingInvitations, WithPendingInvitations)) import qualified Brig.ZAuth as ZAuth @@ -95,6 +94,7 @@ import Data.Range (fromRange) import Data.Time (addUTCTime) import Data.UUID.V4 import Imports +import Wire.API.Password import Wire.API.Provider.Service import qualified Wire.API.Team.Feature as ApiFt import Wire.API.User diff --git a/services/brig/src/Brig/Phone.hs b/services/brig/src/Brig/Phone.hs index 55dabbd3a1..d9955db34a 100644 --- a/services/brig/src/Brig/Phone.hs +++ b/services/brig/src/Brig/Phone.hs @@ -39,7 +39,6 @@ where import Bilge.Retry (httpHandlers) import Brig.App -import Brig.Budget import Cassandra (MonadClient) import Control.Lens (view) import Control.Monad.Catch @@ -55,6 +54,7 @@ import Ropes.Twilio (LookupDetail (..)) import qualified Ropes.Twilio as Twilio import qualified System.Logger.Class as Log import System.Logger.Message (field, msg, val, (~~)) +import Wire.API.Budget import Wire.API.User ------------------------------------------------------------------------------- diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 23872d077f..3fa18bc303 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -42,7 +42,6 @@ import Brig.Email (mkEmailKey) import qualified Brig.InternalEvent.Types as Internal import Brig.Options (Settings (..)) import qualified Brig.Options as Opt -import Brig.Password import Brig.Provider.DB (ServiceConn (..)) import qualified Brig.Provider.DB as DB import Brig.Provider.Email @@ -105,6 +104,7 @@ import Wire.API.Conversation.Role import Wire.API.Error import qualified Wire.API.Error.Brig as E import qualified Wire.API.Event.Conversation as Public (Event) +import Wire.API.Password import Wire.API.Provider import qualified Wire.API.Provider as Public import qualified Wire.API.Provider.Bot as Ext diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index 0a344566d6..a7d1f50a04 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -19,9 +19,6 @@ module Brig.Provider.DB where import Brig.Data.Instances () import Brig.Email (EmailKey, emailKeyOrig, emailKeyUniq) -import Brig.Password --- import Brig.Provider.DB.Instances () - import Brig.Types.Instances () import Brig.Types.Provider.Tag import Cassandra as C @@ -34,6 +31,7 @@ import qualified Data.Set as Set import qualified Data.Text as Text import Imports import UnliftIO (mapConcurrently) +import Wire.API.Password import Wire.API.Provider import Wire.API.Provider.Service hiding (updateServiceTags) import Wire.API.Provider.Service.Tag diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 125e844446..a6ceb06f5d 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -41,7 +41,6 @@ import Bilge.RPC import Brig.API.Types import Brig.API.User (changeSingleAccountStatus) import Brig.App -import Brig.Budget import qualified Brig.Code as Code import qualified Brig.Data.Activation as Data import Brig.Data.Client @@ -78,6 +77,7 @@ import Network.Wai.Utilities.Error ((!>>)) import Polysemy import System.Logger (field, msg, val, (~~)) import qualified System.Logger.Class as Log +import Wire.API.Budget import Wire.API.Team.Feature import qualified Wire.API.Team.Feature as Public import Wire.API.User diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 76ef6e2d27..607620bc96 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -32,7 +32,6 @@ import qualified Bilge as Http import Bilge.Assert hiding (assert) import qualified Brig.Code as Code import qualified Brig.Options as Opts -import Brig.Password (Password, mkSafePassword) import Brig.Types.Intra import Brig.User.Auth.Cookie (revokeAllCookies) import Brig.ZAuth (ZAuth, runZAuth) @@ -69,6 +68,7 @@ import qualified Test.Tasty.HUnit as HUnit import UnliftIO.Async hiding (wait) import Util import Wire.API.Conversation (Conversation (..)) +import Wire.API.Password (Password, mkSafePassword) import qualified Wire.API.Team.Feature as Public import Wire.API.User import qualified Wire.API.User as Public diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 4e94167b01..cf7e6a12bd 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -704,6 +704,7 @@ executable galley-schema V77_MLSGroupMemberClient V78_TeamFeatureOutlookCalIntegration V79_TeamFeatureMlsE2EId + V80_AddConversationCodePassword hs-source-dirs: schema/src default-extensions: diff --git a/services/galley/schema/src/Main.hs b/services/galley/schema/src/Main.hs index 5f493498f8..0b4aae92aa 100644 --- a/services/galley/schema/src/Main.hs +++ b/services/galley/schema/src/Main.hs @@ -82,6 +82,7 @@ import qualified V76_ProposalOrigin import qualified V77_MLSGroupMemberClient import qualified V78_TeamFeatureOutlookCalIntegration import qualified V79_TeamFeatureMlsE2EId +import qualified V80_AddConversationCodePassword main :: IO () main = do @@ -149,7 +150,8 @@ main = do V76_ProposalOrigin.migration, V77_MLSGroupMemberClient.migration, V78_TeamFeatureOutlookCalIntegration.migration, - V79_TeamFeatureMlsE2EId.migration + V79_TeamFeatureMlsE2EId.migration, + V80_AddConversationCodePassword.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Galley.Cassandra -- (see also docs/developer/cassandra-interaction.md) diff --git a/services/galley/schema/src/V80_AddConversationCodePassword.hs b/services/galley/schema/src/V80_AddConversationCodePassword.hs new file mode 100644 index 0000000000..34c67a4253 --- /dev/null +++ b/services/galley/schema/src/V80_AddConversationCodePassword.hs @@ -0,0 +1,35 @@ +-- 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 V80_AddConversationCodePassword + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 80 "Add optional password to conversation_codes table" $ + schema' + [r| + ALTER TABLE conversation_codes ADD ( + password blob + ) + |] diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 73bb3cb575..1c8205e76d 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -24,6 +24,7 @@ import Galley.API.Query import Galley.API.Update import Galley.App import Galley.Cassandra.TeamFeatures +import Imports import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Conversation @@ -55,9 +56,11 @@ conversationAPI = <@> mkNamedAPI @"add-members-to-conversation-unqualified2" (callsFed addMembersUnqualifiedV2) <@> mkNamedAPI @"add-members-to-conversation" (callsFed addMembers) <@> mkNamedAPI @"join-conversation-by-id-unqualified" (callsFed joinConversationById) - <@> mkNamedAPI @"join-conversation-by-code-unqualified" (callsFed (joinConversationByReusableCode @Cassandra)) + <@> mkNamedAPI @"join-conversation-by-code-unqualified@v3" (callsFed (joinConversationByReusableCode @Cassandra)) + <@> mkNamedAPI @"join-conversation-by-code-unqualified" (callsFed (joinConversationByReusableCodeWithMaybePassword @Cassandra)) <@> mkNamedAPI @"code-check" (checkReusableCode @Cassandra) - <@> mkNamedAPI @"create-conversation-code-unqualified" (addCodeUnqualified @Cassandra) + <@> mkNamedAPI @"create-conversation-code-unqualified@v3" (addCodeUnqualified @Cassandra Nothing) + <@> mkNamedAPI @"create-conversation-code-unqualified" (addCodeUnqualifiedWithReqBody @Cassandra) <@> mkNamedAPI @"get-conversation-guest-links-status" (getConversationGuestLinksStatus @Cassandra) <@> mkNamedAPI @"remove-code-unqualified" rmCodeUnqualified <@> mkNamedAPI @"get-code" (getCode @Cassandra) diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 26290f4c1e..d5b404549d 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -61,6 +61,7 @@ import qualified Galley.API.Mapping as Mapping import Galley.API.Util import qualified Galley.Data.Conversation as Data import Galley.Data.Types (Code (codeConversation)) +import qualified Galley.Data.Types as Data import Galley.Effects import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FederatorAccess as E @@ -624,6 +625,7 @@ getConversationByReusableCode :: Member CodeStore r, Member ConversationStore r, Member (ErrorS 'CodeNotFound) r, + Member (ErrorS 'InvalidConversationPassword) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'GuestLinksDisabled) r, @@ -638,17 +640,18 @@ getConversationByReusableCode :: Value -> Sem r ConversationCoverView getConversationByReusableCode lusr key value = do - c <- verifyReusableCode (ConversationCode key value Nothing) + c <- verifyReusableCode False Nothing (ConversationCode key value Nothing Nothing) conv <- E.getConversation (codeConversation c) >>= noteS @'ConvNotFound ensureConversationAccess (tUnqualified lusr) conv CodeAccess ensureGuestLinksEnabled @db (Data.convTeam conv) - pure $ coverView conv + pure $ coverView c conv where - coverView :: Data.Conversation -> ConversationCoverView - coverView conv = + coverView :: Data.Code -> Data.Conversation -> ConversationCoverView + coverView c conv = ConversationCoverView { cnvCoverConvId = Data.convId conv, - cnvCoverName = Data.convName conv + cnvCoverName = Data.convName conv, + cnvCoverHasPassword = Data.codeHasPassword c } ensureGuestLinksEnabled :: diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 3583729285..378810c147 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -23,8 +23,10 @@ module Galley.API.Update unblockConvH, checkReusableCode, joinConversationByReusableCode, + joinConversationByReusableCodeWithMaybePassword, joinConversationById, addCodeUnqualified, + addCodeUnqualifiedWithReqBody, rmCodeUnqualified, getCode, updateUnqualifiedConversationName, @@ -131,6 +133,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Message +import Wire.API.Password (mkSafePassword) import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Routes.Public.Util (UpdateResult (..)) @@ -472,6 +475,29 @@ deleteLocalConversation lusr con lcnv = getUpdateResult :: Sem (Error NoChanges ': r) a -> Sem r (UpdateResult a) getUpdateResult = fmap (either (const Unchanged) Updated) . runError +addCodeUnqualifiedWithReqBody :: + forall db r. + ( Member CodeStore r, + Member ConversationStore r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'GuestLinksDisabled) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (Embed IO) r, + Member (Input Opts) r, + Member (TeamFeatureStore db) r, + FeaturePersistentConstraint db GuestLinksConfig + ) => + UserId -> + Maybe ConnId -> + ConvId -> + CreateConversationCodeRequest -> + Sem r AddCodeResult +addCodeUnqualifiedWithReqBody usr mZcon cnv req = addCodeUnqualified @db (Just req) usr mZcon cnv + addCodeUnqualified :: forall db r. ( Member CodeStore r, @@ -484,17 +510,19 @@ addCodeUnqualified :: Member (Input (Local ())) r, Member (Input UTCTime) r, Member (Input Opts) r, + Member (Embed IO) r, Member (TeamFeatureStore db) r, FeaturePersistentConstraint db GuestLinksConfig ) => + Maybe CreateConversationCodeRequest -> UserId -> Maybe ConnId -> ConvId -> Sem r AddCodeResult -addCodeUnqualified usr mZcon cnv = do +addCodeUnqualified mReq usr mZcon cnv = do lusr <- qualifyLocal usr lcnv <- qualifyLocal cnv - addCode @db lusr mZcon lcnv + addCode @db lusr mZcon lcnv mReq addCode :: forall db r. @@ -508,13 +536,15 @@ addCode :: Member (Input UTCTime) r, Member (Input Opts) r, Member (TeamFeatureStore db) r, + Member (Embed IO) r, FeaturePersistentConstraint db GuestLinksConfig ) => Local UserId -> Maybe ConnId -> Local ConvId -> + Maybe CreateConversationCodeRequest -> Sem r AddCodeResult -addCode lusr mZcon lcnv = do +addCode lusr mZcon lcnv mReq = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled @db (Data.convTeam conv) Query.ensureConvAdmin (Data.convLocalMembers conv) (tUnqualified lusr) @@ -526,19 +556,22 @@ addCode lusr mZcon lcnv = do case mCode of Nothing -> do code <- E.generateCode (tUnqualified lcnv) ReusableCode (Timeout 3600 * 24 * 365) -- one year FUTUREWORK: configurable - E.createCode code + mPw <- forM (cccrPassword =<< mReq) mkSafePassword + E.createCode code mPw now <- input - conversationCode <- createCode code + conversationCode <- do + cc <- createCode code + pure $ cc {conversationHasPassword = Just $ isJust mPw} let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate conversationCode) pushConversationEvent mZcon event (qualifyAs lusr (map lmId users)) bots pure $ CodeAdded event - Just code -> do + Just (code, _) -> do conversationCode <- createCode code pure $ CodeAlreadyExisted conversationCode where createCode :: Code -> Sem r ConversationCode createCode code = do - mkConversationCode (codeKey code) (codeValue code) <$> E.getConversationCodeURI + mkConversationCode (codeKey code) (codeValue code) (codeHasPassword code) <$> E.getConversationCodeURI ensureGuestsOrNonTeamMembersAllowed :: Data.Conversation -> Sem r () ensureGuestsOrNonTeamMembersAllowed conv = unless @@ -613,12 +646,12 @@ getCode lusr cnv = do ensureAccess conv CodeAccess ensureConvMember (Data.convLocalMembers conv) (tUnqualified lusr) key <- E.makeKey cnv - c <- E.getCode key ReusableCode >>= noteS @'CodeNotFound + (c, _) <- E.getCode key ReusableCode >>= noteS @'CodeNotFound returnCode c returnCode :: Member CodeStore r => Code -> Sem r ConversationCode returnCode c = do - mkConversationCode (codeKey c) (codeValue c) <$> E.getConversationCodeURI + mkConversationCode (codeKey c) (codeValue c) (codeHasPassword c) <$> E.getConversationCodeURI checkReusableCode :: forall db r. @@ -627,13 +660,14 @@ checkReusableCode :: Member (TeamFeatureStore db) r, Member (ErrorS 'CodeNotFound) r, Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidConversationPassword) r, Member (Input Opts) r, FeaturePersistentConstraint db GuestLinksConfig ) => ConversationCode -> Sem r () checkReusableCode convCode = do - code <- verifyReusableCode convCode + code <- verifyReusableCode False Nothing convCode conv <- E.getConversation (codeConversation code) >>= noteS @'ConvNotFound mapErrorS @'GuestLinksDisabled @'CodeNotFound $ Query.ensureGuestLinksEnabled @db (Data.convTeam conv) @@ -644,6 +678,7 @@ joinConversationByReusableCode :: Member CodeStore r, Member ConversationStore r, Member (ErrorS 'CodeNotFound) r, + Member (ErrorS 'InvalidConversationPassword) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'GuestLinksDisabled) r, @@ -665,8 +700,39 @@ joinConversationByReusableCode :: ConnId -> ConversationCode -> Sem r (UpdateResult Event) -joinConversationByReusableCode lusr zcon convCode = do - c <- verifyReusableCode convCode +joinConversationByReusableCode lusr zcon code = + joinConversationByReusableCodeWithMaybePassword @db lusr zcon (JoinConversationByCode code Nothing) + +joinConversationByReusableCodeWithMaybePassword :: + forall db r. + ( Member BrigAccess r, + Member CodeStore r, + Member ConversationStore r, + Member (ErrorS 'CodeNotFound) r, + Member (ErrorS 'InvalidConversationPassword) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'GuestLinksDisabled) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TooManyMembers) r, + Member FederatorAccess r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + Member (Logger (Msg -> Msg)) r, + FeaturePersistentConstraint db GuestLinksConfig + ) => + Local UserId -> + ConnId -> + JoinConversationByCode -> + Sem r (UpdateResult Event) +joinConversationByReusableCodeWithMaybePassword lusr zcon req = do + c <- verifyReusableCode True (jcbcPassword req) (jcbcCode req) conv <- E.getConversation (codeConversation c) >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled @db (Data.convTeam conv) joinConversation lusr zcon conv CodeAccess diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 582c426755..573a28fe97 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -29,7 +29,7 @@ import Data.Id as Id import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.List.Extra (chunksOf, nubOrd) import qualified Data.Map as Map -import Data.Misc (PlainTextPassword6) +import Data.Misc (PlainTextPassword6, PlainTextPassword8) import Data.Qualified import qualified Data.Set as Set import Data.Singletons @@ -76,6 +76,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.Password (verifyPassword) import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team.Member @@ -594,16 +595,25 @@ pushConversationEvent conn e lusers bots = do verifyReusableCode :: ( Member CodeStore r, - Member (ErrorS 'CodeNotFound) r + Member (ErrorS 'CodeNotFound) r, + Member (ErrorS 'InvalidConversationPassword) r ) => + Bool -> + Maybe PlainTextPassword8 -> ConversationCode -> Sem r DataTypes.Code -verifyReusableCode convCode = do - c <- +verifyReusableCode checkPw mPtpw convCode = do + (c, mPw) <- getCode (conversationKey convCode) DataTypes.ReusableCode >>= noteS @'CodeNotFound unless (DataTypes.codeValue c == conversationCode convCode) $ throwS @'CodeNotFound + case (checkPw, mPtpw, mPw) of + (True, Just ptpw, Just pw) -> + unless (verifyPassword ptpw pw) $ throwS @'InvalidConversationPassword + (True, Nothing, Just _) -> + throwS @'InvalidConversationPassword + (_, _, _) -> pure () pure c ensureConversationAccess :: diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index 8d75052d2d..093120d6a1 100644 --- a/services/galley/src/Galley/Cassandra.hs +++ b/services/galley/src/Galley/Cassandra.hs @@ -20,4 +20,4 @@ module Galley.Cassandra (schemaVersion) where import Imports schemaVersion :: Int32 -schemaVersion = 79 +schemaVersion = 80 diff --git a/services/galley/src/Galley/Cassandra/Code.hs b/services/galley/src/Galley/Cassandra/Code.hs index 754df8e747..c205733718 100644 --- a/services/galley/src/Galley/Cassandra/Code.hs +++ b/services/galley/src/Galley/Cassandra/Code.hs @@ -33,6 +33,7 @@ import Galley.Options import Imports import Polysemy import Polysemy.Input +import Wire.API.Password interpretCodeStoreToCassandra :: ( Member (Embed IO) r, @@ -43,7 +44,7 @@ interpretCodeStoreToCassandra :: Sem r a interpretCodeStoreToCassandra = interpret $ \case GetCode k s -> embedClient $ lookupCode k s - CreateCode code -> embedClient $ insertCode code + CreateCode code mPw -> embedClient $ insertCode code mPw DeleteCode k s -> embedClient $ deleteCode k s MakeKey cid -> Code.mkKey cid GenerateCode cid s t -> Code.generate cid s t @@ -51,18 +52,19 @@ interpretCodeStoreToCassandra = interpret $ \case view (options . optSettings . setConversationCodeURI) <$> input -- | Insert a conversation code -insertCode :: Code -> Client () -insertCode c = do +insertCode :: Code -> Maybe Password -> Client () +insertCode c mPw = do let k = codeKey c let v = codeValue c let cnv = codeConversation c let t = round (codeTTL c) let s = codeScope c - retry x5 (write Cql.insertCode (params LocalQuorum (k, v, cnv, s, t))) + retry x5 (write Cql.insertCode (params LocalQuorum (k, v, cnv, s, mPw, t))) -- | Lookup a conversation by code. -lookupCode :: Key -> Scope -> Client (Maybe Code) -lookupCode k s = fmap (toCode k s) <$> retry x1 (query1 Cql.lookupCode (params LocalQuorum (k, s))) +lookupCode :: Key -> Scope -> Client (Maybe (Code, Maybe Password)) +lookupCode k s = + fmap (toCode k s) <$> retry x1 (query1 Cql.lookupCode (params LocalQuorum (k, s))) -- | Delete a code associated with the given conversation key deleteCode :: Key -> Scope -> Client () diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 6796456070..db3b5b91a8 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -36,6 +36,7 @@ import Wire.API.Conversation.Role import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.MLS.PublicGroupState +import Wire.API.Password (Password) import Wire.API.Provider import Wire.API.Provider.Service import Wire.API.Routes.Internal.Galley.TeamsIntra @@ -285,11 +286,11 @@ updatePublicGroupState = "update conversation set public_group_state = ? where c -- Conversations accessible by code ----------------------------------------- -insertCode :: PrepQuery W (Key, Value, ConvId, Scope, Int32) () -insertCode = "INSERT INTO conversation_codes (key, value, conversation, scope) VALUES (?, ?, ?, ?) USING TTL ?" +insertCode :: PrepQuery W (Key, Value, ConvId, Scope, Maybe Password, Int32) () +insertCode = "INSERT INTO conversation_codes (key, value, conversation, scope, password) VALUES (?, ?, ?, ?, ?) USING TTL ?" -lookupCode :: PrepQuery R (Key, Scope) (Value, Int32, ConvId) -lookupCode = "SELECT value, ttl(value), conversation FROM conversation_codes WHERE key = ? AND scope = ?" +lookupCode :: PrepQuery R (Key, Scope) (Value, Int32, ConvId, Maybe Password) +lookupCode = "SELECT value, ttl(value), conversation, password FROM conversation_codes WHERE key = ? AND scope = ?" deleteCode :: PrepQuery W (Key, Scope) () deleteCode = "DELETE FROM conversation_codes WHERE key = ? AND scope = ?" diff --git a/services/galley/src/Galley/Data/Types.hs b/services/galley/src/Galley/Data/Types.hs index a314af11db..53fe356379 100644 --- a/services/galley/src/Galley/Data/Types.hs +++ b/services/galley/src/Galley/Data/Types.hs @@ -41,6 +41,7 @@ import Galley.Data.Scope import Imports import OpenSSL.EVP.Digest (digestBS, getDigestByName) import OpenSSL.Random (randBytes) +import Wire.API.Password (Password) -------------------------------------------------------------------------------- -- Code @@ -50,19 +51,23 @@ data Code = Code codeValue :: !Value, codeTTL :: !Timeout, codeConversation :: !ConvId, - codeScope :: !Scope + codeScope :: !Scope, + codeHasPassword :: !Bool } deriving (Eq, Show, Generic) -toCode :: Key -> Scope -> (Value, Int32, ConvId) -> Code -toCode k s (val, ttl, cnv) = - Code - { codeKey = k, - codeValue = val, - codeTTL = Timeout (fromIntegral ttl), - codeConversation = cnv, - codeScope = s - } +toCode :: Key -> Scope -> (Value, Int32, ConvId, Maybe Password) -> (Code, Maybe Password) +toCode k s (val, ttl, cnv, mPw) = + ( Code + { codeKey = k, + codeValue = val, + codeTTL = Timeout (fromIntegral ttl), + codeConversation = cnv, + codeScope = s, + codeHasPassword = isJust mPw + }, + mPw + ) -- Note on key/value used for a conversation Code -- @@ -81,7 +86,8 @@ generate cnv s t = do codeValue = val, codeConversation = cnv, codeTTL = t, - codeScope = s + codeScope = s, + codeHasPassword = False } mkKey :: MonadIO m => ConvId -> m Key diff --git a/services/galley/src/Galley/Effects/CodeStore.hs b/services/galley/src/Galley/Effects/CodeStore.hs index c4662be23e..88b31b0dfc 100644 --- a/services/galley/src/Galley/Effects/CodeStore.hs +++ b/services/galley/src/Galley/Effects/CodeStore.hs @@ -45,10 +45,11 @@ import Data.Misc import Galley.Data.Types import Imports import Polysemy +import Wire.API.Password data CodeStore m a where - CreateCode :: Code -> CodeStore m () - GetCode :: Key -> Scope -> CodeStore m (Maybe Code) + CreateCode :: Code -> Maybe Password -> CodeStore m () + GetCode :: Key -> Scope -> CodeStore m (Maybe (Code, Maybe Password)) DeleteCode :: Key -> Scope -> CodeStore m () MakeKey :: ConvId -> CodeStore m Key GenerateCode :: ConvId -> Scope -> Timeout -> CodeStore m Code diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 2b6fe71b34..6e12b9169f 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -55,6 +55,7 @@ import Data.List.NonEmpty (NonEmpty (..)) import Data.List1 hiding (head) import qualified Data.List1 as List1 import qualified Data.Map.Strict as Map +import Data.Misc import Data.Qualified import Data.Range import qualified Data.Set as Set @@ -227,7 +228,8 @@ tests s = test s "post message qualified - remote owning backend - success" postMessageQualifiedRemoteOwningBackendSuccess, test s "join conversation" postJoinConvOk, test s "get code-access conversation information" testJoinCodeConv, - test s "join code-access conversation" postJoinCodeConvOk, + test s "join code-access conversation - no password" postJoinCodeConvOk, + test s "join code-access conversation - password" postJoinCodeConvWithPassword, test s "convert invite to code-access conversation" postConvertCodeConv, test s "convert code to team-access conversation" postConvertTeamConv, test s "local and remote guests are removed when access changes" testAccessUpdateGuestRemoved, @@ -1332,7 +1334,7 @@ testJoinCodeConv = do qbob <- randomQualifiedUser let bob = qUnqualified qbob getJoinCodeConv bob (conversationKey cCode) (conversationCode cCode) !!! do - const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const (Right (ConversationCoverView convId (Just convName) False)) === responseJsonEither -- A user that would not be able to join conversation cannot view it either. eve <- ephemeralUser @@ -1398,7 +1400,7 @@ testJoinTeamConvGuestLinksDisabled = do -- guest can join if guest link feature is enabled checkFeatureStatus Public.FeatureStatusEnabled getJoinCodeConv eve (conversationKey cCode) (conversationCode cCode) !!! do - const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const (Right (ConversationCoverView convId (Just convName) False)) === responseJsonEither const 200 === statusCode postConvCodeCheck cCode !!! const 200 === statusCode postJoinCodeConv eve cCode !!! const 200 === statusCode @@ -1429,7 +1431,7 @@ testJoinTeamConvGuestLinksDisabled = do TeamFeatures.putTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley owner teamId enabled !!! do const 200 === statusCode getJoinCodeConv eve' (conversationKey cCode) (conversationCode cCode) !!! do - const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const (Right (ConversationCoverView convId (Just convName) False)) === responseJsonEither const 200 === statusCode postConvCodeCheck cCode !!! const 200 === statusCode postJoinCodeConv eve' cCode !!! const 200 === statusCode @@ -1450,7 +1452,7 @@ testJoinNonTeamConvGuestLinksDisabled = do -- works by default getJoinCodeConv userNotInTeam (conversationKey cCode) (conversationCode cCode) !!! do - const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const (Right (ConversationCoverView convId (Just convName) False)) === responseJsonEither const 200 === statusCode -- for non-team conversations it still works if status is disabled for the team but not server wide @@ -1459,7 +1461,7 @@ testJoinNonTeamConvGuestLinksDisabled = do const 200 === statusCode getJoinCodeConv userNotInTeam (conversationKey cCode) (conversationCode cCode) !!! do - const (Right (ConversationCoverView convId (Just convName))) === responseJsonEither + const (Right (ConversationCoverView convId (Just convName) False)) === responseJsonEither const 200 === statusCode -- @SF.Separation @TSFI.RESTfulAPI @S2 @@ -1482,6 +1484,7 @@ postJoinCodeConvOk = do conv <- decodeConvId <$> postConv alice [] (Just "gossip") [CodeAccess] (Just accessRoles) Nothing let qconv = Qualified conv (qDomain qbob) cCode <- decodeConvCodeEvent <$> postConvCode alice conv + liftIO $ conversationHasPassword cCode @?= Just False -- currently ConversationCode is used both as return type for POST ../code and as body for ../join -- POST /code gives code,key,uri -- POST /join expects code,key @@ -1517,6 +1520,27 @@ postJoinCodeConvOk = do -- @END +postJoinCodeConvWithPassword :: TestM () +postJoinCodeConvWithPassword = do + alice <- randomUser + qbob <- randomQualifiedUser + let bob = qUnqualified qbob + Right accessRoles <- liftIO $ genAccessRolesV2 [TeamMemberAccessRole, NonTeamMemberAccessRole] [GuestAccessRole] + conv <- decodeConvId <$> postConv alice [] (Just "gossip") [CodeAccess] (Just accessRoles) Nothing + let _qconv = Qualified conv (qDomain qbob) + let pw = plainTextPassword8Unsafe "password" + cCode <- decodeConvCodeEvent <$> postConvCode' (Just pw) alice conv + liftIO $ conversationHasPassword cCode @?= Just True + getJoinCodeConv bob (conversationKey cCode) (conversationCode cCode) !!! do + const (Right (ConversationCoverView conv (Just "gossip") True)) === responseJsonEither + const 200 === statusCode + -- join without password should fail + postJoinCodeConv' Nothing bob cCode !!! const 403 === statusCode + -- join with wrong password should fail + postJoinCodeConv' (Just (plainTextPassword8Unsafe "wrong-password")) bob cCode !!! const 403 === statusCode + -- join with correct password should succeed + postJoinCodeConv' (Just pw) bob cCode !!! const 200 === statusCode + postConvertCodeConv :: TestM () postConvertCodeConv = do c <- view tsCannon diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index b259d9b659..7062923e90 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -107,6 +107,7 @@ import Web.Cookie import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation.Action +import Wire.API.Conversation.Code (CreateConversationCodeRequest (..), JoinConversationByCode (JoinConversationByCode)) import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing @@ -1346,15 +1347,18 @@ postJoinConv u c = do . zType "access" postJoinCodeConv :: UserId -> ConversationCode -> TestM ResponseLBS -postJoinCodeConv u j = do +postJoinCodeConv = postJoinCodeConv' Nothing + +postJoinCodeConv' :: Maybe PlainTextPassword8 -> UserId -> ConversationCode -> TestM ResponseLBS +postJoinCodeConv' mPw u j = do g <- viewGalley post $ g - . paths ["/conversations", "join"] + . paths ["conversations", "join"] . zUser u . zConn "conn" . zType "access" - . json j + . json (JoinConversationByCode j mPw) putQualifiedAccessUpdate :: (MonadHttp m, HasGalley m, MonadIO m) => @@ -1406,7 +1410,10 @@ putMessageTimerUpdate u c acc = do . json acc postConvCode :: UserId -> ConvId -> TestM ResponseLBS -postConvCode u c = do +postConvCode = postConvCode' Nothing + +postConvCode' :: Maybe PlainTextPassword8 -> UserId -> ConvId -> TestM ResponseLBS +postConvCode' mPw u c = do g <- viewGalley post $ g @@ -1414,6 +1421,7 @@ postConvCode u c = do . zUser u . zConn "conn" . zType "access" + . json (CreateConversationCodeRequest mPw) postConvCodeCheck :: ConversationCode -> TestM ResponseLBS postConvCodeCheck code = do