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